Merge changes Ib99310a4,I3bae98bc into main

* changes:
  ModifierShortcutTest improvement
  Add flag for making ModifierShortcutManager multiuser aware.
diff --git a/BAL_OWNERS b/BAL_OWNERS
index d56a1d4..ec779e7 100644
--- a/BAL_OWNERS
+++ b/BAL_OWNERS
@@ -2,4 +2,6 @@
 achim@google.com
 topjohnwu@google.com
 lus@google.com
+haok@google.com
+wnan@google.com
 
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 dfa7206..ee03e4b 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -120,6 +120,7 @@
 import android.os.ThreadLocalWorkSource;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.os.WorkSource;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
@@ -1794,7 +1795,8 @@
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
 
         mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms();
-        mStartUserBeforeScheduledAlarms = Flags.startUserBeforeScheduledAlarms();
+        mStartUserBeforeScheduledAlarms = Flags.startUserBeforeScheduledAlarms()
+                && UserManager.supportsMultipleUsers();
         if (mStartUserBeforeScheduledAlarms) {
             mUserWakeupStore = new UserWakeupStore();
             mUserWakeupStore.init();
@@ -3015,7 +3017,7 @@
                     mUseFrozenStateToDropListenerAlarms);
             pw.println();
             pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS,
-                    mStartUserBeforeScheduledAlarms);
+                    Flags.startUserBeforeScheduledAlarms());
             pw.decreaseIndent();
             pw.println();
             pw.println();
diff --git a/api/coverage/tools/ExtractFlaggedApis.kt b/api/coverage/tools/ExtractFlaggedApis.kt
index bf67187..0a3ae4f 100644
--- a/api/coverage/tools/ExtractFlaggedApis.kt
+++ b/api/coverage/tools/ExtractFlaggedApis.kt
@@ -16,9 +16,9 @@
 
 package android.platform.coverage
 
+import com.android.tools.metalava.model.CallableItem
 import com.android.tools.metalava.model.ClassItem
 import com.android.tools.metalava.model.Item
-import com.android.tools.metalava.model.MethodItem
 import com.android.tools.metalava.model.text.ApiFile
 import java.io.File
 import java.io.FileWriter
@@ -40,24 +40,24 @@
 
 fun extractFlaggedApisFromClass(
     classItem: ClassItem,
-    methods: List<MethodItem>,
+    callables: List<CallableItem>,
     packageName: String,
     builder: FlagApiMap.Builder
 ) {
-    if (methods.isEmpty()) return
+    if (callables.isEmpty()) return
     val classFlag = getClassFlag(classItem)
-    for (method in methods) {
-        val methodFlag = getFlagAnnotation(method) ?: classFlag
+    for (callable in callables) {
+        val callableFlag = getFlagAnnotation(callable) ?: classFlag
         val api =
             JavaMethod.newBuilder()
                 .setPackageName(packageName)
                 .setClassName(classItem.fullName())
-                .setMethodName(method.name())
-        for (param in method.parameters()) {
+                .setMethodName(callable.name())
+        for (param in callable.parameters()) {
             api.addParameters(param.type().toTypeString())
         }
-        if (methodFlag != null) {
-            addFlaggedApi(builder, api, methodFlag)
+        if (callableFlag != null) {
+            addFlaggedApi(builder, api, callableFlag)
         }
     }
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index 9ad8dd3..0f23721 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -26593,7 +26593,7 @@
   public final class MediaProjectionManager {
     method @NonNull public android.content.Intent createScreenCaptureIntent();
     method @NonNull public android.content.Intent createScreenCaptureIntent(@NonNull android.media.projection.MediaProjectionConfig);
-    method public android.media.projection.MediaProjection getMediaProjection(int, @NonNull android.content.Intent);
+    method @Nullable public android.media.projection.MediaProjection getMediaProjection(int, @NonNull android.content.Intent);
   }
 
 }
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 2ca9f2e..44c4ab4 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1796,8 +1796,10 @@
   public class InputSettings {
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") public static int getAccessibilityBounceKeysThreshold(@NonNull android.content.Context);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") public static int getAccessibilitySlowKeysThreshold(@NonNull android.content.Context);
+    method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") public static boolean isAccessibilityMouseKeysEnabled(@NonNull android.content.Context);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") public static boolean isAccessibilityStickyKeysEnabled(@NonNull android.content.Context);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityBounceKeysThreshold(@NonNull android.content.Context, int);
+    method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityMouseKeysEnabled(@NonNull android.content.Context, boolean);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilitySlowKeysThreshold(@NonNull android.content.Context, int);
     method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityStickyKeysEnabled(@NonNull android.content.Context, boolean);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static void setMaximumObscuringOpacityForTouch(@NonNull android.content.Context, @FloatRange(from=0, to=1) float);
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 5a3a8d5..85b6e51b 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -5823,7 +5823,8 @@
      *
      * @param intent The intent to start.
      * @param requestCode If >= 0, this code will be returned in
-     *                    onActivityResult() when the activity exits.
+     *                    onActivityResult() when the activity exits;
+     *                    If < 0, no result will return when the activity exits.
      *
      * @throws android.content.ActivityNotFoundException
      *
@@ -5858,7 +5859,8 @@
      *
      * @param intent The intent to start.
      * @param requestCode If >= 0, this code will be returned in
-     *                    onActivityResult() when the activity exits.
+     *                    onActivityResult() when the activity exits;
+     *                    If < 0, no result will return when the activity exits.
      * @param options Additional options for how the Activity should be started.
      * See {@link android.content.Context#startActivity(Intent, Bundle)}
      * Context.startActivity(Intent, Bundle)} for more details.
@@ -5966,7 +5968,8 @@
      *
      * @param intent      The intent to start.
      * @param requestCode If >= 0, this code will be returned in
-     *                    onActivityResult() when the activity exits.
+     *                    onActivityResult() when the activity exits;
+     *                    If < 0, no result will return when the activity exits.
      * @param user        The user to start the intent as.
      * @hide Implement to provide correct calling token.
      */
@@ -6002,7 +6005,8 @@
      *
      * @param intent      The intent to start.
      * @param requestCode If >= 0, this code will be returned in
-     *                    onActivityResult() when the activity exits.
+     *                    onActivityResult() when the activity exits;
+     *                    If < 0, no result will return when the activity exits.
      * @param options     Additional options for how the Activity should be started. See {@link
      *                    android.content.Context#startActivity(Intent, Bundle)} for more details.
      * @param user        The user to start the intent as.
@@ -6040,7 +6044,8 @@
      *
      * @param intent      The intent to start.
      * @param requestCode If >= 0, this code will be returned in
-     *                    onActivityResult() when the activity exits.
+     *                    onActivityResult() when the activity exits;
+     *                    If < 0, no result will return when the activity exits.
      * @param options     Additional options for how the Activity should be started. See {@link
      *                    android.content.Context#startActivity(Intent, Bundle)} for more details.
      * @param user        The user to start the intent as.
@@ -6166,7 +6171,8 @@
      *
      * @param intent The IntentSender to launch.
      * @param requestCode If >= 0, this code will be returned in
-     *                    onActivityResult() when the activity exits.
+     *                    onActivityResult() when the activity exits;
+     *                    If < 0, no result will return when the activity exits.
      * @param fillInIntent If non-null, this will be provided as the
      * intent parameter to {@link IntentSender#sendIntent}.
      * @param flagsMask Intent flags in the original IntentSender that you
@@ -6205,7 +6211,8 @@
      *
      * @param intent The IntentSender to launch.
      * @param requestCode If >= 0, this code will be returned in
-     *                    onActivityResult() when the activity exits.
+     *                    onActivityResult() when the activity exits;
+     *                    If < 0, no result will return when the activity exits.
      * @param fillInIntent If non-null, this will be provided as the
      * intent parameter to {@link IntentSender#sendIntent}.
      * @param flagsMask Intent flags in the original IntentSender that you
@@ -6437,8 +6444,8 @@
      *
      * @param intent The intent to start.
      * @param requestCode If >= 0, this code will be returned in
-     *         onActivityResult() when the activity exits, as described in
-     *         {@link #startActivityForResult}.
+     *         onActivityResult() when the activity exits; If < 0, no result will
+     *         return when the activity exits, as described in {@link #startActivityForResult}.
      *
      * @return If a new activity was launched then true is returned; otherwise
      *         false is returned and you must handle the Intent yourself.
@@ -6469,7 +6476,8 @@
      *
      * @param intent The intent to start.
      * @param requestCode If >= 0, this code will be returned in
-     *         onActivityResult() when the activity exits, as described in
+     *         onActivityResult() when the activity exits; If < 0, no result
+     *         will return when the activity exits, as described in
      *         {@link #startActivityForResult}.
      * @param options Additional options for how the Activity should be started.
      * See {@link android.content.Context#startActivity(Intent, Bundle)}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 26ee4e8..c2c7b81 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -317,3 +317,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "modify_private_space_secondary_unlock_setup_flow"
+  namespace: "profile_experiences"
+  description: "Updates to setting up secondary unlock factor from Settings for the first time"
+  bug: "332850595"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
index 978a8f9..e3dbb2b 100644
--- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
+++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
@@ -747,6 +747,10 @@
      * {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList}.
      * </p>
      *
+     * <p>This function returns an empty array if
+     * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO}
+     * is not supported.</p>
+     *
      * @return an array of supported high speed video recording sizes
      * @see #getHighSpeedVideoFpsRangesFor(Size)
      * @see CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
@@ -836,6 +840,10 @@
      * supported for the same recording rate.</li>
      * </p>
      *
+     * <p>This function returns an empty array if
+     * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO}
+     * is not supported.</p>
+     *
      * @return an array of supported high speed video recording FPS ranges The upper bound of
      *         returned ranges is guaranteed to be larger or equal to 120.
      * @see #getHighSpeedVideoSizesFor
diff --git a/core/java/android/hardware/display/BrightnessInfo.java b/core/java/android/hardware/display/BrightnessInfo.java
index 6a96a54..c091062 100644
--- a/core/java/android/hardware/display/BrightnessInfo.java
+++ b/core/java/android/hardware/display/BrightnessInfo.java
@@ -60,8 +60,7 @@
     @IntDef(prefix = {"BRIGHTNESS_MAX_REASON_"}, value = {
             BRIGHTNESS_MAX_REASON_NONE,
             BRIGHTNESS_MAX_REASON_THERMAL,
-            BRIGHTNESS_MAX_REASON_POWER_IC,
-            BRIGHTNESS_MAX_REASON_WEAR_BEDTIME_MODE
+            BRIGHTNESS_MAX_REASON_POWER_IC
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface BrightnessMaxReason {}
@@ -158,8 +157,6 @@
                 return "thermal";
             case BRIGHTNESS_MAX_REASON_POWER_IC:
                 return "power IC";
-            case BRIGHTNESS_MAX_REASON_WEAR_BEDTIME_MODE:
-                return "wear bedtime";
         }
         return "invalid";
     }
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 0321e1df..97f6899 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -1669,6 +1669,46 @@
     }
 
     /**
+     * Gets the mapping between the doze brightness sensor values and brightness values. The doze
+     * brightness sensor is a light sensor used to determine the brightness while the device is
+     * dozing. Light sensor values are typically integers in the rage of 0-4. The returned values
+     * are between {@link PowerManager#BRIGHTNESS_MIN} and {@link PowerManager#BRIGHTNESS_MAX}, or
+     * -1 meaning that the current brightness should be kept.
+     * <p>
+     * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS}
+     * permission.
+     * </p>
+     *
+     * @param displayId The ID of the display
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+    @Nullable
+    public float[] getDozeBrightnessSensorValueToBrightness(int displayId) {
+        return mGlobal.getDozeBrightnessSensorValueToBrightness(displayId);
+    }
+
+    /**
+     * Gets the default doze brightness.
+     * The returned values are between {@link PowerManager#BRIGHTNESS_MIN} and
+     * {@link PowerManager#BRIGHTNESS_MAX}.
+     * <p>
+     * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS}
+     * permission.
+     * </p>
+     *
+     * @param displayId The ID of the display
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+    @FloatRange(from = 0f, to = 1f)
+    public float getDefaultDozeBrightness(int displayId) {
+        return mGlobal.getDefaultDozeBrightness(displayId);
+    }
+
+    /**
      * Listens for changes in available display devices.
      */
     public interface DisplayListener {
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index e9cd37a..cae33d0 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -21,6 +21,7 @@
 import static android.view.Display.HdrCapabilities.HdrType;
 
 import android.Manifest;
+import android.annotation.FloatRange;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -1226,6 +1227,32 @@
         }
     }
 
+    /**
+     * @see DisplayManager#getDozeBrightnessSensorValueToBrightness
+     */
+    @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+    @Nullable
+    public float[] getDozeBrightnessSensorValueToBrightness(int displayId) {
+        try {
+            return mDm.getDozeBrightnessSensorValueToBrightness(displayId);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @see DisplayManager#getDefaultDozeBrightness
+     */
+    @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+    @FloatRange(from = 0f, to = 1f)
+    public float getDefaultDozeBrightness(int displayId) {
+        try {
+            return mDm.getDefaultDozeBrightness(displayId);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
     private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub {
         @Override
         public void onDisplayEvent(int displayId, @DisplayEvent int event) {
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 77277ee..f3c21e9f 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -246,4 +246,12 @@
     // Restricts display modes to specified modeIds.
     @EnforcePermission("RESTRICT_DISPLAY_MODES")
     void requestDisplayModes(in IBinder token, int displayId, in @nullable int[] modeIds);
+
+    // Get the mapping between the doze brightness sensor values and brightness values
+    @EnforcePermission("CONTROL_DISPLAY_BRIGHTNESS")
+    float[] getDozeBrightnessSensorValueToBrightness(int displayId);
+
+    // Get the default doze brightness
+    @EnforcePermission("CONTROL_DISPLAY_BRIGHTNESS")
+    float getDefaultDozeBrightness(int displayId);
 }
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 4c5ebe7..bec1c9e 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -17,11 +17,13 @@
 package android.hardware.input;
 
 import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FLAG;
+import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS;
 import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG;
 import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG;
 import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
 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.input.flags.Flags.enableInputFilterRustImpl;
 
@@ -662,4 +664,64 @@
                 UserHandle.USER_CURRENT);
     }
 
+    /**
+     * Whether Accessibility mouse keys feature flag is enabled.
+     *
+     * <p>
+     * ‘Mouse keys’ is an accessibility feature to aid users who have physical disabilities,
+     * that allows the user to use the keys on the keyboard to control the mouse pointer and
+     * other perform other mouse functionality.
+     * </p>
+     *
+     * @hide
+     */
+    public static boolean isAccessibilityMouseKeysFeatureFlagEnabled() {
+        return keyboardA11yMouseKeys();
+    }
+
+    /**
+     * Whether Accessibility mouse keys is enabled.
+     *
+     * <p>
+     * ‘Mouse keys’ is an accessibility feature to aid users who have physical disabilities,
+     * that allows the user to use the keys on the keyboard to control the mouse pointer and
+     * other perform other mouse functionality.
+     * </p>
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(FLAG_KEYBOARD_A11Y_MOUSE_KEYS)
+    public static boolean isAccessibilityMouseKeysEnabled(@NonNull Context context) {
+        if (!isAccessibilityMouseKeysFeatureFlagEnabled()) {
+            return false;
+        }
+        return Settings.Secure.getIntForUser(context.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, 0, UserHandle.USER_CURRENT)
+                != 0;
+    }
+
+    /**
+     * Set Accessibility mouse keys feature enabled/disabled.
+     *
+     *  <p>
+     * ‘Mouse keys’ is an accessibility feature to aid users who have physical disabilities,
+     * that allows the user to use the keys on the keyboard to control the mouse pointer and
+     * other perform other mouse functionality.
+     * </p>
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(FLAG_KEYBOARD_A11Y_MOUSE_KEYS)
+    @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+    public static void setAccessibilityMouseKeysEnabled(@NonNull Context context,
+            boolean enabled) {
+        if (!isAccessibilityMouseKeysFeatureFlagEnabled()) {
+            return;
+        }
+        Settings.Secure.putIntForUser(context.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, enabled ? 1 : 0,
+                UserHandle.USER_CURRENT);
+    }
 }
diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java
index e2d215e..4bc5bd2 100644
--- a/core/java/android/inputmethodservice/AbstractInputMethodService.java
+++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java
@@ -29,6 +29,7 @@
 import android.view.MotionEvent;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethod;
 import android.view.inputmethod.InputMethodSession;
 import android.window.WindowProviderService;
@@ -186,6 +187,10 @@
             if (callback != null) {
                 callback.finishedEvent(seq, handled);
             }
+            if (Flags.imeSwitcherRevamp() && !handled && event.getAction() == KeyEvent.ACTION_DOWN
+                    && event.getUnicodeChar() > 0 && mInputMethodServiceInternal != null) {
+                mInputMethodServiceInternal.notifyUserActionIfNecessary();
+            }
         }
 
         /**
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 943b04f..855c309 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -16,7 +16,6 @@
 
 package android.inputmethodservice;
 
-import static android.view.inputmethod.Flags.predictiveBackIme;
 import static android.inputmethodservice.InputMethodServiceProto.CANDIDATES_VIEW_STARTED;
 import static android.inputmethodservice.InputMethodServiceProto.CANDIDATES_VISIBILITY;
 import static android.inputmethodservice.InputMethodServiceProto.CONFIGURATION;
@@ -57,6 +56,7 @@
 import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED;
 import static android.view.inputmethod.Flags.FLAG_CONNECTIONLESS_HANDWRITING;
 import static android.view.inputmethod.Flags.ctrlShiftShortcut;
+import static android.view.inputmethod.Flags.predictiveBackIme;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
@@ -4341,6 +4341,16 @@
     }
 
     /**
+     * Called when the IME switch button was clicked from the client. This will show the input
+     * method picker dialog.
+     *
+     * @hide
+     */
+    final void onImeSwitchButtonClickFromClient() {
+        mPrivOps.onImeSwitchButtonClickFromClient(getDisplayId());
+    }
+
+    /**
      * Used to inject custom {@link InputMethodServiceInternal}.
      *
      * @return the {@link InputMethodServiceInternal} to be used.
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index de67e06..3ce67b0 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -42,6 +42,7 @@
 import android.view.WindowInsetsController.Appearance;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
+import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
 
 import com.android.internal.inputmethod.InputMethodNavButtonFlags;
@@ -145,7 +146,8 @@
         return mImpl.toDebugString();
     }
 
-    private static final class Impl implements Callback, Window.DecorCallback {
+    private static final class Impl implements Callback, Window.DecorCallback,
+            NavigationBarView.ButtonClickListener {
         private static final int DEFAULT_COLOR_ADAPT_TRANSITION_TIME = 1700;
 
         // Copied from com.android.systemui.animation.Interpolators#LEGACY_DECELERATE
@@ -241,6 +243,7 @@
                                     ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN
                                     : 0);
                     navigationBarView.setNavigationIconHints(hints);
+                    navigationBarView.prepareNavButtons(this);
                 }
             } else {
                 mNavigationBarFrame.setLayoutParams(new FrameLayout.LayoutParams(
@@ -592,6 +595,17 @@
             return drawLegacyNavigationBarBackground;
         }
 
+        @Override
+        public void onImeSwitchButtonClick(View v) {
+            mService.onImeSwitchButtonClickFromClient();
+        }
+
+        @Override
+        public boolean onImeSwitchButtonLongClick(View v) {
+            v.getContext().getSystemService(InputMethodManager.class).showInputMethodPicker();
+            return true;
+        }
+
         /**
          * Returns the height of the IME caption bar if this should be shown, or {@code 0} instead.
          */
diff --git a/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java b/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java
index f423672..540243c 100644
--- a/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java
+++ b/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java
@@ -41,6 +41,7 @@
 import android.view.ViewConfiguration;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputConnection;
 import android.widget.ImageView;
 
@@ -58,12 +59,30 @@
     private int mTouchDownY;
     private AudioManager mAudioManager;
     private boolean mGestureAborted;
+    /**
+     * Whether the long click action has been invoked. The short click action is invoked on the up
+     * event while a long click is invoked as soon as the long press duration is reached, so a long
+     * click could be performed before the short click is checked, in which case the short click's
+     * action should not be invoked.
+     *
+     * @see View#mHasPerformedLongPress
+     */
+    private boolean mLongClicked;
     private OnClickListener mOnClickListener;
     private final KeyButtonRipple mRipple;
     private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
     private float mDarkIntensity;
     private boolean mHasOvalBg = false;
 
+    /** Runnable for checking whether the long click action should be performed. */
+    private final Runnable mCheckLongPress = new Runnable() {
+        public void run() {
+            if (isPressed() && performLongClick()) {
+                mLongClicked = true;
+            }
+        }
+    };
+
     public KeyButtonView(Context context, AttributeSet attrs) {
         super(context, attrs);
 
@@ -159,6 +178,7 @@
         switch (action) {
             case MotionEvent.ACTION_DOWN:
                 mDownTime = SystemClock.uptimeMillis();
+                mLongClicked = false;
                 setPressed(true);
 
                 // Use raw X and Y to detect gestures in case a parent changes the x and y values
@@ -173,6 +193,10 @@
                 if (!showSwipeUI) {
                     playSoundEffect(SoundEffectConstants.CLICK);
                 }
+                if (Flags.imeSwitcherRevamp() && isLongClickable()) {
+                    removeCallbacks(mCheckLongPress);
+                    postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
+                }
                 break;
             case MotionEvent.ACTION_MOVE:
                 x = (int) ev.getRawX();
@@ -183,6 +207,9 @@
                     // When quick step is enabled, prevent animating the ripple triggered by
                     // setPressed and decide to run it on touch up
                     setPressed(false);
+                    if (isLongClickable()) {
+                        removeCallbacks(mCheckLongPress);
+                    }
                 }
                 break;
             case MotionEvent.ACTION_CANCEL:
@@ -190,9 +217,12 @@
                 if (mCode != KEYCODE_UNKNOWN) {
                     sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
                 }
+                if (isLongClickable()) {
+                    removeCallbacks(mCheckLongPress);
+                }
                 break;
             case MotionEvent.ACTION_UP:
-                final boolean doIt = isPressed();
+                final boolean doIt = isPressed() && !mLongClicked;
                 setPressed(false);
                 final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
                 if (showSwipeUI) {
@@ -201,7 +231,7 @@
                         performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
                         playSoundEffect(SoundEffectConstants.CLICK);
                     }
-                } else if (doHapticFeedback) {
+                } else if (doHapticFeedback && !mLongClicked) {
                     // Always send a release ourselves because it doesn't seem to be sent elsewhere
                     // and it feels weird to sometimes get a release haptic and other times not.
                     performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
@@ -221,6 +251,9 @@
                         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
                     }
                 }
+                if (isLongClickable()) {
+                    removeCallbacks(mCheckLongPress);
+                }
                 break;
         }
 
diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
index e28f345..a3beaf4 100644
--- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
+++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
@@ -26,6 +26,7 @@
 import android.animation.PropertyValuesHolder;
 import android.annotation.DrawableRes;
 import android.annotation.FloatRange;
+import android.annotation.NonNull;
 import android.app.StatusBarManager;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -39,6 +40,7 @@
 import android.view.View;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
 
@@ -79,6 +81,28 @@
 
     private NavigationBarInflaterView mNavigationInflaterView;
 
+    /**
+     * Interface definition for callbacks to be invoked when navigation bar buttons are clicked.
+     */
+    public interface ButtonClickListener {
+
+        /**
+         * Called when the IME switch button is clicked.
+         *
+         * @param v The view that was clicked.
+         */
+        void onImeSwitchButtonClick(View v);
+
+        /**
+         * Called when the IME switch button has been clicked and held.
+         *
+         * @param v The view that was clicked and held.
+         *
+         * @return true if the callback consumed the long click, false otherwise.
+         */
+        boolean onImeSwitchButtonLongClick(View v);
+    }
+
     public NavigationBarView(Context context, AttributeSet attrs) {
         super(context, attrs);
 
@@ -98,13 +122,27 @@
                 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_home_handle));
 
         mDeadZone = new android.inputmethodservice.navigationbar.DeadZone(this);
+    }
 
+    /**
+     * Prepares the navigation bar buttons to be used and sets the on click listeners.
+     *
+     * @param listener The listener used to handle the clicks on the navigation bar buttons.
+     */
+    public void prepareNavButtons(@NonNull ButtonClickListener listener) {
         getBackButton().setLongClickable(false);
 
-        final ButtonDispatcher imeSwitchButton = getImeSwitchButton();
-        imeSwitchButton.setLongClickable(false);
-        imeSwitchButton.setOnClickListener(view -> view.getContext()
-                .getSystemService(InputMethodManager.class).showInputMethodPicker());
+        if (Flags.imeSwitcherRevamp()) {
+            final var imeSwitchButton = getImeSwitchButton();
+            imeSwitchButton.setLongClickable(true);
+            imeSwitchButton.setOnClickListener(listener::onImeSwitchButtonClick);
+            imeSwitchButton.setOnLongClickListener(listener::onImeSwitchButtonLongClick);
+        } else {
+            final ButtonDispatcher imeSwitchButton = getImeSwitchButton();
+            imeSwitchButton.setLongClickable(false);
+            imeSwitchButton.setOnClickListener(view -> view.getContext()
+                    .getSystemService(InputMethodManager.class).showInputMethodPicker());
+        }
     }
 
     @Override
diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl
index 8b1577c..97993b6 100644
--- a/core/java/android/os/IVibratorManagerService.aidl
+++ b/core/java/android/os/IVibratorManagerService.aidl
@@ -41,5 +41,5 @@
     // There is no order guarantee with respect to the two-way APIs above like
     // vibrate/isVibrating/cancel.
     oneway void performHapticFeedback(int uid, int deviceId, String opPkg, int constant,
-            boolean always, String reason, boolean fromIme);
+            String reason, int flags, int privFlags);
 }
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 406a1a6..026013c 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -564,8 +564,7 @@
             BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM,
             BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM,
             BRIGHTNESS_CONSTRAINT_TYPE_DEFAULT,
-            BRIGHTNESS_CONSTRAINT_TYPE_DIM,
-            BRIGHTNESS_CONSTRAINT_TYPE_DOZE
+            BRIGHTNESS_CONSTRAINT_TYPE_DIM
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface BrightnessConstraint{}
@@ -594,12 +593,6 @@
     public static final int BRIGHTNESS_CONSTRAINT_TYPE_DIM = 3;
 
     /**
-     * Brightness constraint type: minimum allowed value.
-     * @hide
-     */
-    public static final int BRIGHTNESS_CONSTRAINT_TYPE_DOZE = 4;
-
-    /**
      * @hide
      */
     @IntDef(prefix = { "WAKE_REASON_" }, value = {
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index 2a62c24..5339d73 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -206,13 +206,12 @@
     }
 
     @Override
-    public void performHapticFeedback(
-            int constant, boolean always, String reason, boolean fromIme) {
+    public void performHapticFeedback(int constant, String reason, int flags, int privFlags) {
         if (mVibratorManager == null) {
             Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager.");
             return;
         }
-        mVibratorManager.performHapticFeedback(constant, always, reason, fromIme);
+        mVibratorManager.performHapticFeedback(constant, reason, flags, privFlags);
     }
 
     @Override
diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java
index c80bcac..a9846ba 100644
--- a/core/java/android/os/SystemVibratorManager.java
+++ b/core/java/android/os/SystemVibratorManager.java
@@ -147,15 +147,14 @@
     }
 
     @Override
-    public void performHapticFeedback(int constant, boolean always, String reason,
-            boolean fromIme) {
+    public void performHapticFeedback(int constant, String reason, int flags, int privFlags) {
         if (mService == null) {
             Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager service.");
             return;
         }
         try {
-            mService.performHapticFeedback(
-                    mUid, mContext.getDeviceId(), mPackageName, constant, always, reason, fromIme);
+            mService.performHapticFeedback(mUid, mContext.getDeviceId(), mPackageName, constant,
+                    reason, flags, privFlags);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed to perform haptic feedback.", e);
         }
@@ -245,9 +244,8 @@
         }
 
         @Override
-        public void performHapticFeedback(int effectId, boolean always, String reason,
-                boolean fromIme) {
-            SystemVibratorManager.this.performHapticFeedback(effectId, always, reason, fromIme);
+        public void performHapticFeedback(int effectId, String reason, int flags, int privFlags) {
+            SystemVibratorManager.this.performHapticFeedback(effectId, reason, flags, privFlags);
         }
 
         @Override
diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java
index 8af371c..71c83f2 100644
--- a/core/java/android/os/Vibrator.java
+++ b/core/java/android/os/Vibrator.java
@@ -33,6 +33,7 @@
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibratorFrequencyProfile;
 import android.util.Log;
+import android.view.HapticFeedbackConstants;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -519,17 +520,15 @@
      *
      * @param constant the ID for the haptic feedback. This should be one of the constants defined
      *          in {@link HapticFeedbackConstants}.
-     * @param always {@code true} if the haptic feedback should be played regardless of the user
-     *          vibration intensity settings applicable to the corresponding vibration.
-     *          {@code false} if the vibration for the haptic feedback should respect the applicable
-     *          vibration intensity settings.
      * @param reason the reason for this haptic feedback.
-     * @param fromIme the haptic feedback is performed from an IME.
+     * @param flags Additional flags as per {@link HapticFeedbackConstants}.
+     * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}.
      *
      * @hide
      */
-    public void performHapticFeedback(int constant, boolean always, String reason,
-            boolean fromIme) {
+    public void performHapticFeedback(int constant, String reason,
+            @HapticFeedbackConstants.Flags int flags,
+            @HapticFeedbackConstants.PrivateFlags int privFlags) {
         Log.w(TAG, "performHapticFeedback is not supported");
     }
 
diff --git a/core/java/android/os/VibratorManager.java b/core/java/android/os/VibratorManager.java
index 513c4bd..2c7a852 100644
--- a/core/java/android/os/VibratorManager.java
+++ b/core/java/android/os/VibratorManager.java
@@ -23,6 +23,7 @@
 import android.app.ActivityThread;
 import android.content.Context;
 import android.util.Log;
+import android.view.HapticFeedbackConstants;
 
 /**
  * Provides access to all vibrators from the device, as well as the ability to run them
@@ -142,15 +143,14 @@
      *
      * @param constant the ID of the requested haptic feedback. Should be one of the constants
      *          defined in {@link HapticFeedbackConstants}.
-     * @param always {@code true} if the haptic feedback should be played regardless of the user
-     *          vibration intensity settings applicable to the corresponding vibration.
-     *          {@code false} otherwise.
      * @param reason the reason for this haptic feedback.
-     * @param fromIme the haptic feedback is performed from an IME.
+     * @param flags Additional flags as per {@link HapticFeedbackConstants}.
+     * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}.
      * @hide
      */
-    public void performHapticFeedback(int constant, boolean always, String reason,
-            boolean fromIme) {
+    public void performHapticFeedback(int constant, String reason,
+            @HapticFeedbackConstants.Flags int flags,
+            @HapticFeedbackConstants.PrivateFlags int privFlags) {
         Log.w(TAG, "performHapticFeedback is not supported");
     }
 
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index c73a422..ad2f59d 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -46,6 +46,7 @@
 flag {
     namespace: "haptics"
     name: "vibration_xml_apis"
+    is_exported: true
     description: "Enabled System APIs for vibration effect XML parser and serializer"
     bug: "347273158"
     metadata {
diff --git a/core/java/android/util/StateSet.java b/core/java/android/util/StateSet.java
index 16d6082..17adb32 100644
--- a/core/java/android/util/StateSet.java
+++ b/core/java/android/util/StateSet.java
@@ -288,6 +288,9 @@
             case R.attr.state_activated:
                 sb.append("A ");
                 break;
+            case R.attr.state_hovered:
+                sb.append("H ");
+                break;
             }
         }
 
diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java
index 69228ca..1fe06d4 100644
--- a/core/java/android/view/HapticFeedbackConstants.java
+++ b/core/java/android/view/HapticFeedbackConstants.java
@@ -16,11 +16,30 @@
 
 package android.view;
 
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Constants to be used to perform haptic feedback effects via
  * {@link View#performHapticFeedback(int)} 
  */
 public class HapticFeedbackConstants {
+    /** @hide **/
+    @IntDef(flag = true, prefix = "FLAG_", value = {
+            FLAG_IGNORE_VIEW_SETTING,
+            FLAG_IGNORE_GLOBAL_SETTING,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Flags {}
+
+    /** @hide **/
+    @IntDef(flag = true, prefix = "PRIVATE_FLAG_", value = {
+            PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PrivateFlags {}
 
     private HapticFeedbackConstants() {}
 
@@ -258,4 +277,14 @@
      */
     @Deprecated
     public static final int FLAG_IGNORE_GLOBAL_SETTING = 0x0002;
+
+    /**
+     * Flag for {@link android.os.Vibrator#performHapticFeedback(int, boolean, String, int, int)} or
+     * {@link ViewRootImpl#performHapticFeedback(int, boolean, int, int)}: Perform the haptic
+     * feedback with the input method vibration settings, e.g. applying the keyboard vibration
+     * user settings to the KEYBOARD_* constants.
+     *
+     * @hide
+     */
+    public static final int PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS = 0x0001;
 }
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 070d33b..14407ca 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -140,13 +140,13 @@
             int seqId);
 
     @UnsupportedAppUsage
-    boolean performHapticFeedback(int effectId, boolean always, boolean fromIme);
+    boolean performHapticFeedback(int effectId, int flags, int privFlags);
 
     /**
      * Called by attached views to perform predefined haptic feedback without requiring VIBRATE
      * permission.
      */
-    oneway void performHapticFeedbackAsync(int effectId, boolean always, boolean fromIme);
+    oneway void performHapticFeedbackAsync(int effectId, int flags, int privFlags);
 
     /**
      * Initiate the drag operation itself
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 88d7a83..2edbcc2 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -28636,20 +28636,20 @@
             return false;
         }
 
-        final boolean always = (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0;
-        boolean fromIme = false;
-        if (mAttachInfo.mViewRootImpl != null) {
-            fromIme = mAttachInfo.mViewRootImpl.mWindowAttributes.type == TYPE_INPUT_METHOD;
+        int privFlags = 0;
+        if (mAttachInfo.mViewRootImpl != null
+                && mAttachInfo.mViewRootImpl.mWindowAttributes.type == TYPE_INPUT_METHOD) {
+            privFlags = HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS;
         }
         if (Flags.useVibratorHapticFeedback()) {
             if (!mAttachInfo.canPerformHapticFeedback()) {
                 return false;
             }
-            getSystemVibrator().performHapticFeedback(
-                    feedbackConstant, always, "View#performHapticFeedback", fromIme);
+            getSystemVibrator().performHapticFeedback(feedbackConstant,
+                    "View#performHapticFeedback", flags, privFlags);
             return true;
         }
-        return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant, always, fromIme);
+        return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant, flags, privFlags);
     }
 
     private Vibrator getSystemVibrator() {
@@ -31684,7 +31684,10 @@
 
         interface Callbacks {
             void playSoundEffect(int effectId);
-            boolean performHapticFeedback(int effectId, boolean always, boolean fromIme);
+
+            boolean performHapticFeedback(int effectId,
+                    @HapticFeedbackConstants.Flags int flags,
+                    @HapticFeedbackConstants.PrivateFlags int privFlags);
         }
 
         /**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e5b17c8..596726f 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -9666,18 +9666,18 @@
      * {@inheritDoc}
      */
     @Override
-    public boolean performHapticFeedback(int effectId, boolean always, boolean fromIme) {
+    public boolean performHapticFeedback(int effectId, int flags, int privFlags) {
         if ((mDisplay.getFlags() & Display.FLAG_TOUCH_FEEDBACK_DISABLED) != 0) {
             return false;
         }
 
         try {
             if (USE_ASYNC_PERFORM_HAPTIC_FEEDBACK) {
-                mWindowSession.performHapticFeedbackAsync(effectId, always, fromIme);
+                mWindowSession.performHapticFeedbackAsync(effectId, flags, privFlags);
                 return true;
             } else {
                 // Original blocking binder call path.
-                return mWindowSession.performHapticFeedback(effectId, always, fromIme);
+                return mWindowSession.performHapticFeedback(effectId, flags, privFlags);
             }
         } catch (RemoteException e) {
             return false;
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 55f22a6..7871858 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -503,13 +503,13 @@
     }
 
     @Override
-    public boolean performHapticFeedback(int effectId, boolean always, boolean fromIme) {
+    public boolean performHapticFeedback(int effectId, int flags, int privFlags) {
         return false;
     }
 
     @Override
-    public void performHapticFeedbackAsync(int effectId, boolean always, boolean fromIme) {
-        performHapticFeedback(effectId, always, fromIme);
+    public void performHapticFeedbackAsync(int effectId, int flags, int privFlags) {
+        performHapticFeedback(effectId, flags, privFlags);
     }
 
     @Override
diff --git a/core/java/android/view/accessibility/a11ychecker/Android.bp b/core/java/android/view/accessibility/a11ychecker/Android.bp
deleted file mode 100644
index e5a577c..0000000
--- a/core/java/android/view/accessibility/a11ychecker/Android.bp
+++ /dev/null
@@ -1,7 +0,0 @@
-java_library_static {
-    name: "A11yChecker",
-    srcs: [
-        "*.java",
-    ],
-    visibility: ["//visibility:public"],
-}
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index e7a2fb9..07a9794 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -465,6 +465,20 @@
     }
 
     @AnyThread
+    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    static void onImeSwitchButtonClickFromSystem(int displayId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.onImeSwitchButtonClickFromSystem(displayId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
     @Nullable
     @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
     static InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) {
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 0c63e58..dbbfff0 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -4354,6 +4354,19 @@
     }
 
     /**
+     * Called when the IME switch button was clicked from the system. This will show the input
+     * method picker dialog.
+     *
+     * @param displayId The ID of the display where the input method picker dialog should be shown.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    public void onImeSwitchButtonClickFromSystem(int displayId) {
+        IInputMethodManagerGlobalInvoker.onImeSwitchButtonClickFromSystem(displayId);
+    }
+
+    /**
      * A test API for CTS to check whether there are any pending IME visibility requests.
      *
      * @return {@code true} iff there are pending IME visibility requests.
diff --git a/core/java/android/window/WindowMetricsController.java b/core/java/android/window/WindowMetricsController.java
index 739cf0e..0d5e37e 100644
--- a/core/java/android/window/WindowMetricsController.java
+++ b/core/java/android/window/WindowMetricsController.java
@@ -75,8 +75,8 @@
     /**
      * The core implementation to obtain {@link WindowMetrics}
      *
-     * @param isMaximum {@code true} to obtain {@link WindowManager#getCurrentWindowMetrics()}.
-     *                  {@code false} to obtain {@link WindowManager#getMaximumWindowMetrics()}.
+     * @param isMaximum {@code false} to obtain {@link WindowManager#getCurrentWindowMetrics()}.
+     *                  {@code true} to obtain {@link WindowManager#getMaximumWindowMetrics()}.
      */
     private WindowMetrics getWindowMetricsInternal(boolean isMaximum) {
         final Rect bounds;
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 1362f7b..3f1c06a 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -176,3 +176,10 @@
     description: "Enables desktop windowing app handle education"
     bug: "348208342"
 }
+
+flag {
+    name: "enable_compat_ui_visibility_status"
+    namespace: "lse_desktop_experience"
+    description: "Enables the tracking of the status for compat ui elements."
+    bug: "350953004"
+}
diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig
index 94f6503..6ce9725 100644
--- a/core/java/android/window/flags/responsible_apis.aconfig
+++ b/core/java/android/window/flags/responsible_apis.aconfig
@@ -58,6 +58,13 @@
 }
 
 flag {
+    name: "bal_additional_start_modes"
+    namespace: "responsible_apis"
+    description: "Introduce additional start modes."
+    bug: "352182359"
+}
+
+flag {
     name: "bal_send_intent_with_options"
     namespace: "responsible_apis"
     description: "Add options parameter to IntentSender.sendIntent."
diff --git a/core/java/com/android/internal/display/RefreshRateSettingsUtils.java b/core/java/com/android/internal/display/RefreshRateSettingsUtils.java
index c23a501..47e4943 100644
--- a/core/java/com/android/internal/display/RefreshRateSettingsUtils.java
+++ b/core/java/com/android/internal/display/RefreshRateSettingsUtils.java
@@ -83,4 +83,32 @@
         }
         return maxRefreshRate;
     }
+
+    /**
+     * Find the highest refresh rate among all the modes of all the built-in/physical displays.
+     *
+     * This method will acquire DisplayManager.mLock, so calling it while holding other locks
+     * should be done with care.
+     * @param context The context
+     * @return The highest refresh rate
+     */
+    public static float findHighestRefreshRateAmongAllBuiltInDisplays(Context context) {
+        final DisplayManager dm = context.getSystemService(DisplayManager.class);
+        final Display[] displays = dm.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
+        if (displays.length == 0) {
+            Log.w(TAG, "No valid display devices");
+            return DEFAULT_REFRESH_RATE;
+        }
+
+        float maxRefreshRate = DEFAULT_REFRESH_RATE;
+        for (Display display : displays) {
+            if (display.getType() != Display.TYPE_INTERNAL) continue;
+            for (Display.Mode mode : display.getSupportedModes()) {
+                if (mode.getRefreshRate() > maxRefreshRate) {
+                    maxRefreshRate = mode.getRefreshRate();
+                }
+            }
+        }
+        return maxRefreshRate;
+    }
 }
diff --git a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
index 63623c7..ac4c066 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
@@ -43,6 +43,7 @@
     void switchToPreviousInputMethod(in AndroidFuture future /* T=Boolean */);
     void switchToNextInputMethod(boolean onlyCurrentIme, in AndroidFuture future /* T=Boolean */);
     void shouldOfferSwitchingToNextInputMethod(in AndroidFuture future /* T=Boolean */);
+    void onImeSwitchButtonClickFromClient(int displayId);
     void notifyUserActionAsync();
     void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
             in ImeTracker.Token statsToken);
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 72c41be..2daf0fd 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -378,6 +378,22 @@
     }
 
     /**
+     * Calls {@link IInputMethodPrivilegedOperations#onImeSwitchButtonClickFromClient(int)}
+     */
+    @AnyThread
+    public void onImeSwitchButtonClickFromClient(int displayId) {
+        final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
+        if (ops == null) {
+            return;
+        }
+        try {
+            ops.onImeSwitchButtonClickFromClient(displayId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Calls {@link IInputMethodPrivilegedOperations#notifyUserActionAsync()}
      */
     @AnyThread
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 2b3ffeb2..cba27ce 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -135,6 +135,19 @@
             + "android.Manifest.permission.TEST_INPUT_METHOD)")
     boolean isInputMethodPickerShownForTest();
 
+    /**
+     * Called when the IME switch button was clicked from the system. Depending on the number of
+     * enabled IME subtypes, this will either switch to the next IME/subtype, or show the input
+     * method picker dialog.
+     *
+     * @param displayId The ID of the display where the input method picker dialog should be shown.
+     * @param userId    The ID of the user that triggered the click.
+     */
+    @EnforcePermission("WRITE_SECURE_SETTINGS")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+            + "android.Manifest.permission.WRITE_SECURE_SETTINGS)")
+    oneway void onImeSwitchButtonClickFromSystem(int displayId);
+
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
             + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
     @nullable InputMethodSubtype getCurrentInputMethodSubtype(int userId);
diff --git a/core/jni/android_view_InputChannel.cpp b/core/jni/android_view_InputChannel.cpp
index d11166f..e874163 100644
--- a/core/jni/android_view_InputChannel.cpp
+++ b/core/jni/android_view_InputChannel.cpp
@@ -16,18 +16,22 @@
 
 #define LOG_TAG "InputChannel-JNI"
 
-#include "android-base/stringprintf.h"
-#include <nativehelper/JNIHelp.h>
-#include "nativehelper/scoped_utf_chars.h"
+#include "android_view_InputChannel.h"
+
 #include <android_runtime/AndroidRuntime.h>
 #include <binder/Parcel.h>
-#include <utils/Log.h>
+#include <com_android_input_flags.h>
 #include <input/InputTransport.h>
-#include "android_view_InputChannel.h"
+#include <nativehelper/JNIHelp.h>
+#include <utils/Log.h>
+
+#include "android-base/stringprintf.h"
 #include "android_os_Parcel.h"
 #include "android_util_Binder.h"
-
 #include "core_jni_helpers.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace input_flags = com::android::input::flags;
 
 namespace android {
 
@@ -69,6 +73,9 @@
 }
 
 void NativeInputChannel::setDisposeCallback(InputChannelObjDisposeCallback callback, void* data) {
+    if (input_flags::remove_input_channel_from_windowstate()) {
+        return;
+    }
     mDisposeCallback = callback;
     mDisposeData = data;
 }
diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp
index 59d18b8..30c926c 100644
--- a/core/jni/platform/host/HostRuntime.cpp
+++ b/core/jni/platform/host/HostRuntime.cpp
@@ -104,6 +104,7 @@
 extern int register_android_view_KeyEvent(JNIEnv* env);
 extern int register_android_view_InputDevice(JNIEnv* env);
 extern int register_android_view_MotionEvent(JNIEnv* env);
+extern int register_android_view_Surface(JNIEnv* env);
 extern int register_android_view_ThreadedRenderer(JNIEnv* env);
 extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env);
 extern int register_android_view_VelocityTracker(JNIEnv* env);
@@ -151,6 +152,7 @@
         {"android.view.KeyEvent", REG_JNI(register_android_view_KeyEvent)},
         {"android.view.InputDevice", REG_JNI(register_android_view_InputDevice)},
         {"android.view.MotionEvent", REG_JNI(register_android_view_MotionEvent)},
+        {"android.view.Surface", REG_JNI(register_android_view_Surface)},
         {"android.view.VelocityTracker", REG_JNI(register_android_view_VelocityTracker)},
         {"com.android.internal.util.VirtualRefBasePtr",
          REG_JNI(register_com_android_internal_util_VirtualRefBasePtr)},
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index d277169..41696df 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -63,7 +63,6 @@
         "-c fa",
     ],
     static_libs: [
-        "A11yChecker",
         "collector-device-lib-platform",
         "frameworks-base-testutils",
         "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 033ac7c..c5b75ff 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import static android.util.SequenceUtils.getInitSeq;
+import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING;
 import static android.view.Surface.FRAME_RATE_CATEGORY_DEFAULT;
 import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
 import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
@@ -494,8 +495,8 @@
                 0, displayInfo, new DisplayAdjustments());
         ViewRootImpl viewRootImpl = new ViewRootImpl(sContext, display);
 
-        boolean result = viewRootImpl.performHapticFeedback(
-                HapticFeedbackConstants.CONTEXT_CLICK, true, false /* fromIme */);
+        boolean result = viewRootImpl.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK,
+                FLAG_IGNORE_GLOBAL_SETTING, 0 /* privFlags */);
 
         assertThat(result).isFalse();
     }
diff --git a/core/tests/coretests/src/android/view/accessibility/a11ychecker/OWNERS b/core/tests/coretests/src/android/view/accessibility/a11ychecker/OWNERS
deleted file mode 100644
index 872a180..0000000
--- a/core/tests/coretests/src/android/view/accessibility/a11ychecker/OWNERS
+++ /dev/null
@@ -1,5 +0,0 @@
-# Android Accessibility Framework owners
-include /core/java/android/view/accessibility/a11ychecker/OWNERS
-include /services/accessibility/OWNERS
-
-yaraabdullatif@google.com
diff --git a/keystore/java/android/security/KeyChain.java b/keystore/java/android/security/KeyChain.java
index 4b367e0..f9fd369 100644
--- a/keystore/java/android/security/KeyChain.java
+++ b/keystore/java/android/security/KeyChain.java
@@ -66,6 +66,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
 import javax.security.auth.x500.X500Principal;
@@ -376,6 +377,8 @@
      */
     public static final int KEY_ATTESTATION_FAILURE = 4;
 
+    private static final int BIND_KEY_CHAIN_SERVICE_TIMEOUT_MS = 30 * 1000;
+
     /**
      * Used by DPC or delegated app in
      * {@link android.app.admin.DeviceAdminReceiver#onChoosePrivateKeyAlias} or
@@ -1120,7 +1123,10 @@
             context.unbindService(keyChainServiceConnection);
             throw new AssertionError("could not bind to KeyChainService");
         }
-        countDownLatch.await();
+        if (!countDownLatch.await(BIND_KEY_CHAIN_SERVICE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            context.unbindService(keyChainServiceConnection);
+            throw new AssertionError("binding to KeyChainService timeout");
+        }
         IKeyChainService service = keyChainService.get();
         if (service != null) {
             return new KeyChainConnection(context, keyChainServiceConnection, service);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
index 196f89d..df80946 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
@@ -107,4 +107,24 @@
      * @param pilferCallback the callback to pilfer pointers.
      */
     void setPilferPointerCallback(Runnable pilferCallback);
+
+    /**
+     * Set a callback to requestTopUi.
+     * @param topUiRequest the callback to requestTopUi.
+     */
+    void setTopUiRequestCallback(TopUiRequest topUiRequest);
+
+    /**
+     * Callback to request SysUi to call
+     * {@link android.app.IActivityManager#setHasTopUi(boolean)}.
+     */
+    interface TopUiRequest {
+
+        /**
+         * Request {@link android.app.IActivityManager#setHasTopUi(boolean)} to be called.
+         * @param requestTopUi  whether topUi should be requested or not
+         * @param tag           tag of the request-source
+         */
+        void requestTopUi(boolean requestTopUi, String tag);
+    }
 }
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 8467e97..bb239ad 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
@@ -179,6 +179,7 @@
     @BackNavigationInfo.BackTargetType
     private int mPreviousNavigationType;
     private Runnable mPilferPointerCallback;
+    private BackAnimation.TopUiRequest mRequestTopUiCallback;
 
     public BackAnimationController(
             @NonNull ShellInit shellInit,
@@ -357,6 +358,11 @@
                 mPilferPointerCallback = callback;
             });
         }
+
+        @Override
+        public void setTopUiRequestCallback(TopUiRequest topUiRequest) {
+            mShellExecutor.execute(() -> mRequestTopUiCallback = topUiRequest);
+        }
     }
 
     private static class IBackAnimationImpl extends IBackAnimation.Stub
@@ -557,6 +563,7 @@
             if (!mShellBackAnimationRegistry.startGesture(backType)) {
                 mActiveCallback = null;
             }
+            requestTopUi(true, backType);
             tryPilferPointers();
         } else {
             mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
@@ -906,6 +913,7 @@
             mPreviousNavigationType = mBackNavigationInfo.getType();
             mBackNavigationInfo.onBackNavigationFinished(triggerBack);
             mBackNavigationInfo = null;
+            requestTopUi(false, mPreviousNavigationType);
         }
     }
 
@@ -969,6 +977,13 @@
         }
     }
 
+    private void requestTopUi(boolean hasTopUi, int backType) {
+        if (mRequestTopUiCallback != null && (backType == BackNavigationInfo.TYPE_CROSS_TASK
+                || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY)) {
+            mRequestTopUiCallback.requestTopUi(hasTopUi, TAG);
+        }
+    }
+
     /**
      * Validate animation targets.
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
index 4f04c5c..4e0c82b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -61,8 +61,7 @@
     private val context: Context,
     private val background: BackAnimationBackground,
     private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
-    protected val transaction: SurfaceControl.Transaction,
-    private val choreographer: Choreographer
+    protected val transaction: SurfaceControl.Transaction
 ) : ShellBackAnimation() {
 
     protected val startClosingRect = RectF()
@@ -269,7 +268,9 @@
             .setSpring(postCommitFlingSpring)
         flingAnimation.start()
         // do an animation-frame immediately to prevent idle frame
-        flingAnimation.doAnimationFrame(choreographer.lastFrameTimeNanos / TimeUtils.NANOS_PER_MS)
+        flingAnimation.doAnimationFrame(
+            Choreographer.getInstance().lastFrameTimeNanos / TimeUtils.NANOS_PER_MS
+        )
 
         val valueAnimator =
             ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration())
@@ -362,7 +363,7 @@
     }
 
     protected fun applyTransaction() {
-        transaction.setFrameTimelineVsync(choreographer.vsyncId)
+        transaction.setFrameTimelineVsync(Choreographer.getInstance().vsyncId)
         transaction.apply()
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index 103a654..e2b0513 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -52,7 +52,6 @@
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 import javax.inject.Inject;
 
@@ -69,7 +68,6 @@
  * IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back navigation to
  * launcher starts.
  */
-@ShellMainThread
 public class CrossTaskBackAnimation extends ShellBackAnimation {
     private static final int BACKGROUNDCOLOR = 0x43433A;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
index e266e2c..b02f97b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
@@ -19,7 +19,6 @@
 import android.graphics.Rect
 import android.graphics.RectF
 import android.util.MathUtils
-import android.view.Choreographer
 import android.view.SurfaceControl
 import android.view.animation.Animation
 import android.view.animation.Transformation
@@ -31,27 +30,23 @@
 import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.protolog.ShellProtoLogGroup
-import com.android.wm.shell.shared.annotations.ShellMainThread
 import javax.inject.Inject
 import kotlin.math.max
 import kotlin.math.min
 
 /** Class that handles customized predictive cross activity back animations. */
-@ShellMainThread
 class CustomCrossActivityBackAnimation(
     context: Context,
     background: BackAnimationBackground,
     rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
     transaction: SurfaceControl.Transaction,
-    choreographer: Choreographer,
     private val customAnimationLoader: CustomAnimationLoader
 ) :
     CrossActivityBackAnimation(
         context,
         background,
         rootTaskDisplayAreaOrganizer,
-        transaction,
-        choreographer
+        transaction
     ) {
 
     private var enterAnimation: Animation? = null
@@ -70,7 +65,6 @@
         background,
         rootTaskDisplayAreaOrganizer,
         SurfaceControl.Transaction(),
-        Choreographer.getInstance(),
         CustomAnimationLoader(
             TransitionAnimation(context, false /* debug */, "CustomCrossActivityBackAnimation")
         )
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
index 3b5eb36..c747e1e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
@@ -16,18 +16,15 @@
 package com.android.wm.shell.back
 
 import android.content.Context
-import android.view.Choreographer
 import android.view.SurfaceControl
 import android.window.BackEvent
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.animation.Interpolators
-import com.android.wm.shell.shared.annotations.ShellMainThread
 import javax.inject.Inject
 import kotlin.math.max
 
 /** Class that defines cross-activity animation. */
-@ShellMainThread
 class DefaultCrossActivityBackAnimation
 @Inject
 constructor(
@@ -39,8 +36,7 @@
         context,
         background,
         rootTaskDisplayAreaOrganizer,
-        SurfaceControl.Transaction(),
-        Choreographer.getInstance()
+        SurfaceControl.Transaction()
     ) {
 
     private val postCommitInterpolator = Interpolators.EMPHASIZED
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRepository.kt
new file mode 100644
index 0000000..cb54d89
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRepository.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.wm.shell.compatui.api
+
+/**
+ * Abstraction for the repository of all the available CompatUISpec
+ */
+interface CompatUIRepository {
+    /**
+     * Adds a {@link CompatUISpec} to the repository
+     * @throws IllegalStateException in case of illegal spec
+     */
+    fun addSpec(spec: CompatUISpec)
+
+    /**
+     * Iterates on the list of available {@link CompatUISpec} invoking
+     * fn for each of them.
+     */
+    fun iterateOn(fn: (CompatUISpec) -> Unit)
+
+    /**
+     * Returns the {@link CompatUISpec} for a given key
+     */
+    fun findSpec(name: String): CompatUISpec?
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt
new file mode 100644
index 0000000..24c2c8c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.api
+
+/**
+ * Describes each compat ui component to the framework.
+ */
+data class CompatUISpec(
+    // Unique name for the component. It's used for debug and for generating the
+    // unique component identifier in the system.
+    val name: String
+)
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
index a181eaf..8408ea6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
@@ -19,12 +19,15 @@
 import com.android.wm.shell.compatui.api.CompatUIEvent
 import com.android.wm.shell.compatui.api.CompatUIHandler
 import com.android.wm.shell.compatui.api.CompatUIInfo
+import com.android.wm.shell.compatui.api.CompatUIRepository
 import java.util.function.Consumer
 
 /**
  * Default implementation of {@link CompatUIHandler} to handle CompatUI components
  */
-class DefaultCompatUIHandler : CompatUIHandler {
+class DefaultCompatUIHandler(
+    private val compatUIRepository: CompatUIRepository
+) : CompatUIHandler {
 
     private var compatUIEventSender: Consumer<CompatUIEvent>? = null
     override fun onCompatInfoChanged(compatUIInfo: CompatUIInfo) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepository.kt
new file mode 100644
index 0000000..10d9425
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepository.kt
@@ -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 com.android.wm.shell.compatui.impl
+
+import com.android.wm.shell.compatui.api.CompatUIRepository
+import com.android.wm.shell.compatui.api.CompatUISpec
+
+/**
+ * Default {@link CompatUIRepository} implementation
+ */
+class DefaultCompatUIRepository : CompatUIRepository {
+
+    private val allSpecs = mutableMapOf<String, CompatUISpec>()
+
+    override fun addSpec(spec: CompatUISpec) {
+        if (allSpecs[spec.name] != null) {
+            throw IllegalStateException("Spec with id:${spec.name} already present")
+        }
+        allSpecs[spec.name] = spec
+    }
+
+    override fun iterateOn(fn: (CompatUISpec) -> Unit) =
+        allSpecs.values.forEach(fn)
+
+    override fun findSpec(name: String): CompatUISpec? =
+        allSpecs[name]
+}
\ No newline at end of file
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 9bdc0b2..4b548cb 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
@@ -72,7 +72,9 @@
 import com.android.wm.shell.compatui.CompatUIController;
 import com.android.wm.shell.compatui.CompatUIShellCommandHandler;
 import com.android.wm.shell.compatui.api.CompatUIHandler;
+import com.android.wm.shell.compatui.api.CompatUIRepository;
 import com.android.wm.shell.compatui.impl.DefaultCompatUIHandler;
+import com.android.wm.shell.compatui.impl.DefaultCompatUIRepository;
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
@@ -245,12 +247,13 @@
             Lazy<DockStateReader> dockStateReader,
             Lazy<CompatUIConfiguration> compatUIConfiguration,
             Lazy<CompatUIShellCommandHandler> compatUIShellCommandHandler,
-            Lazy<AccessibilityManager> accessibilityManager) {
+            Lazy<AccessibilityManager> accessibilityManager,
+            CompatUIRepository compatUIRepository) {
         if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) {
             return Optional.empty();
         }
         if (Flags.appCompatUiFramework()) {
-            return Optional.of(new DefaultCompatUIHandler());
+            return Optional.of(new DefaultCompatUIHandler(compatUIRepository));
         }
         return Optional.of(
                 new CompatUIController(
@@ -271,6 +274,12 @@
 
     @WMSingleton
     @Provides
+    static CompatUIRepository provideCompatUIRepository() {
+        return new DefaultCompatUIRepository();
+    }
+
+    @WMSingleton
+    @Provides
     static SyncTransactionQueue provideSyncTransactionQueue(TransactionPool pool,
             @ShellMainThread ShellExecutor mainExecutor) {
         return new SyncTransactionQueue(pool, mainExecutor);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index ca05864..247cc42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -88,14 +88,17 @@
     /** Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */
     fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
         visibleTasksListeners[visibleTasksListener] = executor
-        displayData.keyIterator().forEach { displayId ->
-            val visibleTasksCount = getVisibleTaskCount(displayId)
+        displayData.keyIterator().forEach {
             executor.execute {
-                visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount)
+                visibleTasksListener.onTasksVisibilityChanged(it, visibleTaskCount(it))
             }
         }
     }
 
+    /** Returns a list of all [DisplayData]. */
+    private fun displayDataList(): Sequence<DisplayData> =
+        displayData.valueIterator().asSequence()
+
     /**
      * Add a Consumer which will inform other classes of changes to exclusion regions for all
      * Desktop tasks.
@@ -208,37 +211,17 @@
         return removed
     }
 
-    /** Check if a task with the given [taskId] was marked as an active task */
-    fun isActiveTask(taskId: Int): Boolean {
-        return displayData.valueIterator().asSequence().any { data ->
-            data.activeTasks.contains(taskId)
-        }
-    }
-
-    /** Check if a task with the given [taskId] was marked as a closing task */
-    fun isClosingTask(taskId: Int): Boolean =
-        displayData.valueIterator().asSequence().any { data -> taskId in data.closingTasks }
-
-    /** Whether a task is visible. */
-    fun isVisibleTask(taskId: Int): Boolean {
-        return displayData.valueIterator().asSequence().any { data ->
-            data.visibleTasks.contains(taskId)
-        }
-    }
-
-    /** Return whether the given Task is minimized. */
-    fun isMinimizedTask(taskId: Int): Boolean {
-        return displayData.valueIterator().asSequence().any { data ->
-            data.minimizedTasks.contains(taskId)
-        }
-    }
+    fun isActiveTask(taskId: Int) = displayDataList().any { taskId in it.activeTasks }
+    fun isClosingTask(taskId: Int) = displayDataList().any { taskId in it.closingTasks }
+    fun isVisibleTask(taskId: Int) = displayDataList().any { taskId in it.visibleTasks }
+    fun isMinimizedTask(taskId: Int) = displayDataList().any { taskId in it.minimizedTasks }
 
     /**
      * Check if a task with the given [taskId] is the only visible, non-closing, not-minimized task
      * on its display
      */
     fun isOnlyVisibleNonClosingTask(taskId: Int): Boolean =
-        displayData.valueIterator().asSequence().any { data ->
+        displayDataList().any { data ->
             data.visibleTasks
                 .subtract(data.closingTasks)
                 .subtract(data.minimizedTasks)
@@ -255,12 +238,6 @@
         ArraySet(displayData[displayId]?.minimizedTasks)
 
     /**
-     * Returns whether Desktop Mode is currently showing any tasks, i.e. whether any Desktop Tasks
-     * are visible.
-     */
-    fun isDesktopModeShowing(displayId: Int): Boolean = getVisibleTaskCount(displayId) > 0
-
-    /**
      * Returns a list of Tasks IDs representing all active non-minimized Tasks on the given display,
      * ordered from front to back.
      */
@@ -305,14 +282,14 @@
             return
         }
 
-        val prevCount = getVisibleTaskCount(displayId)
+        val prevCount = visibleTaskCount(displayId)
         if (visible) {
             displayData.getOrCreate(displayId).visibleTasks.add(taskId)
             unminimizeTask(displayId, taskId)
         } else {
             displayData[displayId]?.visibleTasks?.remove(taskId)
         }
-        val newCount = getVisibleTaskCount(displayId)
+        val newCount = visibleTaskCount(displayId)
 
         // Check if count changed
         if (prevCount != newCount) {
@@ -340,7 +317,7 @@
     }
 
     /** Get number of tasks that are marked as visible on given [displayId] */
-    fun getVisibleTaskCount(displayId: Int): Int {
+    fun visibleTaskCount(displayId: Int): Int {
         ProtoLog.d(
             WM_SHELL_DESKTOP_MODE,
             "DesktopTaskRepo: visibleTaskCount= %d",
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 5807246..9a1a8a2 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
@@ -246,10 +246,12 @@
         }
     }
 
-    /** Get number of tasks that are marked as visible */
-    fun getVisibleTaskCount(displayId: Int): Int {
-        return desktopModeTaskRepository.getVisibleTaskCount(displayId)
-    }
+    /** Gets number of visible tasks in [displayId]. */
+    fun visibleTaskCount(displayId: Int): Int =
+        desktopModeTaskRepository.visibleTaskCount(displayId)
+
+    /** Returns true if any tasks are visible in Desktop Mode. */
+    fun isDesktopModeShowing(displayId: Int): Boolean = visibleTaskCount(displayId) > 0
 
     /** Enter desktop by using the focused task in given `displayId` */
     fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) {
@@ -981,7 +983,7 @@
             ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: skip keyguard is locked")
             return null
         }
-        if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) {
+        if (!isDesktopModeShowing(task.displayId)) {
             ProtoLog.d(
                 WM_SHELL_DESKTOP_MODE,
                 "DesktopTasksController: bring desktop tasks to front on transition" +
@@ -1012,7 +1014,7 @@
         transition: IBinder
     ): WindowContainerTransaction? {
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch")
-        if (desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) {
+        if (isDesktopModeShowing(task.displayId)) {
             ProtoLog.d(
                 WM_SHELL_DESKTOP_MODE,
                 "DesktopTasksController: switch fullscreen task to freeform on transition" +
@@ -1045,14 +1047,12 @@
 
     /** Handle task closing by removing wallpaper activity if it's the last active task */
     private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
-        val wct = if (
-            desktopModeTaskRepository.isOnlyVisibleNonClosingTask(task.taskId) &&
-                desktopModeTaskRepository.wallpaperActivityToken != null
-        ) {
+        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleTaskClosing")
+        val wct = WindowContainerTransaction()
+        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(task.taskId)
+            && desktopModeTaskRepository.wallpaperActivityToken != null) {
             // Remove wallpaper activity when the last active task is removed
-            WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) }
-        } else {
-            null
+            removeWallpaperActivity(wct)
         }
         if (!desktopModeTaskRepository.addClosingTask(task.displayId, task.taskId)) {
             // Could happen if the task hasn't been removed from closing list after it disappeared
@@ -1062,7 +1062,12 @@
                 task.taskId
             )
         }
-        return wct
+        // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task.
+        if (Flags.enableDesktopWindowingBackNavigation() &&
+            desktopModeTaskRepository.isVisibleTask(task.taskId)) {
+            wct.removeTask(task.token)
+        }
+        return if (wct.isEmpty) null else wct
     }
 
     private fun addMoveToDesktopChanges(
@@ -1393,8 +1398,7 @@
         onFinishCallback: Consumer<Boolean>
     ): Boolean {
         // TODO(b/320797628): Pass through which display we are dropping onto
-        val activeTasks = desktopModeTaskRepository.getActiveTasks(DEFAULT_DISPLAY)
-        if (!activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
+        if (!isDesktopModeShowing(DEFAULT_DISPLAY)) {
             // Not currently in desktop mode, ignore the drop
             return false
         }
@@ -1553,8 +1557,8 @@
             val result = IntArray(1)
             executeRemoteCallWithTaskPermission(
                 controller,
-                "getVisibleTaskCount",
-                { controller -> result[0] = controller.getVisibleTaskCount(displayId) },
+                "visibleTaskCount",
+                { controller -> result[0] = controller.visibleTaskCount(displayId) },
                 true /* blocking */
             )
             return result[0]
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index ff40d4c..8d63ff2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1980,12 +1980,6 @@
             }
             clearContentOverlay();
         }
-        if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
-            // Avoid double removal, which is fatal.
-            ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: trying to remove overlay (%s) while in UNDEFINED state", TAG, surface);
-            return;
-        }
         if (surface == null || !surface.isValid()) {
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: trying to remove invalid content overlay (%s)", TAG, surface);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 4104234..9bcd9b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -3573,7 +3573,8 @@
         pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible);
         pw.println(innerPrefix + "isSplitActive=" + isSplitActive());
         pw.println(innerPrefix + "isSplitVisible=" + isSplitScreenVisible());
-        pw.println(innerPrefix + "isLeftRightSplit=" + mSplitLayout.isLeftRightSplit());
+        pw.println(innerPrefix + "isLeftRightSplit="
+                + (mSplitLayout != null ? mSplitLayout.isLeftRightSplit() : "null"));
         pw.println(innerPrefix + "MainStage");
         pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition()));
         pw.println(childPrefix + "isActive=" + mMainStage.isActive());
@@ -3585,7 +3586,9 @@
         mSideStage.dump(pw, childPrefix);
         pw.println(innerPrefix + "SideStageListener");
         mSideStageListener.dump(pw, childPrefix);
-        mSplitLayout.dump(pw, childPrefix);
+        if (mSplitLayout != null) {
+            mSplitLayout.dump(pw, childPrefix);
+        }
         if (!mPausingTasks.isEmpty()) {
             pw.println(childPrefix + "mPausingTasks=" + mPausingTasks);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 4f4b809..766a6b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -353,7 +353,7 @@
                 return this::setRecentsTransitionDuringKeyguard;
             } else if (mDesktopTasksController != null
                     // Check on the default display. Recents/gesture nav is only available there
-                    && mDesktopTasksController.getVisibleTaskCount(DEFAULT_DISPLAY) > 0) {
+                    && mDesktopTasksController.visibleTaskCount(DEFAULT_DISPLAY) > 0) {
                 return this::setRecentsTransitionDuringDesktop;
             }
         }
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 1532211..b5b476d 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
@@ -18,6 +18,7 @@
 
 import static android.view.WindowManager.TRANSIT_CHANGE;
 
+import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW;
 import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW;
 
 import android.graphics.Point;
@@ -103,6 +104,9 @@
                 wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true);
                 mTaskOrganizer.applyTransaction(wct);
             }
+        } else {
+            mInteractionJankMonitor.begin(mDesktopWindowDecoration.mTaskSurface,
+                    mDesktopWindowDecoration.mContext, CUJ_DESKTOP_MODE_DRAG_WINDOW);
         }
         mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
         mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
@@ -157,11 +161,16 @@
             }
             mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_RESIZE_WINDOW);
         } else {
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
             DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
                     mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
-            wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
-            mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
+            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;
@@ -202,6 +211,7 @@
         mCtrlType = CTRL_TYPE_UNDEFINED;
         finishCallback.onTransitionFinished(null);
         mIsResizingOrAnimatingResize = false;
+        mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_DRAG_WINDOW);
         return true;
     }
 
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowMultiWindow.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowMultiWindow.kt
new file mode 100644
index 0000000..bbf0ce5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowMultiWindow.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.scenarios
+
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.helpers.MailAppHelper
+import com.android.server.wm.flicker.helpers.NewTasksAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+
+/** Base scenario test for window drag CUJ with multiple windows. */
+@Ignore("Base Test Class")
+abstract class DragAppWindowMultiWindow : DragAppWindowScenarioTestBase()
+{
+    private val imeAppHelper = ImeAppHelper(instrumentation)
+    private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+    private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation))
+    private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation))
+    private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation))
+
+    @Before
+    fun setup() {
+        Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+        testApp.enterDesktopWithDrag(wmHelper, device)
+        mailApp.launchViaIntent(wmHelper)
+        newTasksApp.launchViaIntent(wmHelper)
+        imeApp.launchViaIntent(wmHelper)
+    }
+
+    @Test
+    override fun dragAppWindow() {
+        val (startXIme, startYIme) = getWindowDragStartCoordinate(imeAppHelper)
+
+        imeApp.dragWindow(startXIme, startYIme,
+            endX = startXIme + 150, endY = startYIme + 150,
+            wmHelper, device)
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+        mailApp.exit(wmHelper)
+        newTasksApp.exit(wmHelper)
+        imeApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowScenarioTestBase.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowScenarioTestBase.kt
new file mode 100644
index 0000000..a613ca1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowScenarioTestBase.kt
@@ -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.wm.shell.flicker.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+/** Base test class for window drag CUJ. */
+@Ignore("Base Test Class")
+abstract class DragAppWindowScenarioTestBase {
+
+    val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    val tapl = LauncherInstrumentation()
+    val wmHelper = WindowManagerStateHelper(instrumentation)
+    val device = UiDevice.getInstance(instrumentation)
+
+    @Rule
+    @JvmField
+    val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0)
+
+    @Test abstract fun dragAppWindow()
+
+    /** Return the top-center coordinate of the app header as the start coordinate. */
+    fun getWindowDragStartCoordinate(appHelper: StandardAppHelper): Pair<Int, Int> {
+        val windowRect = wmHelper.getWindowRegion(appHelper).bounds
+        // Set start x-coordinate as center of app header.
+        val startX = windowRect.centerX()
+        val startY = windowRect.top
+        return Pair(startX, startY)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowSingleWindow.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowSingleWindow.kt
new file mode 100644
index 0000000..0655620
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowSingleWindow.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.scenarios
+
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+
+/** Base scenario test for window drag CUJ with single window. */
+@Ignore("Base Test Class")
+abstract class DragAppWindowSingleWindow : DragAppWindowScenarioTestBase()
+{
+    private val simpleAppHelper = SimpleAppHelper(instrumentation)
+    private val testApp = DesktopModeAppHelper(simpleAppHelper)
+
+    @Before
+    fun setup() {
+        Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+        testApp.enterDesktopWithDrag(wmHelper, device)
+    }
+
+    @Test
+    override fun dragAppWindow() {
+        val (startXTest, startYTest) = getWindowDragStartCoordinate(simpleAppHelper)
+        testApp.dragWindow(startXTest, startYTest,
+            endX = startXTest + 150, endY = startYTest + 150,
+            wmHelper, device)
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt
index 8bf0111..080ad90 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt
@@ -25,7 +25,6 @@
 import android.os.RemoteException
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.Choreographer
 import android.view.RemoteAnimationTarget
 import android.view.SurfaceControl
 import android.view.SurfaceControl.Transaction
@@ -37,8 +36,6 @@
 import com.android.internal.policy.TransitionAnimation
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTestCase
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 import junit.framework.TestCase.assertEquals
 import org.junit.Assert
 import org.junit.Before
@@ -50,12 +47,13 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 
 @SmallTest
 @TestableLooper.RunWithLooper
@@ -82,7 +80,6 @@
                 backAnimationBackground,
                 rootTaskDisplayAreaOrganizer,
                 transaction,
-                mock(Choreographer::class.java),
                 customAnimationLoader
             )
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt
new file mode 100644
index 0000000..1a86cfd
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.impl
+
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.compatui.api.CompatUIRepository
+import com.android.wm.shell.compatui.api.CompatUISpec
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for {@link DefaultCompatUIRepository}.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:DefaultCompatUIRepositoryTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class DefaultCompatUIRepositoryTest {
+
+    lateinit var repository: CompatUIRepository
+
+    @get:Rule
+    val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+    @Before
+    fun setUp() {
+        repository = DefaultCompatUIRepository()
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun `addSpec throws exception with specs with duplicate id`() {
+        repository.addSpec(CompatUISpec("one"))
+        repository.addSpec(CompatUISpec("one"))
+    }
+
+    @Test
+    fun `iterateOn invokes the consumer`() {
+        with(repository) {
+            addSpec(CompatUISpec("one"))
+            addSpec(CompatUISpec("two"))
+            addSpec(CompatUISpec("three"))
+            val consumer = object : (CompatUISpec) -> Unit {
+                var acc = ""
+                override fun invoke(spec: CompatUISpec) {
+                    acc += spec.name
+                }
+            }
+            iterateOn(consumer)
+            assertEquals("onetwothree", consumer.acc)
+        }
+    }
+
+    @Test
+    fun `findSpec returns existing specs`() {
+        with(repository) {
+            val one = CompatUISpec("one")
+            val two = CompatUISpec("two")
+            val three = CompatUISpec("three")
+            addSpec(one)
+            addSpec(two)
+            addSpec(three)
+            assertEquals(findSpec("one"), one)
+            assertEquals(findSpec("two"), two)
+            assertEquals(findSpec("three"), three)
+            assertNull(findSpec("abc"))
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIRepository.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIRepository.kt
new file mode 100644
index 0000000..cdc524a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIRepository.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.wm.shell.compatui.impl
+
+import com.android.wm.shell.compatui.api.CompatUIRepository
+import com.android.wm.shell.compatui.api.CompatUISpec
+
+/**
+ * Fake implementation for {@link CompatUIRepository}
+ */
+class FakeCompatUIRepository : CompatUIRepository {
+    val allSpecs = mutableMapOf<String, CompatUISpec>()
+    override fun addSpec(spec: CompatUISpec) {
+        if (findSpec(spec.name) != null) {
+            throw IllegalStateException("Spec with name:${spec.name} already present")
+        }
+        allSpecs[spec.name] = spec
+    }
+
+    override fun iterateOn(fn: (CompatUISpec) -> Unit) =
+        allSpecs.values.forEach(fn)
+
+    override fun findSpec(name: String): CompatUISpec? =
+        allSpecs[name]
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 6612aee..18b08bf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -337,65 +337,65 @@
     }
 
     @Test
-    fun getVisibleTaskCount() {
+    fun visibleTaskCount_defaultDisplay_returnsCorrectCount() {
         // No tasks, count is 0
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
 
         // New task increments count to 1
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
 
         // Visibility update to same task does not increase count
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
 
         // Second task visible increments count
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
 
         // Hiding a task decrements count
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
 
         // Hiding all tasks leaves count at 0
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false)
-        assertThat(repo.getVisibleTaskCount(displayId = 9)).isEqualTo(0)
+        assertThat(repo.visibleTaskCount(displayId = 9)).isEqualTo(0)
 
         // Hiding a not existing task, count remains at 0
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 999, visible = false)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
     }
 
     @Test
-    fun getVisibleTaskCount_multipleDisplays() {
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
-        assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0)
+    fun visibleTaskCount_multipleDisplays_returnsCorrectCount() {
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+        assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(0)
 
         // New task on default display increments count for that display only
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
-        assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+        assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(0)
 
         // New task on secondary display, increments count for that display only
         repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 2, visible = true)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
-        assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+        assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
 
         // Marking task visible on another display, updates counts for both displays
         repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
-        assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+        assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(2)
 
         // Marking task that is on secondary display, hidden on default display, does not affect
         // secondary display
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
-        assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+        assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(2)
 
         // Hiding a task on that display, decrements count
         repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = false)
-        assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
-        assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
+        assertThat(repo.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+        assertThat(repo.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
     }
 
     @Test
@@ -494,28 +494,6 @@
     }
 
     @Test
-    fun isDesktopModeShowing_noActiveTasks_returnsFalse() {
-        assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse()
-    }
-
-    @Test
-    fun isDesktopModeShowing_noTasksVisible_returnsFalse() {
-        repo.addActiveTask(displayId = 0, taskId = 1)
-        repo.addActiveTask(displayId = 0, taskId = 2)
-
-        assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse()
-    }
-
-    @Test
-    fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() {
-        repo.addActiveTask(displayId = 0, taskId = 1)
-        repo.addActiveTask(displayId = 0, taskId = 2)
-        repo.updateVisibleFreeformTasks(displayId = 0, taskId = 1, visible = true)
-
-        assertThat(repo.isDesktopModeShowing(displayId = 0)).isTrue()
-    }
-
-    @Test
     fun getActiveNonMinimizedTasksOrderedFrontToBack_returnsFreeformTasksInCorrectOrder() {
         repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1)
         repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2)
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 8558a77..da88686 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
@@ -20,7 +20,6 @@
 import android.app.ActivityManager.RunningTaskInfo
 import android.app.KeyguardManager
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
-import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
@@ -68,6 +67,7 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito.never
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.window.flags.Flags
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
 import com.android.wm.shell.MockToken
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
@@ -125,11 +125,11 @@
 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.quality.Strictness
+import org.mockito.Mockito.`when` as whenever
 
 /**
  * Test class for {@link DesktopTasksController}
@@ -311,6 +311,31 @@
   }
 
   @Test
+  fun isDesktopModeShowing_noTasks_returnsFalse() {
+    assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
+  }
+
+  @Test
+  fun isDesktopModeShowing_noTasksVisible_returnsFalse() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    markTaskHidden(task1)
+    markTaskHidden(task2)
+
+    assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
+  }
+
+  @Test
+  fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    markTaskVisible(task1)
+    markTaskHidden(task2)
+
+    assertThat(controller.isDesktopModeShowing(displayId = 0)).isTrue()
+  }
+
+  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() {
     val homeTask = setUpHomeTask(SECOND_DISPLAY)
@@ -526,32 +551,32 @@
   }
 
   @Test
-  fun getVisibleTaskCount_noTasks_returnsZero() {
-    assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+  fun visibleTaskCount_noTasks_returnsZero() {
+    assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
   }
 
   @Test
-  fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() {
+  fun visibleTaskCount_twoTasks_bothVisible_returnsTwo() {
     setUpHomeTask()
     setUpFreeformTask().also(::markTaskVisible)
     setUpFreeformTask().also(::markTaskVisible)
-    assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
+    assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
   }
 
   @Test
-  fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() {
+  fun visibleTaskCount_twoTasks_oneVisible_returnsOne() {
     setUpHomeTask()
     setUpFreeformTask().also(::markTaskVisible)
     setUpFreeformTask().also(::markTaskHidden)
-    assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+    assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
   }
 
   @Test
-  fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() {
+  fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() {
     setUpHomeTask()
     setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible)
     setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible)
-    assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
+    assertThat(controller.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
   }
 
   @Test
@@ -1451,7 +1476,7 @@
             .setActivityType(ACTIVITY_TYPE_STANDARD)
             .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
             .build()
-    val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE)
+    val transition = createTransition(task = task, type = TRANSIT_CLOSE)
     val result = controller.handleRequest(Binder(), transition)
     assertThat(result).isNull()
   }
@@ -1545,8 +1570,11 @@
   }
 
   @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() {
+  @DisableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
+  )
+  fun handleRequest_backTransition_singleActiveTaskNoToken_wallpaperDisabled_backNavDisabled_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -1555,8 +1583,22 @@
   }
 
   @Test
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_backTransition_singleActiveTaskNoToken_wallpaperEnabled_backNavEnabled_removesTask() {
+    val task = setUpFreeformTask()
+
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+    assertNotNull(result, "Should handle request").assertRemoveAt(0, task.token)
+  }
+
+  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_backTransition_singleActiveTaskNoToken_backNavigationDisabled_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -1565,8 +1607,11 @@
   }
 
   @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() {
+  @DisableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_backTransition_singleActiveTaskWithToken_wallpaperDisabled_backNavDisabled_doesNotHandle() {
     val task = setUpFreeformTask()
 
     desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
@@ -1576,22 +1621,42 @@
   }
 
   @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() {
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_backTransition_singleActiveTaskWithToken_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
     desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
 
-    assertNotNull(result, "Should handle request")
-      // Should create remove wallpaper transaction
-      .assertRemoveAt(index = 0, wallpaperToken)
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    result.assertRemoveAt(index = 1, task.token)
   }
 
   @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_multipleActiveTasksFlagDisabled_doesNotHandle() {
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_backTransition_singleActiveTaskWithToken_backNavigationDisabled_removesWallpaper() {
+    val task = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @DisableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_backTransition_multipleTasks_wallpaperDisabled_backNavDisabled_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -1602,8 +1667,24 @@
   }
 
   @Test
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_backTransition_multipleTasks_wallpaperEnabled_backNavEnabled_removesTask() {
+    val task1 = setUpFreeformTask()
+    setUpFreeformTask()
+
+    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token)
+  }
+
+  @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_multipleActiveTasksFlagEnabled_doesNotHandle() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_backTransition_multipleTasks_backNavigationDisabled_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -1614,8 +1695,11 @@
   }
 
   @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_multipleActiveTasksSingleNonClosing_handlesRequest() {
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_backTransition_multipleTasksSingleNonClosing_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1624,14 +1708,33 @@
     desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
 
-    assertNotNull(result, "Should handle request")
-      // Should create remove wallpaper transaction
-      .assertRemoveAt(index = 0, wallpaperToken)
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    result.assertRemoveAt(index = 1, task1.token)
   }
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_backTransition_multipleTasksSingleNonClosing_backNavigationDisabled_removesWallpaper() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_backTransition_multipleTasksSingleNonMinimized_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1640,24 +1743,53 @@
     desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
 
-    assertNotNull(result, "Should handle request")
-      // Should create remove wallpaper transaction
-      .assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() {
-    val task = setUpFreeformTask()
-
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
-    assertNull(result, "Should not handle request")
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    result.assertRemoveAt(index = 1, task1.token)
   }
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_backTransition_multipleTasksSingleNonMinimized_backNavigationDisabled_removesWallpaper() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_backTransition_nonMinimizadTask_wallpaperEnabled_backNavEnabled_removesWallpaper() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    // Task is being minimized so mark it as not visible.
+    desktopModeTaskRepository
+      .updateVisibleFreeformTasks(displayId = DEFAULT_DISPLAY, task2.taskId, false)
+    val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK))
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
+  @DisableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_closeTransition_singleActiveTaskNoToken_wallpaperDisabled_backNavDisabled_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -1666,8 +1798,35 @@
   }
 
   @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() {
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_closeTransition_singleActiveTaskNoToken_wallpaperEnabled_backNavEnabled_removesTask() {
+    val task = setUpFreeformTask()
+
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task.token)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_closeTransition_singleActiveTaskNoToken_backNavigationDisabled_doesNotHandle() {
+    val task = setUpFreeformTask()
+
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
+  @DisableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_closeTransition_singleActiveTaskWithToken_wallpaperDisabled_backNavDisabled_doesNotHandle() {
     val task = setUpFreeformTask()
 
     desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
@@ -1677,22 +1836,71 @@
   }
 
   @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() {
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_closeTransition_singleActiveTaskWithToken_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
     desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
 
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    result.assertRemoveAt(index = 1, task.token)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_closeTransition_singleActiveTaskWithToken_backNavigationDisabled_removesWallpaper() {
+    val task = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @DisableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_closeTransition_multipleTasks_wallpaperDisabled_backNavDisabled_doesNotHandle() {
+    val task1 = setUpFreeformTask()
+    setUpFreeformTask()
+
+    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_closeTransition_multipleTasks_wallpaperEnabled_backNavEnabled_removesTask() {
+    val task1 = setUpFreeformTask()
+    setUpFreeformTask()
+
+    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
     assertNotNull(result, "Should handle request")
-      // Should create remove wallpaper transaction
-      .assertRemoveAt(index = 0, wallpaperToken)
+    result.assertRemoveAt(index = 0, task1.token)
   }
 
   @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_multipleActiveTasksFlagDisabled_doesNotHandle() {
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_closeTransition_multipleTasksFlagEnabled_backNavigationDisabled_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -1703,20 +1911,11 @@
   }
 
   @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_multipleActiveTasksFlagEnabled_doesNotHandle() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_multipleActiveTasksSingleNonClosing_handlesRequest() {
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1725,14 +1924,33 @@
     desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
 
-    assertNotNull(result, "Should handle request")
-      // Should create remove wallpaper transaction
-      .assertRemoveAt(index = 0, wallpaperToken)
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    result.assertRemoveAt(index = 1, task1.token)
   }
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_backNavigationDisabled_removesWallpaper() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_closeTransition_multipleTasksOneNonMinimized_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1741,9 +1959,45 @@
     desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
 
-    assertNotNull(result, "Should handle request")
-      // Should create remove wallpaper transaction
-      .assertRemoveAt(index = 0, wallpaperToken)
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    result.assertRemoveAt(index = 1, task1.token)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+  fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_backNavigationDisabled_removesWallpaper() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+    // Should create remove wallpaper transaction
+    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @EnableFlags(
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+  )
+  fun handleRequest_closeTransition_minimizadTask_wallpaperEnabled_backNavEnabled_removesWallpaper() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    // Task is being minimized so mark it as not visible.
+    desktopModeTaskRepository
+      .updateVisibleFreeformTasks(displayId = DEFAULT_DISPLAY, task2.taskId, false)
+    val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK))
+
+    assertNull(result, "Should not handle request")
   }
 
   @Test
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index d42b256..d20b7f0 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -762,14 +762,14 @@
 
     void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);
 
-    oneway void startLoudnessCodecUpdates(int sessionId);
+    void startLoudnessCodecUpdates(int sessionId);
 
-    oneway void stopLoudnessCodecUpdates(int sessionId);
+    void stopLoudnessCodecUpdates(int sessionId);
 
-    oneway void addLoudnessCodecInfo(int sessionId, int mediaCodecHash,
+    void addLoudnessCodecInfo(int sessionId, int mediaCodecHash,
             in LoudnessCodecInfo codecInfo);
 
-    oneway void removeLoudnessCodecInfo(int sessionId, in LoudnessCodecInfo codecInfo);
+    void removeLoudnessCodecInfo(int sessionId, in LoudnessCodecInfo codecInfo);
 
     PersistableBundle getLoudnessParams(in LoudnessCodecInfo codecInfo);
 
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 4059291..999f40e5 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -312,6 +312,10 @@
          * <p>Once a MediaProjection has been stopped, it's up to the application to release any
          * resources it may be holding (e.g. releasing the {@link VirtualDisplay} and
          * {@link Surface}).
+         *
+         * <p>After this callback any call to
+         * {@link MediaProjection#createVirtualDisplay} will fail, even if no such
+         * {@link VirtualDisplay} was ever created for this MediaProjection session.
          */
         public void onStop() { }
 
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 7ed67dc..7a7137a 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -43,25 +43,31 @@
 /**
  * Manages the retrieval of certain types of {@link MediaProjection} tokens.
  *
- * <p><ol>An example flow of starting a media projection will be:
- *     <li>Declare a foreground service with the type {@code mediaProjection} in
- *     the {@code AndroidManifest.xml}.
- *     </li>
- *     <li>Create an intent by calling {@link MediaProjectionManager#createScreenCaptureIntent()}
- *         and pass this intent to {@link Activity#startActivityForResult(Intent, int)}.
- *     </li>
- *     <li>On getting {@link Activity#onActivityResult(int, int, Intent)},
- *         start the foreground service with the type
- *         {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}.
- *     </li>
- *     <li>Retrieve the media projection token by calling
- *         {@link MediaProjectionManager#getMediaProjection(int, Intent)} with the result code and
- *         intent from the {@link Activity#onActivityResult(int, int, Intent)} above.
- *     </li>
- *     <li>Start the screen capture session for media projection by calling
- *         {@link MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
- *         android.hardware.display.VirtualDisplay.Callback, Handler)}.
- *     </li>
+ * <p>
+ *
+ * <ol>
+ *   An example flow of starting a media projection will be:
+ *   <li>Declare a foreground service with the type {@code mediaProjection} in the {@code
+ *       AndroidManifest.xml}.
+ *   <li>Create an intent by calling {@link MediaProjectionManager#createScreenCaptureIntent()} and
+ *       pass this intent to {@link Activity#startActivityForResult(Intent, int)}.
+ *   <li>On getting {@link Activity#onActivityResult(int, int, Intent)}, start the foreground
+ *       service with the type {@link
+ *       android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}.
+ *   <li>Retrieve the media projection token by calling {@link
+ *       MediaProjectionManager#getMediaProjection(int, Intent)} with the result code and intent
+ *       from the {@link Activity#onActivityResult(int, int, Intent)} above.
+ *   <li>Register a {@link MediaProjection.Callback} by calling {@link
+ *       MediaProjection#registerCallback(MediaProjection.Callback, Handler)}. This is required to
+ *       receive notifications about when the {@link MediaProjection} or captured content changes
+ *       state. When receiving an `onStop()` callback, the client must clean up any resources it is
+ *       holding, e.g. the {@link VirtualDisplay} and {@link Surface}. The MediaProjection may
+ *       further no longer create any new {@link VirtualDisplay}s via {@link
+ *       MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
+ *       VirtualDisplay.Callback, Handler)}.
+ *   <li>Start the screen capture session for media projection by calling {@link
+ *       MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
+ *       android.hardware.display.VirtualDisplay.Callback, Handler)}.
  * </ol>
  */
 @SystemService(Context.MEDIA_PROJECTION_SERVICE)
@@ -257,6 +263,7 @@
      * @see <a href="/guide/topics/large-screens/media-projection">
      * Media projection developer guide</a>
      */
+    @Nullable
     public MediaProjection getMediaProjection(int resultCode, @NonNull Intent resultData) {
         if (resultCode != Activity.RESULT_OK || resultData == null) {
             return null;
diff --git a/nfc/java/android/nfc/AvailableNfcAntenna.java b/nfc/java/android/nfc/AvailableNfcAntenna.java
index 6e6512a..e76aeb0 100644
--- a/nfc/java/android/nfc/AvailableNfcAntenna.java
+++ b/nfc/java/android/nfc/AvailableNfcAntenna.java
@@ -28,13 +28,13 @@
 public final class AvailableNfcAntenna implements Parcelable {
     /**
      * Location of the antenna on the Y axis in millimeters.
-     * 0 is the bottom-left when the user is facing the screen
+     * 0 is the top-left when the user is facing the screen
      * and the device orientation is Portrait.
      */
     private final int mLocationX;
     /**
      * Location of the antenna on the Y axis in millimeters.
-     * 0 is the bottom-left when the user is facing the screen
+     * 0 is the top-left when the user is facing the screen
      * and the device orientation is Portrait.
      */
     private final int mLocationY;
@@ -46,7 +46,7 @@
 
     /**
      * Location of the antenna on the X axis in millimeters.
-     * 0 is the bottom-left when the user is facing the screen
+     * 0 is the top-left when the user is facing the screen
      * and the device orientation is Portrait.
      */
     public int getLocationX() {
@@ -55,7 +55,7 @@
 
     /**
      * Location of the antenna on the Y axis in millimeters.
-     * 0 is the bottom-left when the user is facing the screen
+     * 0 is the top-left when the user is facing the screen
      * and the device orientation is Portrait.
      */
     public int getLocationY() {
diff --git a/nfc/java/android/nfc/NfcAntennaInfo.java b/nfc/java/android/nfc/NfcAntennaInfo.java
index b002ca2..c57b2e0 100644
--- a/nfc/java/android/nfc/NfcAntennaInfo.java
+++ b/nfc/java/android/nfc/NfcAntennaInfo.java
@@ -64,9 +64,9 @@
 
     /**
      * Whether the device is foldable. When the device is foldable,
-     * the 0, 0 is considered to be bottom-left when the device is unfolded and
+     * the 0, 0 is considered to be top-left when the device is unfolded and
      * the screens are facing the user. For non-foldable devices 0, 0
-     * is bottom-left when the user is facing the screen.
+     * is top-left when the user is facing the screen.
      */
     public boolean isDeviceFoldable() {
         return mDeviceFoldable;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.kt
new file mode 100644
index 0000000..2eaa804
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManagerExt.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.settingslib.bluetooth
+
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+/** [Flow] for [BluetoothCallback] device profile connection state change events */
+val BluetoothEventManager.onProfileConnectionStateChanged: Flow<ProfileConnectionState>
+    get() = callbackFlow {
+        val callback =
+            object : BluetoothCallback {
+                override fun onProfileConnectionStateChanged(
+                    cachedDevice: CachedBluetoothDevice,
+                    @BluetoothCallback.ConnectionState state: Int,
+                    bluetoothProfile: Int
+                ) {
+                    launch { send(ProfileConnectionState(cachedDevice, state, bluetoothProfile)) }
+                }
+            }
+        registerCallback(callback)
+        awaitClose { unregisterCallback(callback) }
+    }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt
new file mode 100644
index 0000000..91a99ae
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.settingslib.bluetooth
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothLeBroadcastAssistant
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.bluetooth.BluetoothLeBroadcastReceiveState
+import com.android.internal.util.ConcurrentUtils
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+/** [Flow] for [BluetoothLeBroadcastAssistant.Callback] source connected/removed events */
+val LocalBluetoothLeBroadcastAssistant.onSourceConnectedOrRemoved: Flow<Unit>
+    get() = callbackFlow {
+        val callback =
+            object : BluetoothLeBroadcastAssistant.Callback {
+                override fun onReceiveStateChanged(
+                    sink: BluetoothDevice,
+                    sourceId: Int,
+                    state: BluetoothLeBroadcastReceiveState
+                ) {
+                    if (BluetoothUtils.isConnected(state)) {
+                        launch { send(Unit) }
+                    }
+                }
+
+                override fun onSourceRemoved(sink: BluetoothDevice, sourceId: Int, reason: Int) {
+                    launch { send(Unit) }
+                }
+
+                override fun onSearchStarted(reason: Int) {}
+
+                override fun onSearchStartFailed(reason: Int) {}
+
+                override fun onSearchStopped(reason: Int) {}
+
+                override fun onSearchStopFailed(reason: Int) {}
+
+                override fun onSourceFound(source: BluetoothLeBroadcastMetadata) {}
+
+                override fun onSourceAdded(sink: BluetoothDevice, sourceId: Int, reason: Int) {}
+
+                override fun onSourceAddFailed(
+                    sink: BluetoothDevice,
+                    source: BluetoothLeBroadcastMetadata,
+                    reason: Int
+                ) {}
+
+                override fun onSourceModified(sink: BluetoothDevice, sourceId: Int, reason: Int) {}
+
+                override fun onSourceModifyFailed(
+                    sink: BluetoothDevice,
+                    sourceId: Int,
+                    reason: Int
+                ) {}
+
+                override fun onSourceRemoveFailed(
+                    sink: BluetoothDevice,
+                    sourceId: Int,
+                    reason: Int
+                ) {}
+            }
+        registerServiceCallBack(
+            ConcurrentUtils.DIRECT_EXECUTOR,
+            callback,
+        )
+        awaitClose { unregisterServiceCallBack(callback) }
+    }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt
new file mode 100644
index 0000000..45aaa66
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/ProfileConnectionState.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.settingslib.bluetooth
+
+data class ProfileConnectionState(
+    val cachedDevice: CachedBluetoothDevice,
+    @BluetoothCallback.ConnectionState val state: Int,
+    val bluetoothProfile: Int,
+)
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index ef94526..b2fcb5f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -16,7 +16,9 @@
 
 package com.android.settingslib.notification.data.repository
 
+import android.annotation.SuppressLint
 import android.app.NotificationManager
+import android.app.NotificationManager.EXTRA_NOTIFICATION_POLICY
 import android.content.BroadcastReceiver
 import android.content.ContentResolver
 import android.content.Context
@@ -57,6 +59,7 @@
     val modes: Flow<List<ZenMode>>
 }
 
+@SuppressLint("SharedFlowCreation")
 class ZenModeRepositoryImpl(
     private val context: Context,
     private val notificationManager: NotificationManager,
@@ -74,7 +77,7 @@
                 val receiver =
                     object : BroadcastReceiver() {
                         override fun onReceive(context: Context?, intent: Intent?) {
-                            intent?.action?.let { action -> launch { send(action) } }
+                            intent?.let { launch { send(it) } }
                         }
                     }
 
@@ -99,12 +102,11 @@
             }
             .let {
                 if (Flags.volumePanelBroadcastFix()) {
+                    // Share the flow to avoid having multiple broadcasts.
                     it.flowOn(backgroundCoroutineContext)
+                        .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
                 } else {
-                    it.shareIn(
-                        started = SharingStarted.WhileSubscribed(),
-                        scope = scope,
-                    )
+                    it.shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
                 }
             }
     }
@@ -112,7 +114,9 @@
     override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy {
         if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi())
             flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) {
-                notificationManager.consolidatedNotificationPolicy
+                // If available, get the value from extras to avoid a potential binder call.
+                it?.extras?.getParcelable(EXTRA_NOTIFICATION_POLICY)
+                    ?: notificationManager.consolidatedNotificationPolicy
             }
         else
             flowFromBroadcast(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) {
@@ -126,11 +130,11 @@
         }
     }
 
-    private fun <T> flowFromBroadcast(intentAction: String, mapper: () -> T) =
+    private fun <T> flowFromBroadcast(intentAction: String, mapper: (Intent?) -> T) =
         notificationBroadcasts
-            .filter { intentAction == it }
-            .map { mapper() }
-            .onStart { emit(mapper()) }
+            .filter { intentAction == it.action }
+            .map { mapper(it) }
+            .onStart { emit(mapper(null)) }
             .flowOn(backgroundCoroutineContext)
             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
index 9dbf23e..eb33a7a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
@@ -16,33 +16,84 @@
 
 package com.android.settingslib.volume.data.repository
 
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothCsipSetCoordinator
+import android.bluetooth.BluetoothDevice
 import android.bluetooth.BluetoothLeBroadcast
 import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.bluetooth.BluetoothProfile
+import android.bluetooth.BluetoothVolumeControl
+import android.content.ContentResolver
+import android.content.Context
+import android.database.ContentObserver
+import android.provider.Settings
+import androidx.annotation.IntRange
 import com.android.internal.util.ConcurrentUtils
+import com.android.settingslib.bluetooth.BluetoothUtils
 import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.onProfileConnectionStateChanged
+import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved
 import com.android.settingslib.flags.Flags
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN
 import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.runningFold
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+typealias GroupIdToVolumes = Map<Int, Int>
 
 /** Provides audio sharing functionality. */
 interface AudioSharingRepository {
     /** Whether the device is in audio sharing. */
     val inAudioSharing: Flow<Boolean>
+
+    /** The secondary headset groupId in audio sharing. */
+    val secondaryGroupId: StateFlow<Int>
+
+    /** The headset groupId to volume map during audio sharing. */
+    val volumeMap: StateFlow<GroupIdToVolumes>
+
+    /** Set the volume of secondary headset during audio sharing. */
+    suspend fun setSecondaryVolume(
+        @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
+        volume: Int
+    )
+
+    companion object {
+        const val AUDIO_SHARING_VOLUME_MIN = 0
+        const val AUDIO_SHARING_VOLUME_MAX = 255
+    }
 }
 
+@OptIn(ExperimentalCoroutinesApi::class)
 class AudioSharingRepositoryImpl(
-    private val localBluetoothManager: LocalBluetoothManager?,
-    backgroundCoroutineContext: CoroutineContext,
+    private val context: Context,
+    private val contentResolver: ContentResolver,
+    private val btManager: LocalBluetoothManager?,
+    private val coroutineScope: CoroutineScope,
+    private val backgroundCoroutineContext: CoroutineContext,
 ) : AudioSharingRepository {
     override val inAudioSharing: Flow<Boolean> =
         if (Flags.enableLeAudioSharing()) {
-            localBluetoothManager?.profileManager?.leAudioBroadcastProfile?.let { leBroadcast ->
+            btManager?.profileManager?.leAudioBroadcastProfile?.let { leBroadcast ->
                 callbackFlow {
                         val listener =
                             object : BluetoothLeBroadcast.Callback {
@@ -92,9 +143,117 @@
             flowOf(false)
         }
 
+    private val primaryChange: Flow<Unit> = callbackFlow {
+        val callback =
+            object : ContentObserver(null) {
+                override fun onChange(selfChange: Boolean) {
+                    launch { send(Unit) }
+                }
+            }
+        contentResolver.registerContentObserver(
+            Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()),
+            false,
+            callback)
+        awaitClose { contentResolver.unregisterContentObserver(callback) }
+    }
+
+    override val secondaryGroupId: StateFlow<Int> =
+        if (Flags.volumeDialogAudioSharingFix()) {
+                merge(
+                        btManager
+                            ?.profileManager
+                            ?.leAudioBroadcastAssistantProfile
+                            ?.onSourceConnectedOrRemoved
+                            ?.map { getSecondaryGroupId() } ?: emptyFlow(),
+                        btManager
+                            ?.eventManager
+                            ?.onProfileConnectionStateChanged
+                            ?.filter { profileConnection ->
+                                profileConnection.state == BluetoothAdapter.STATE_DISCONNECTED &&
+                                    profileConnection.bluetoothProfile ==
+                                        BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
+                            }
+                            ?.map { getSecondaryGroupId() } ?: emptyFlow(),
+                        primaryChange.map { getSecondaryGroupId() })
+                    .onStart { emit(getSecondaryGroupId()) }
+                    .distinctUntilChanged()
+                    .flowOn(backgroundCoroutineContext)
+            } else {
+                emptyFlow()
+            }
+            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), getSecondaryGroupId())
+
+    override val volumeMap: StateFlow<GroupIdToVolumes> =
+        if (Flags.volumeDialogAudioSharingFix()) {
+            btManager?.profileManager?.volumeControlProfile?.let { volumeControl ->
+                inAudioSharing.flatMapLatest { isSharing ->
+                    if (isSharing) {
+                        callbackFlow {
+                                val callback =
+                                    object : BluetoothVolumeControl.Callback {
+                                        override fun onDeviceVolumeChanged(
+                                            device: BluetoothDevice,
+                                            @IntRange(
+                                                from = AUDIO_SHARING_VOLUME_MIN.toLong(),
+                                                to = AUDIO_SHARING_VOLUME_MAX.toLong())
+                                            volume: Int
+                                        ) {
+                                            launch { send(Pair(device, volume)) }
+                                        }
+                                    }
+                                // Once registered, we will receive the initial volume of all
+                                // connected BT devices on VolumeControlProfile via callbacks
+                                volumeControl.registerCallback(
+                                    ConcurrentUtils.DIRECT_EXECUTOR, callback)
+                                awaitClose { volumeControl.unregisterCallback(callback) }
+                            }
+                            .runningFold(emptyMap<Int, Int>()) { acc, value ->
+                                val groupId =
+                                    BluetoothUtils.getGroupId(
+                                        btManager.cachedDeviceManager?.findDevice(value.first))
+                                if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+                                    acc + Pair(groupId, value.second)
+                                } else {
+                                    acc
+                                }
+                            }
+                            .distinctUntilChanged()
+                            .flowOn(backgroundCoroutineContext)
+                    } else {
+                        emptyFlow()
+                    }
+                }
+            } ?: emptyFlow()
+        } else {
+            emptyFlow()
+        }
+        .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap())
+
+    override suspend fun setSecondaryVolume(
+        @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
+        volume: Int
+    ) {
+        withContext(backgroundCoroutineContext) {
+            if (Flags.volumeDialogAudioSharingFix()) {
+                btManager?.profileManager?.volumeControlProfile?.let {
+                    // Find secondary headset and set volume.
+                    val cachedDevice =
+                        BluetoothUtils.getSecondaryDeviceForBroadcast(context, btManager)
+                    if (cachedDevice != null) {
+                        it.setDeviceVolume(cachedDevice.device, volume, /* isGroupOp= */ true)
+                    }
+                }
+            }
+        }
+    }
+
     private fun isBroadcasting(): Boolean {
         return Flags.enableLeAudioSharing() &&
-            (localBluetoothManager?.profileManager?.leAudioBroadcastProfile?.isEnabled(null)
-                ?: false)
+            (btManager?.profileManager?.leAudioBroadcastProfile?.isEnabled(null) ?: false)
+    }
+
+    private fun getSecondaryGroupId(): Int {
+        return BluetoothUtils.getGroupId(
+            BluetoothUtils.getSecondaryDeviceForBroadcast(context, btManager))
     }
 }
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt
index 1c80ef4..000664d 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt
@@ -16,15 +16,33 @@
 
 package com.android.settingslib.volume.data.repository
 
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
 import android.bluetooth.BluetoothLeBroadcast
+import android.bluetooth.BluetoothLeBroadcastAssistant
+import android.bluetooth.BluetoothLeBroadcastReceiveState
+import android.bluetooth.BluetoothProfile
+import android.bluetooth.BluetoothVolumeControl
+import android.content.ContentResolver
+import android.content.Context
+import android.database.ContentObserver
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.Settings
+import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.BluetoothEventManager
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.settingslib.bluetooth.VolumeControlProfile
 import com.android.settingslib.flags.Flags
 import com.google.common.truth.Truth
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -39,6 +57,9 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.never
@@ -52,27 +73,76 @@
 @RunWith(AndroidJUnit4::class)
 class AudioSharingRepositoryTest {
     @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
+
     @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
 
-    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
-    @Mock private lateinit var localBluetoothProfileManager: LocalBluetoothProfileManager
-    @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
+    @Mock private lateinit var btManager: LocalBluetoothManager
+
+    @Mock private lateinit var profileManager: LocalBluetoothProfileManager
+
+    @Mock private lateinit var broadcast: LocalBluetoothLeBroadcast
+
+    @Mock private lateinit var assistant: LocalBluetoothLeBroadcastAssistant
+
+    @Mock private lateinit var volumeControl: VolumeControlProfile
+
+    @Mock private lateinit var eventManager: BluetoothEventManager
+
+    @Mock private lateinit var deviceManager: CachedBluetoothDeviceManager
+
+    @Mock private lateinit var device1: BluetoothDevice
+
+    @Mock private lateinit var device2: BluetoothDevice
+
+    @Mock private lateinit var cachedDevice1: CachedBluetoothDevice
+
+    @Mock private lateinit var cachedDevice2: CachedBluetoothDevice
+
+    @Mock private lateinit var receiveState: BluetoothLeBroadcastReceiveState
+
+    @Mock private lateinit var contentResolver: ContentResolver
 
     @Captor
-    private lateinit var leBroadcastCallbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
-    private val testScope = TestScope()
+    private lateinit var broadcastCallbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
 
+    @Captor
+    private lateinit var assistantCallbackCaptor:
+        ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback>
+
+    @Captor private lateinit var btCallbackCaptor: ArgumentCaptor<BluetoothCallback>
+
+    @Captor private lateinit var contentObserverCaptor: ArgumentCaptor<ContentObserver>
+
+    @Captor
+    private lateinit var volumeCallbackCaptor: ArgumentCaptor<BluetoothVolumeControl.Callback>
+
+    private val testScope = TestScope()
+    private val context: Context = ApplicationProvider.getApplicationContext()
     private lateinit var underTest: AudioSharingRepository
 
     @Before
     fun setup() {
-        `when`(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager)
-        `when`(localBluetoothProfileManager.leAudioBroadcastProfile)
-            .thenReturn(localBluetoothLeBroadcast)
-        `when`(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(true)
+        `when`(btManager.profileManager).thenReturn(profileManager)
+        `when`(profileManager.leAudioBroadcastProfile).thenReturn(broadcast)
+        `when`(profileManager.leAudioBroadcastAssistantProfile).thenReturn(assistant)
+        `when`(profileManager.volumeControlProfile).thenReturn(volumeControl)
+        `when`(btManager.eventManager).thenReturn(eventManager)
+        `when`(btManager.cachedDeviceManager).thenReturn(deviceManager)
+        `when`(broadcast.isEnabled(null)).thenReturn(true)
+        `when`(cachedDevice1.groupId).thenReturn(TEST_GROUP_ID1)
+        `when`(cachedDevice1.device).thenReturn(device1)
+        `when`(deviceManager.findDevice(device1)).thenReturn(cachedDevice1)
+        `when`(cachedDevice2.groupId).thenReturn(TEST_GROUP_ID2)
+        `when`(cachedDevice2.device).thenReturn(device2)
+        `when`(deviceManager.findDevice(device2)).thenReturn(cachedDevice2)
+        `when`(receiveState.bisSyncState).thenReturn(arrayListOf(TEST_RECEIVE_STATE_CONTENT))
+        `when`(assistant.getAllSources(any())).thenReturn(listOf(receiveState))
         underTest =
             AudioSharingRepositoryImpl(
-                localBluetoothManager,
+                context,
+                contentResolver,
+                btManager,
+                testScope.backgroundScope,
                 testScope.testScheduler,
             )
     }
@@ -84,9 +154,9 @@
             val states = mutableListOf<Boolean?>()
             underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope)
             runCurrent()
-            triggerAudioSharingStateChange(false)
+            triggerAudioSharingStateChange(TriggerType.BROADCAST_STOP, broadcastStopped)
             runCurrent()
-            triggerAudioSharingStateChange(true)
+            triggerAudioSharingStateChange(TriggerType.BROADCAST_START, broadcastStarted)
             runCurrent()
 
             Truth.assertThat(states).containsExactly(true, false, true)
@@ -102,19 +172,229 @@
             runCurrent()
 
             Truth.assertThat(states).containsExactly(false)
-            verify(localBluetoothLeBroadcast, never()).registerServiceCallBack(any(), any())
-            verify(localBluetoothLeBroadcast, never()).isEnabled(any())
+            verify(broadcast, never()).registerServiceCallBack(any(), any())
+            verify(broadcast, never()).isEnabled(any())
         }
     }
 
-    private fun triggerAudioSharingStateChange(inAudioSharing: Boolean) {
-        verify(localBluetoothLeBroadcast)
-            .registerServiceCallBack(any(), leBroadcastCallbackCaptor.capture())
-        `when`(localBluetoothLeBroadcast.isEnabled(null)).thenReturn(inAudioSharing)
-        if (inAudioSharing) {
-            leBroadcastCallbackCaptor.value.onBroadcastStarted(0, 0)
-        } else {
-            leBroadcastCallbackCaptor.value.onBroadcastStopped(0, 0)
+    @Test
+    @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    fun secondaryGroupIdChange_emitValues() {
+        testScope.runTest {
+            val groupIds = mutableListOf<Int?>()
+            underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope)
+            runCurrent()
+            triggerSourceAdded()
+            runCurrent()
+            triggerContentObserverChange()
+            runCurrent()
+            triggerSourceRemoved()
+            runCurrent()
+            triggerSourceAdded()
+            runCurrent()
+            triggerProfileConnectionChange(
+                BluetoothAdapter.STATE_CONNECTING, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT)
+            runCurrent()
+            triggerProfileConnectionChange(
+                BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO)
+            runCurrent()
+            triggerProfileConnectionChange(
+                BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT)
+            runCurrent()
+
+            Truth.assertThat(groupIds)
+                .containsExactly(
+                    TEST_GROUP_ID_INVALID,
+                    TEST_GROUP_ID2,
+                    TEST_GROUP_ID1,
+                    TEST_GROUP_ID_INVALID,
+                    TEST_GROUP_ID2,
+                    TEST_GROUP_ID_INVALID)
         }
     }
+
+    @Test
+    @DisableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    fun secondaryGroupIdChange_audioSharingFlagOff_returnFalse() {
+        testScope.runTest {
+            val groupIds = mutableListOf<Int?>()
+            underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope)
+            runCurrent()
+
+            Truth.assertThat(groupIds).containsExactly(TEST_GROUP_ID_INVALID)
+            verify(assistant, never()).registerServiceCallBack(any(), any())
+            verify(eventManager, never()).registerCallback(any())
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    fun volumeMapChange_emitValues() {
+        testScope.runTest {
+            val volumeMaps = mutableListOf<GroupIdToVolumes?>()
+            underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope)
+            runCurrent()
+            triggerVolumeMapChange(Pair(device1, TEST_VOLUME1))
+            runCurrent()
+            triggerVolumeMapChange(Pair(device1, TEST_VOLUME2))
+            runCurrent()
+            triggerAudioSharingStateChange(TriggerType.BROADCAST_STOP, broadcastStopped)
+            runCurrent()
+            verify(volumeControl).unregisterCallback(any())
+            runCurrent()
+
+            Truth.assertThat(volumeMaps)
+                .containsExactly(
+                    emptyMap<Int, Int>(),
+                    mapOf(TEST_GROUP_ID1 to TEST_VOLUME1),
+                    mapOf(TEST_GROUP_ID1 to TEST_VOLUME2))
+        }
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    fun volumeMapChange_audioSharingFlagOff_returnFalse() {
+        testScope.runTest {
+            val volumeMaps = mutableListOf<GroupIdToVolumes?>()
+            underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope)
+            runCurrent()
+
+            Truth.assertThat(volumeMaps).isEmpty()
+            verify(broadcast, never()).registerServiceCallBack(any(), any())
+            verify(volumeControl, never()).registerCallback(any(), any())
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    fun setSecondaryVolume_setValue() {
+        testScope.runTest {
+            Settings.Secure.putInt(
+                context.contentResolver,
+                BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+                TEST_GROUP_ID2)
+            `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
+            underTest.setSecondaryVolume(TEST_VOLUME1)
+
+            runCurrent()
+            verify(volumeControl).setDeviceVolume(device1, TEST_VOLUME1, true)
+        }
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    fun setSecondaryVolume_audioSharingFlagOff_doNothing() {
+        testScope.runTest {
+            Settings.Secure.putInt(
+                context.contentResolver,
+                BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+                TEST_GROUP_ID2)
+            `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
+            underTest.setSecondaryVolume(TEST_VOLUME1)
+
+            runCurrent()
+            verify(volumeControl, never()).setDeviceVolume(any(), anyInt(), anyBoolean())
+        }
+    }
+
+    private fun triggerAudioSharingStateChange(
+        type: TriggerType,
+        broadcastAction: BluetoothLeBroadcast.Callback.() -> Unit
+    ) {
+        verify(broadcast).registerServiceCallBack(any(), broadcastCallbackCaptor.capture())
+        when (type) {
+            TriggerType.BROADCAST_START -> {
+                `when`(broadcast.isEnabled(null)).thenReturn(true)
+                broadcastCallbackCaptor.value.broadcastAction()
+            }
+            TriggerType.BROADCAST_STOP -> {
+                `when`(broadcast.isEnabled(null)).thenReturn(false)
+                broadcastCallbackCaptor.value.broadcastAction()
+            }
+        }
+    }
+
+    private fun triggerSourceAdded() {
+        verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture())
+        Settings.Secure.putInt(
+            context.contentResolver,
+            BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+            TEST_GROUP_ID1)
+        `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
+        assistantCallbackCaptor.value.sourceAdded(device1, receiveState)
+    }
+
+    private fun triggerSourceRemoved() {
+        verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture())
+        `when`(assistant.allConnectedDevices).thenReturn(listOf(device1))
+        Settings.Secure.putInt(
+            context.contentResolver,
+            BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+            TEST_GROUP_ID1)
+        assistantCallbackCaptor.value.sourceRemoved(device2)
+    }
+
+    private fun triggerProfileConnectionChange(state: Int, profile: Int) {
+        verify(eventManager).registerCallback(btCallbackCaptor.capture())
+        `when`(assistant.allConnectedDevices).thenReturn(listOf(device1))
+        Settings.Secure.putInt(
+            context.contentResolver,
+            BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+            TEST_GROUP_ID1)
+        btCallbackCaptor.value.onProfileConnectionStateChanged(cachedDevice2, state, profile)
+    }
+
+    private fun triggerContentObserverChange() {
+        verify(contentResolver)
+            .registerContentObserver(
+                eq(Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast())),
+                eq(false),
+                contentObserverCaptor.capture())
+        `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
+        Settings.Secure.putInt(
+            context.contentResolver,
+            BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
+            TEST_GROUP_ID2)
+        contentObserverCaptor.value.primaryChanged()
+    }
+
+    private fun triggerVolumeMapChange(change: Pair<BluetoothDevice, Int>) {
+        verify(volumeControl).registerCallback(any(), volumeCallbackCaptor.capture())
+        volumeCallbackCaptor.value.onDeviceVolumeChanged(change.first, change.second)
+    }
+
+    private enum class TriggerType {
+        BROADCAST_START,
+        BROADCAST_STOP
+    }
+
+    private companion object {
+        const val TEST_GROUP_ID_INVALID = -1
+        const val TEST_GROUP_ID1 = 1
+        const val TEST_GROUP_ID2 = 2
+        const val TEST_SOURCE_ID = 1
+        const val TEST_BROADCAST_ID = 1
+        const val TEST_REASON = 1
+        const val TEST_RECEIVE_STATE_CONTENT = 1L
+        const val TEST_VOLUME1 = 10
+        const val TEST_VOLUME2 = 20
+
+        val broadcastStarted: BluetoothLeBroadcast.Callback.() -> Unit = {
+            onBroadcastStarted(TEST_REASON, TEST_BROADCAST_ID)
+        }
+        val broadcastStopped: BluetoothLeBroadcast.Callback.() -> Unit = {
+            onBroadcastStopped(TEST_REASON, TEST_BROADCAST_ID)
+        }
+        val sourceAdded:
+            BluetoothLeBroadcastAssistant.Callback.(
+                sink: BluetoothDevice, state: BluetoothLeBroadcastReceiveState) -> Unit =
+            { sink, state ->
+                onReceiveStateChanged(sink, TEST_SOURCE_ID, state)
+            }
+        val sourceRemoved: BluetoothLeBroadcastAssistant.Callback.(sink: BluetoothDevice) -> Unit =
+            { sink ->
+                onSourceRemoved(sink, TEST_SOURCE_ID, TEST_REASON)
+            }
+        val primaryChanged: ContentObserver.() -> Unit = { onChange(false) }
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
index 6e11e1f..67c73b1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
@@ -22,8 +22,10 @@
 import android.content.Context
 import android.content.Intent
 import android.database.ContentObserver
+import android.os.Parcelable
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import android.provider.Settings.Global
 import androidx.test.filters.SmallTest
 import com.android.settingslib.flags.Flags
@@ -38,6 +40,7 @@
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
@@ -54,6 +57,8 @@
 @RunWith(RobolectricTestRunner::class)
 @SmallTest
 class ZenModeRepositoryTest {
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
     @Mock private lateinit var context: Context
 
     @Mock private lateinit var notificationManager: NotificationManager
@@ -126,6 +131,26 @@
         }
     }
 
+    @EnableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
+    @Test
+    fun consolidatedPolicyChanges_repositoryEmitsFromExtras() {
+        testScope.runTest {
+            val values = mutableListOf<NotificationManager.Policy?>()
+            `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy1)
+            underTest.consolidatedNotificationPolicy
+                .onEach { values.add(it) }
+                .launchIn(backgroundScope)
+            runCurrent()
+
+            triggerIntent(
+                NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED,
+                extras = mapOf(NotificationManager.EXTRA_NOTIFICATION_POLICY to testPolicy2))
+            runCurrent()
+
+            assertThat(values).containsExactly(null, testPolicy1, testPolicy2).inOrder()
+        }
+    }
+
     @Test
     fun zenModeChanges_repositoryEmits() {
         testScope.runTest {
@@ -174,9 +199,13 @@
         }
     }
 
-    private fun triggerIntent(action: String) {
+    private fun triggerIntent(action: String, extras: Map<String, Parcelable>? = null) {
         verify(context).registerReceiver(receiverCaptor.capture(), any(), any(), any())
-        receiverCaptor.value.onReceive(context, Intent(action))
+        val intent = Intent(action)
+        if (extras?.isNotEmpty() == true) {
+            extras.forEach { (key, value) -> intent.putExtra(key, value) }
+        }
+        receiverCaptor.value.onReceive(context, intent)
     }
 
     private fun triggerZenModeSettingUpdate() {
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
index 8dd51b2..8de0c35 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
@@ -22,8 +22,6 @@
 
 import android.content.ContentResolver;
 import android.os.Bundle;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
@@ -73,24 +71,9 @@
     }
 
     /**
-     * Test that setting overrides are properly disabled when the flag is off.
-     */
-    @Test
-    @RequiresFlagsDisabled("com.android.providers.settings.support_overrides")
-    public void testOverrideDisabled() throws IOException {
-        final String newValue = "value2";
-
-        executeShellCommand("device_config put " + sNamespace + " " + sKey + " " + sValue);
-        executeShellCommand("device_config override " + sNamespace + " " + sKey + " " + newValue);
-        String result = readShellCommandOutput("device_config get " + sNamespace + " " + sKey);
-        assertEquals(sValue + "\n", result);
-    }
-
-    /**
      * Test that overrides are readable and can be cleared.
      */
     @Test
-    @RequiresFlagsEnabled("com.android.providers.settings.support_overrides")
     public void testOverride() throws IOException {
         final String newValue = "value2";
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 43d51c3..92f03d7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -45,7 +45,7 @@
 import com.android.systemui.communal.shared.model.CommunalBackgroundType
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.shared.model.CommunalTransitionKeys
-import com.android.systemui.communal.ui.compose.Dimensions.SlideOffsetY
+import com.android.systemui.communal.ui.compose.Dimensions.Companion.SlideOffsetY
 import com.android.systemui.communal.ui.compose.extensions.allowGestures
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.util.CommunalColors
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 8f247f6..768e653 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.communal.ui.compose
 
+import android.content.Context
+import android.content.res.Configuration
 import android.graphics.drawable.Icon
 import android.os.Bundle
 import android.util.SizeF
@@ -40,6 +42,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
@@ -64,6 +67,7 @@
 import androidx.compose.foundation.lazy.grid.LazyGridState
 import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
 import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.selection.selectable
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
@@ -114,6 +118,7 @@
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
@@ -127,6 +132,7 @@
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
@@ -386,6 +392,10 @@
     }
 }
 
+val hubDimensions: Dimensions
+    @Composable
+    get() = Dimensions(LocalContext.current, LocalConfiguration.current, LocalDensity.current)
+
 @Composable
 private fun DisclaimerBottomSheetContent(onButtonClicked: () -> Unit) {
     val colors = LocalAndroidColorScheme.current
@@ -507,7 +517,6 @@
                 gridState = gridState,
                 contentListState = contentListState,
                 contentOffset = contentOffset,
-                updateDragPositionForRemove = updateDragPositionForRemove
             )
 
         // A full size box in background that listens to widget drops from the picker.
@@ -515,7 +524,7 @@
         // for android drag events.
         Box(Modifier.fillMaxSize().dragAndDropTarget(dragAndDropTargetState)) {}
     } else {
-        gridModifier = gridModifier.height(Dimensions.GridHeight)
+        gridModifier = gridModifier.height(hubDimensions.GridHeight)
     }
 
     LazyHorizontalGrid(
@@ -593,7 +602,7 @@
 ) {
     val colors = LocalAndroidColorScheme.current
     Card(
-        modifier = Modifier.height(Dimensions.GridHeight).padding(contentPadding),
+        modifier = Modifier.height(hubDimensions.GridHeight).padding(contentPadding),
         colors = CardDefaults.cardColors(containerColor = Color.Transparent),
         border = BorderStroke(3.dp, colors.secondary),
         shape = RoundedCornerShape(size = 80.dp)
@@ -963,9 +972,25 @@
     val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle()
     val selectedIndex =
         selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
+
+    val isSelected = selectedKey == model.key
+
+    val selectableModifier =
+        if (viewModel.isEditMode) {
+            Modifier.selectable(
+                selected = isSelected,
+                onClick = { viewModel.setSelectedKey(model.key) },
+                interactionSource = remember { MutableInteractionSource() },
+                indication = null,
+            )
+        } else {
+            Modifier
+        }
+
     Box(
         modifier =
             modifier
+                .then(selectableModifier)
                 .thenIf(!viewModel.isEditMode && model.inQuietMode) {
                     Modifier.pointerInput(Unit) {
                         // consume tap to prevent the child view from triggering interactions with
@@ -978,7 +1003,6 @@
                 .thenIf(viewModel.isEditMode) {
                     Modifier.semantics {
                         contentDescription = accessibilityLabel
-                        onClick(label = clickActionLabel, action = null)
                         val deleteAction =
                             CustomAccessibilityAction(removeWidgetActionLabel) {
                                 contentListState.onRemove(index)
@@ -995,7 +1019,7 @@
                                 true
                             }
 
-                        val actions = mutableListOf(deleteAction, selectWidgetAction)
+                        val actions = mutableListOf(selectWidgetAction, deleteAction)
 
                         if (selectedIndex != null && selectedIndex != index) {
                             actions.add(
@@ -1262,7 +1286,7 @@
         return PaddingValues(
             start = Dimensions.ItemSpacing,
             end = Dimensions.ItemSpacing,
-            top = Dimensions.GridTopSpacing,
+            top = hubDimensions.GridTopSpacing,
         )
     }
     val context = LocalContext.current
@@ -1271,7 +1295,8 @@
     val screenHeight = with(density) { windowMetrics.bounds.height().toDp() }
     val toolbarHeight = with(density) { Dimensions.ToolbarPaddingTop + toolbarSize.height.toDp() }
     val verticalPadding =
-        ((screenHeight - toolbarHeight - Dimensions.GridHeight + Dimensions.GridTopSpacing) / 2)
+        ((screenHeight - toolbarHeight - hubDimensions.GridHeight + hubDimensions.GridTopSpacing) /
+                2)
             .coerceAtLeast(Dimensions.Spacing)
     return PaddingValues(
         start = Dimensions.ToolbarPaddingHorizontal,
@@ -1327,29 +1352,44 @@
     fun toOffset(): Offset = Offset(start, top)
 }
 
-object Dimensions {
-    val CardHeightFull = 530.dp
-    val GridTopSpacing = 114.dp
-    val GridHeight = CardHeightFull + GridTopSpacing
-    val ItemSpacing = 50.dp
-    val CardHeightHalf = (CardHeightFull - ItemSpacing) / 2
-    val CardHeightThird = (CardHeightFull - (2 * ItemSpacing)) / 3
-    val CardWidth = 360.dp
-    val CardOutlineWidth = 3.dp
-    val Spacing = ItemSpacing / 2
+class Dimensions(val context: Context, val config: Configuration, val density: Density) {
+    val GridTopSpacing: Dp
+        get() {
+            if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+                return 114.dp
+            } else {
+                val windowMetrics =
+                    WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context)
+                val screenHeight = with(density) { windowMetrics.bounds.height().toDp() }
 
-    // The sizing/padding of the toolbar in glanceable hub edit mode
-    val ToolbarPaddingTop = 27.dp
-    val ToolbarPaddingHorizontal = ItemSpacing
-    val ToolbarButtonPaddingHorizontal = 24.dp
-    val ToolbarButtonPaddingVertical = 16.dp
-    val ButtonPadding =
-        PaddingValues(
-            vertical = ToolbarButtonPaddingVertical,
-            horizontal = ToolbarButtonPaddingHorizontal,
-        )
-    val IconSize = 40.dp
-    val SlideOffsetY = 30.dp
+                return (screenHeight - CardHeightFull) / 2
+            }
+        }
+
+    val GridHeight = CardHeightFull + GridTopSpacing
+
+    companion object {
+        val CardHeightFull = 530.dp
+        val ItemSpacing = 50.dp
+        val CardHeightHalf = (CardHeightFull - ItemSpacing) / 2
+        val CardHeightThird = (CardHeightFull - (2 * ItemSpacing)) / 3
+        val CardWidth = 360.dp
+        val CardOutlineWidth = 3.dp
+        val Spacing = ItemSpacing / 2
+
+        // The sizing/padding of the toolbar in glanceable hub edit mode
+        val ToolbarPaddingTop = 27.dp
+        val ToolbarPaddingHorizontal = ItemSpacing
+        val ToolbarButtonPaddingHorizontal = 24.dp
+        val ToolbarButtonPaddingVertical = 16.dp
+        val ButtonPadding =
+            PaddingValues(
+                vertical = ToolbarButtonPaddingVertical,
+                horizontal = ToolbarButtonPaddingHorizontal,
+            )
+        val IconSize = 40.dp
+        val SlideOffsetY = 30.dp
+    }
 }
 
 private object Colors {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
index 9e6f22a..0c29394 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
@@ -18,17 +18,13 @@
 
 import android.content.ClipDescription
 import android.view.DragEvent
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.draganddrop.dragAndDropTarget
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
 import androidx.compose.foundation.lazy.grid.LazyGridState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.rememberUpdatedState
@@ -45,8 +41,7 @@
 import com.android.systemui.communal.util.WidgetPickerIntentUtils
 import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtraFromIntent
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.isActive
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.launch
 
 /**
@@ -59,32 +54,22 @@
     gridState: LazyGridState,
     contentOffset: Offset,
     contentListState: ContentListState,
-    updateDragPositionForRemove: (offset: Offset) -> Boolean,
 ): DragAndDropTargetState {
     val scope = rememberCoroutineScope()
-    val autoScrollSpeed = remember { mutableFloatStateOf(0f) }
-    // Threshold of distance from edges that should start auto-scroll - chosen to be a narrow value
-    // that allows differentiating intention of scrolling from intention of dragging over the first
-    // visible item.
     val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() }
     val state =
-        remember(gridState, contentListState) {
+        remember(gridState, contentOffset, contentListState, autoScrollThreshold, scope) {
             DragAndDropTargetState(
                 state = gridState,
                 contentOffset = contentOffset,
                 contentListState = contentListState,
-                scope = scope,
-                autoScrollSpeed = autoScrollSpeed,
                 autoScrollThreshold = autoScrollThreshold,
-                updateDragPositionForRemove = updateDragPositionForRemove,
+                scope = scope,
             )
         }
-    LaunchedEffect(autoScrollSpeed.floatValue) {
-        if (autoScrollSpeed.floatValue != 0f) {
-            while (isActive) {
-                gridState.scrollBy(autoScrollSpeed.floatValue)
-                delay(10)
-            }
+    LaunchedEffect(state) {
+        for (diff in state.scrollChannel) {
+            gridState.scrollBy(diff)
         }
     }
     return state
@@ -96,7 +81,6 @@
  * @see androidx.compose.foundation.draganddrop.dragAndDropTarget
  * @see DragEvent
  */
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 internal fun Modifier.dragAndDropTarget(
     dragDropTargetState: DragAndDropTargetState,
@@ -122,6 +106,10 @@
                         return state.onDrop(event)
                     }
 
+                    override fun onExited(event: DragAndDropEvent) {
+                        state.onExited()
+                    }
+
                     override fun onEnded(event: DragAndDropEvent) {
                         state.onEnded()
                     }
@@ -149,19 +137,17 @@
     private val state: LazyGridState,
     private val contentOffset: Offset,
     private val contentListState: ContentListState,
-    private val scope: CoroutineScope,
-    private val autoScrollSpeed: MutableState<Float>,
     private val autoScrollThreshold: Float,
-    private val updateDragPositionForRemove: (offset: Offset) -> Boolean,
+    private val scope: CoroutineScope,
 ) {
     /**
      * The placeholder item that is treated as if it is being dragged across the grid. It is added
      * to grid once drag and drop event is started and removed when event ends.
      */
     private var placeHolder = CommunalContentModel.WidgetPlaceholder()
-
     private var placeHolderIndex: Int? = null
-    private var isOnRemoveButton = false
+
+    internal val scrollChannel = Channel<Float>()
 
     fun onStarted() {
         // assume item will be added to the end.
@@ -170,39 +156,39 @@
     }
 
     fun onMoved(event: DragAndDropEvent) {
-        val dragEvent = event.toAndroidDragEvent()
-        isOnRemoveButton = updateDragPositionForRemove(Offset(dragEvent.x, dragEvent.y))
-        if (!isOnRemoveButton) {
-            findTargetItem(dragEvent)?.apply {
-                var scrollIndex: Int? = null
-                var scrollOffset: Int? = null
-                if (placeHolderIndex == state.firstVisibleItemIndex) {
-                    // Save info about the first item before the move, to neutralize the automatic
-                    // keeping first item first.
-                    scrollIndex = placeHolderIndex
-                    scrollOffset = state.firstVisibleItemScrollOffset
-                }
+        val dragOffset = event.toOffset()
 
-                autoScrollIfNearEdges(dragEvent)
+        val targetItem =
+            state.layoutInfo.visibleItemsInfo
+                .asSequence()
+                .filter { item -> contentListState.isItemEditable(item.index) }
+                .firstItemAtOffset(dragOffset - contentOffset)
 
-                if (contentListState.isItemEditable(this.index)) {
-                    movePlaceholderTo(this.index)
-                    placeHolderIndex = this.index
-                }
-
-                if (scrollIndex != null && scrollOffset != null) {
-                    // this is needed to neutralize automatic keeping the first item first.
-                    scope.launch { state.scrollToItem(scrollIndex, scrollOffset) }
-                }
+        if (targetItem != null) {
+            var scrollIndex: Int? = null
+            var scrollOffset: Int? = null
+            if (placeHolderIndex == state.firstVisibleItemIndex) {
+                // Save info about the first item before the move, to neutralize the automatic
+                // keeping first item first.
+                scrollIndex = placeHolderIndex
+                scrollOffset = state.firstVisibleItemScrollOffset
             }
+
+            if (contentListState.isItemEditable(targetItem.index)) {
+                movePlaceholderTo(targetItem.index)
+                placeHolderIndex = targetItem.index
+            }
+
+            if (scrollIndex != null && scrollOffset != null) {
+                // this is needed to neutralize automatic keeping the first item first.
+                scope.launch { state.scrollToItem(scrollIndex, scrollOffset) }
+            }
+        } else {
+            computeAutoscroll(dragOffset).takeIf { it != 0f }?.let { scrollChannel.trySend(it) }
         }
     }
 
     fun onDrop(event: DragAndDropEvent): Boolean {
-        autoScrollSpeed.value = 0f
-        if (isOnRemoveButton) {
-            return false
-        }
         return placeHolderIndex?.let { dropIndex ->
             val widgetExtra = event.maybeWidgetExtra() ?: return false
             val (componentName, user) = widgetExtra
@@ -221,39 +207,35 @@
     }
 
     fun onEnded() {
-        autoScrollSpeed.value = 0f
         placeHolderIndex = null
         contentListState.list.remove(placeHolder)
-        isOnRemoveButton = updateDragPositionForRemove(Offset.Zero)
     }
 
-    private fun autoScrollIfNearEdges(dragEvent: DragEvent) {
+    fun onExited() {
+        onEnded()
+    }
+
+    private fun computeAutoscroll(dragOffset: Offset): Float {
         val orientation = state.layoutInfo.orientation
         val distanceFromStart =
             if (orientation == Orientation.Horizontal) {
-                dragEvent.x
+                dragOffset.x
             } else {
-                dragEvent.y
+                dragOffset.y
             }
         val distanceFromEnd =
             if (orientation == Orientation.Horizontal) {
-                state.layoutInfo.viewportSize.width - dragEvent.x
+                state.layoutInfo.viewportEndOffset - dragOffset.x
             } else {
-                state.layoutInfo.viewportSize.height - dragEvent.y
+                state.layoutInfo.viewportEndOffset - dragOffset.y
             }
-        autoScrollSpeed.value =
-            when {
-                distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd
-                distanceFromStart < autoScrollThreshold ->
-                    -(autoScrollThreshold - distanceFromStart)
-                else -> 0f
-            }
-    }
 
-    private fun findTargetItem(dragEvent: DragEvent): LazyGridItemInfo? =
-        state.layoutInfo.visibleItemsInfo.firstItemAtOffset(
-            Offset(dragEvent.x, dragEvent.y) - contentOffset
-        )
+        return when {
+            distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd
+            distanceFromStart < autoScrollThreshold -> distanceFromStart - autoScrollThreshold
+            else -> 0f
+        }
+    }
 
     private fun movePlaceholderTo(index: Int) {
         val currentIndex = contentListState.list.indexOf(placeHolder)
@@ -271,4 +253,6 @@
         val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 }
         return clipData?.getItemAt(0)?.intent?.let { intent -> getWidgetExtraFromIntent(intent) }
     }
+
+    private fun DragAndDropEvent.toOffset() = this.toAndroidDragEvent().run { Offset(x, y) }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
index 1ea73e1..620892a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
@@ -23,6 +23,8 @@
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -39,11 +41,16 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
@@ -54,6 +61,7 @@
 import com.android.systemui.communal.ui.viewmodel.PopupType
 import com.android.systemui.res.R
 import javax.inject.Inject
+import kotlinx.coroutines.delay
 
 class CommunalPopupSection
 @Inject
@@ -91,6 +99,17 @@
         onClick: () -> Unit,
         onDismissRequest: () -> Unit,
     ) {
+        val interactionSource = remember { MutableInteractionSource() }
+        val focusRequester = remember { FocusRequester() }
+
+        val context = LocalContext.current
+
+        LaunchedEffect(Unit) {
+            // Adding a delay to ensure the animation completes before requesting focus
+            delay(250)
+            focusRequester.requestFocus()
+        }
+
         Popup(
             alignment = Alignment.TopCenter,
             offset = IntOffset(0, 40),
@@ -100,6 +119,8 @@
             Button(
                 modifier =
                     Modifier.height(56.dp)
+                        .focusRequester(focusRequester)
+                        .focusable(interactionSource = interactionSource)
                         .graphicsLayer { transformOrigin = TransformOrigin(0f, 0f) }
                         .animateEnterExit(
                             enter =
@@ -142,8 +163,7 @@
                 ) {
                     Icon(
                         imageVector = Icons.Outlined.Widgets,
-                        contentDescription =
-                            stringResource(R.string.button_to_configure_widgets_text),
+                        contentDescription = null,
                         tint = colors.onSecondary,
                         modifier = Modifier.size(20.dp)
                     )
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index a1f2042..859c036 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -90,10 +90,16 @@
      */
     @Composable
     fun SceneScope.Notifications(burnInParams: BurnInParameters?, modifier: Modifier = Modifier) {
+        val areNotificationsVisible by
+            lockscreenContentViewModel
+                .areNotificationsVisible(sceneKey)
+                .collectAsStateWithLifecycle(initialValue = false)
+        if (!areNotificationsVisible) {
+            return
+        }
+
         val isShadeLayoutWide by
             lockscreenContentViewModel.isShadeLayoutWide.collectAsStateWithLifecycle()
-        val areNotificationsVisible by
-            lockscreenContentViewModel.areNotificationsVisible.collectAsStateWithLifecycle()
         val splitShadeTopMargin: Dp =
             if (Flags.centralizedStatusBarHeightFix()) {
                 LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp
@@ -101,10 +107,6 @@
                 dimensionResource(id = R.dimen.large_screen_shade_header_height)
             }
 
-        if (!areNotificationsVisible) {
-            return
-        }
-
         ConstrainedNotificationStack(
             stackScrollView = stackScrollView.get(),
             viewModel = viewModel,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index a184cf3..776e166 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -198,6 +198,7 @@
 
     LaunchedEffect(scrollableState.isScrollInProgress) {
         if (!scrollableState.isScrollInProgress && scrollOffset <= minScrollOffset) {
+            viewModel.setHeadsUpAnimatingAway(false)
             viewModel.snoozeHun()
         }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
index f9f7df8..4f5d0e5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
+import com.android.systemui.biometrics.FaceHelpMessageDebouncer
 import com.android.systemui.biometrics.data.repository.FaceSensorInfo
 import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
 import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
@@ -36,7 +37,7 @@
 import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
 import com.android.systemui.bouncer.shared.flag.fakeComposeBouncerFlags
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
 import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
 import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
 import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
@@ -75,15 +76,20 @@
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
     private lateinit var underTest: BouncerMessageViewModel
+    private val ignoreHelpMessageId = 1
 
     @Before
     fun setUp() {
         kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
         kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true
+        overrideResource(
+            R.array.config_face_acquire_device_entry_ignorelist,
+            intArrayOf(ignoreHelpMessageId)
+        )
         underTest = kosmos.bouncerMessageViewModel
         overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable")
         kosmos.fakeSystemPropertiesHelper.set(
-            DeviceEntryInteractor.SYS_BOOT_REASON_PROP,
+            DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
             "not mainline reboot"
         )
     }
@@ -379,7 +385,15 @@
             runCurrent()
 
             kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
-                HelpFaceAuthenticationStatus(1, "some helpful message")
+                HelpFaceAuthenticationStatus(0, "some helpful message", 0)
+            )
+            runCurrent()
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(
+                    0,
+                    "some helpful message",
+                    FaceHelpMessageDebouncer.DEFAULT_WINDOW_MS
+                )
             )
             runCurrent()
             assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index 0de0369..2cbfa7d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -16,9 +16,11 @@
 
 package com.android.systemui.communal
 
+import android.platform.test.annotations.EnableFlags
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
@@ -60,6 +62,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@EnableFlags(FLAG_COMMUNAL_HUB)
 class CommunalSceneStartableTest : SysuiTestCase() {
     private val kosmos = testKosmos()
 
@@ -98,7 +101,7 @@
     }
 
     @Test
-    fun keyguardGoesAway_forceBlankScene() =
+    fun keyguardGoesAway_whenLaunchingWidget_doNotForceBlankScene() =
         with(kosmos) {
             testScope.runTest {
                 val scene by collectLastValue(communalSceneInteractor.currentScene)
@@ -106,6 +109,27 @@
                 communalSceneInteractor.changeScene(CommunalScenes.Communal)
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
 
+                communalSceneInteractor.setIsLaunchingWidget(true)
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                    testScope = this
+                )
+
+                assertThat(scene).isEqualTo(CommunalScenes.Communal)
+            }
+        }
+
+    @Test
+    fun keyguardGoesAway_whenNotLaunchingWidget_forceBlankScene() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalSceneInteractor.currentScene)
+
+                communalSceneInteractor.changeScene(CommunalScenes.Communal)
+                assertThat(scene).isEqualTo(CommunalScenes.Communal)
+
+                communalSceneInteractor.setIsLaunchingWidget(false)
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.PRIMARY_BOUNCER,
                     to = KeyguardState.GONE,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandlerTest.kt
index 0cd3fb2..d51d356 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandlerTest.kt
@@ -26,14 +26,23 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController
 import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.isNull
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.notNull
 import org.mockito.kotlin.refEq
 import org.mockito.kotlin.verify
 
@@ -41,6 +50,7 @@
 @RunWith(AndroidJUnit4::class)
 class SmartspaceInteractionHandlerTest : SysuiTestCase() {
     private val activityStarter = mock<ActivityStarter>()
+    private val kosmos = testKosmos()
 
     private val testIntent =
         PendingIntent.getActivity(
@@ -51,29 +61,43 @@
         )
     private val testResponse = RemoteResponse.fromPendingIntent(testIntent)
 
-    private val underTest: SmartspaceInteractionHandler by lazy {
-        SmartspaceInteractionHandler(activityStarter)
+    private lateinit var underTest: SmartspaceInteractionHandler
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            underTest = SmartspaceInteractionHandler(activityStarter, communalSceneInteractor)
+        }
     }
 
     @Test
     fun launchAnimatorIsUsedForSmartspaceView() {
-        val parent = FrameLayout(context)
-        val view = SmartspaceAppWidgetHostView(context)
-        parent.addView(view)
-        val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
+        with(kosmos) {
+            testScope.runTest {
+                val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget)
+                assertFalse(launching!!)
 
-        underTest.onInteraction(view, testIntent, testResponse)
+                val parent = FrameLayout(context)
+                val view = SmartspaceAppWidgetHostView(context)
+                parent.addView(view)
+                val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
 
-        // Verify that we pass in a non-null animation controller
-        verify(activityStarter)
-            .startPendingIntentWithoutDismissing(
-                /* intent = */ eq(testIntent),
-                /* dismissShade = */ eq(false),
-                /* intentSentUiThreadCallback = */ isNull(),
-                /* animationController = */ notNull(),
-                /* fillInIntent = */ refEq(fillInIntent),
-                /* extraOptions = */ refEq(activityOptions.toBundle()),
-            )
+                underTest.onInteraction(view, testIntent, testResponse)
+
+                // Verify that we set the state correctly
+                assertTrue(launching!!)
+                // Verify that we pass in a non-null Communal animation controller
+                verify(activityStarter)
+                    .startPendingIntentWithoutDismissing(
+                        /* intent = */ eq(testIntent),
+                        /* dismissShade = */ eq(false),
+                        /* intentSentUiThreadCallback = */ isNull(),
+                        /* animationController = */ any<CommunalTransitionAnimatorController>(),
+                        /* fillInIntent = */ refEq(fillInIntent),
+                        /* extraOptions = */ refEq(activityOptions.toBundle()),
+                    )
+            }
+        }
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt
new file mode 100644
index 0000000..ac50db4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt
@@ -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.
+ */
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalTransitionAnimatorControllerTest : SysuiTestCase() {
+    private val controller = mock<ActivityTransitionAnimator.Controller>()
+    private val kosmos = testKosmos()
+
+    private lateinit var underTest: CommunalTransitionAnimatorController
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            underTest = CommunalTransitionAnimatorController(controller, communalSceneInteractor)
+        }
+    }
+
+    @Test
+    fun doNotAnimate_launchingWidgetStateIsCleared() {
+        with(kosmos) {
+            testScope.runTest {
+                val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget)
+
+                communalSceneInteractor.setIsLaunchingWidget(true)
+                assertTrue(launching!!)
+
+                underTest.onIntentStarted(willAnimate = false)
+                assertFalse(launching!!)
+                verify(controller).onIntentStarted(willAnimate = false)
+            }
+        }
+    }
+
+    @Test
+    fun animationCancelled_launchingWidgetStateIsClearedAndSceneIsNotChanged() {
+        with(kosmos) {
+            testScope.runTest {
+                val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget)
+                val scene by collectLastValue(communalSceneInteractor.currentScene)
+
+                communalSceneInteractor.changeScene(CommunalScenes.Communal)
+                Truth.assertThat(scene).isEqualTo(CommunalScenes.Communal)
+                communalSceneInteractor.setIsLaunchingWidget(true)
+                assertTrue(launching!!)
+
+                underTest.onIntentStarted(willAnimate = true)
+                assertTrue(launching!!)
+                verify(controller).onIntentStarted(willAnimate = true)
+
+                underTest.onTransitionAnimationCancelled(newKeyguardOccludedState = true)
+                assertFalse(launching!!)
+                Truth.assertThat(scene).isEqualTo(CommunalScenes.Communal)
+                verify(controller).onTransitionAnimationCancelled(newKeyguardOccludedState = true)
+            }
+        }
+    }
+
+    @Test
+    fun animationComplete_launchingWidgetStateIsClearedAndSceneIsChanged() {
+        with(kosmos) {
+            testScope.runTest {
+                val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget)
+                val scene by collectLastValue(communalSceneInteractor.currentScene)
+
+                communalSceneInteractor.changeScene(CommunalScenes.Communal)
+                Truth.assertThat(scene).isEqualTo(CommunalScenes.Communal)
+                communalSceneInteractor.setIsLaunchingWidget(true)
+                assertTrue(launching!!)
+
+                underTest.onIntentStarted(willAnimate = true)
+                assertTrue(launching!!)
+                verify(controller).onIntentStarted(willAnimate = true)
+
+                underTest.onTransitionAnimationEnd(isExpandingFullyAbove = true)
+                assertFalse(launching!!)
+                Truth.assertThat(scene).isEqualTo(CommunalScenes.Blank)
+                verify(controller).onTransitionAnimationEnd(isExpandingFullyAbove = true)
+            }
+        }
+    }
+}
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 7044895..ea8b5ab 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
@@ -26,13 +26,21 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.isNull
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.notNull
 import org.mockito.kotlin.refEq
 import org.mockito.kotlin.verify
 
@@ -40,6 +48,7 @@
 @RunWith(AndroidJUnit4::class)
 class WidgetInteractionHandlerTest : SysuiTestCase() {
     private val activityStarter = mock<ActivityStarter>()
+    private val kosmos = testKosmos()
 
     private val testIntent =
         PendingIntent.getActivity(
@@ -50,30 +59,44 @@
         )
     private val testResponse = RemoteResponse.fromPendingIntent(testIntent)
 
-    private val underTest: WidgetInteractionHandler by lazy {
-        WidgetInteractionHandler(activityStarter)
+    private lateinit var underTest: WidgetInteractionHandler
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            underTest = WidgetInteractionHandler(activityStarter, communalSceneInteractor)
+        }
     }
 
     @Test
     fun launchAnimatorIsUsedForWidgetView() {
-        val parent = FrameLayout(context)
-        val view = CommunalAppWidgetHostView(context)
-        parent.addView(view)
-        val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
+        with(kosmos) {
+            testScope.runTest {
+                val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget)
+                assertFalse(launching!!)
 
-        underTest.onInteraction(view, testIntent, testResponse)
+                val parent = FrameLayout(context)
+                val view = CommunalAppWidgetHostView(context)
+                parent.addView(view)
+                val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
 
-        // Verify that we pass in a non-null animation controller
-        verify(activityStarter)
-            .startPendingIntentMaybeDismissingKeyguard(
-                /* intent = */ eq(testIntent),
-                /* dismissShade = */ eq(false),
-                /* intentSentUiThreadCallback = */ isNull(),
-                /* animationController = */ notNull(),
-                /* fillInIntent = */ refEq(fillInIntent),
-                /* extraOptions = */ refEq(activityOptions.toBundle()),
-                /* customMessage */ isNull(),
-            )
+                underTest.onInteraction(view, testIntent, testResponse)
+
+                // Verify that we set the state correctly
+                assertTrue(launching!!)
+                // Verify that we pass in a non-null Communal animation controller
+                verify(activityStarter)
+                    .startPendingIntentMaybeDismissingKeyguard(
+                        /* intent = */ eq(testIntent),
+                        /* dismissShade = */ eq(false),
+                        /* intentSentUiThreadCallback = */ isNull(),
+                        /* animationController = */ any<CommunalTransitionAnimatorController>(),
+                        /* fillInIntent = */ refEq(fillInIntent),
+                        /* extraOptions = */ refEq(activityOptions.toBundle()),
+                        /* customMessage */ isNull(),
+                    )
+            }
+        }
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 2546f27..2bf50b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -52,7 +52,6 @@
 import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
 import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
 import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus
-import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
 import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
 import com.android.systemui.display.data.repository.displayRepository
 import com.android.systemui.dump.DumpManager
@@ -79,7 +78,6 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.res.R
 import com.android.systemui.statusbar.commandQueue
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.testKosmos
@@ -478,29 +476,6 @@
         }
 
     @Test
-    fun faceHelpMessagesAreIgnoredBasedOnConfig() =
-        testScope.runTest {
-            overrideResource(
-                R.array.config_face_acquire_device_entry_ignorelist,
-                intArrayOf(10, 11)
-            )
-            underTest = createDeviceEntryFaceAuthRepositoryImpl()
-            initCollectors()
-            allPreconditionsToRunFaceAuthAreTrue()
-
-            underTest.requestAuthenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
-            faceAuthenticateIsCalled()
-
-            authenticationCallback.value.onAuthenticationHelp(9, "help msg")
-            authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg")
-            authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg")
-
-            val response = authStatus() as HelpFaceAuthenticationStatus
-            assertThat(response.msg).isEqualTo("help msg")
-            assertThat(response.msgId).isEqualTo(response.msgId)
-        }
-
-    @Test
     fun dumpDoesNotErrorOutWhenFaceManagerOrBypassControllerIsNull() =
         testScope.runTest {
             fakeUserRepository.setSelectedUserInfo(primaryUser)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index 546510b..3253edf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -20,7 +20,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.SceneKey
-import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
@@ -32,27 +31,14 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.AdaptiveAuthRequest
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.BouncerLockedOut
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.PolicyLockdown
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.SecurityTimeout
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.TrustAgentDisabled
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.UnattendedUpdate
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.UserLockdown
 import com.android.systemui.flags.EnableSceneContainer
-import com.android.systemui.flags.fakeSystemPropertiesHelper
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeTrustRepository
-import com.android.systemui.keyguard.shared.model.AuthenticationFlags
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
@@ -61,7 +47,6 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -438,231 +423,6 @@
             assertThat(isUnlocked).isTrue()
         }
 
-    @Test
-    fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_alwaysNull() =
-        testScope.runTest {
-            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
-            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
-            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
-            runCurrent()
-
-            verifyRestrictionReasonsForAuthFlags(
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to null,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to null,
-                LockPatternUtils.StrongAuthTracker
-                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
-                    null,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    null,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to null
-            )
-        }
-
-    @Test
-    fun deviceEntryRestrictionReason_whenFaceIsEnrolledAndEnabled_mapsToAuthFlagsState() =
-        testScope.runTest {
-            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
-            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
-            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
-            kosmos.fakeSystemPropertiesHelper.set(
-                DeviceEntryInteractor.SYS_BOOT_REASON_PROP,
-                "not mainline reboot"
-            )
-            runCurrent()
-
-            verifyRestrictionReasonsForAuthFlags(
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
-                    DeviceNotUnlockedSinceReboot,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to
-                    AdaptiveAuthRequest,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to
-                    BouncerLockedOut,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
-                    SecurityTimeout,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
-                    UserLockdown,
-                LockPatternUtils.StrongAuthTracker
-                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
-                    NonStrongBiometricsSecurityTimeout,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    UnattendedUpdate,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
-                    PolicyLockdown,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
-                    null,
-            )
-        }
-
-    @Test
-    fun deviceEntryRestrictionReason_whenFingerprintIsEnrolledAndEnabled_mapsToAuthFlagsState() =
-        testScope.runTest {
-            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
-            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
-            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
-            kosmos.fakeSystemPropertiesHelper.set(
-                DeviceEntryInteractor.SYS_BOOT_REASON_PROP,
-                "not mainline reboot"
-            )
-            runCurrent()
-
-            verifyRestrictionReasonsForAuthFlags(
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
-                    DeviceNotUnlockedSinceReboot,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to
-                    AdaptiveAuthRequest,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to
-                    BouncerLockedOut,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
-                    SecurityTimeout,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
-                    UserLockdown,
-                LockPatternUtils.StrongAuthTracker
-                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
-                    NonStrongBiometricsSecurityTimeout,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    UnattendedUpdate,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
-                    PolicyLockdown,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
-                    null,
-            )
-        }
-
-    @Test
-    fun deviceEntryRestrictionReason_whenTrustAgentIsEnabled_mapsToAuthFlagsState() =
-        testScope.runTest {
-            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
-            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
-            kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
-            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
-            kosmos.fakeSystemPropertiesHelper.set(
-                DeviceEntryInteractor.SYS_BOOT_REASON_PROP,
-                "not mainline reboot"
-            )
-            runCurrent()
-
-            verifyRestrictionReasonsForAuthFlags(
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
-                    DeviceNotUnlockedSinceReboot,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to
-                    AdaptiveAuthRequest,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to
-                    BouncerLockedOut,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
-                    SecurityTimeout,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
-                    UserLockdown,
-                LockPatternUtils.StrongAuthTracker
-                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
-                    NonStrongBiometricsSecurityTimeout,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
-                    UnattendedUpdate,
-                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
-                    PolicyLockdown,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
-                    TrustAgentDisabled,
-                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
-                    TrustAgentDisabled,
-            )
-        }
-
-    @Test
-    fun deviceEntryRestrictionReason_whenDeviceRebootedForMainlineUpdate_mapsToTheCorrectReason() =
-        testScope.runTest {
-            val deviceEntryRestrictionReason by
-                collectLastValue(underTest.deviceEntryRestrictionReason)
-            kosmos.fakeSystemPropertiesHelper.set(
-                DeviceEntryInteractor.SYS_BOOT_REASON_PROP,
-                DeviceEntryInteractor.REBOOT_MAINLINE_UPDATE
-            )
-            kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
-                AuthenticationFlags(
-                    userId = 1,
-                    flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
-                )
-            )
-            runCurrent()
-
-            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
-            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
-            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
-            runCurrent()
-
-            assertThat(deviceEntryRestrictionReason).isNull()
-
-            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
-            runCurrent()
-
-            assertThat(deviceEntryRestrictionReason)
-                .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate)
-
-            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
-            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
-            runCurrent()
-
-            assertThat(deviceEntryRestrictionReason)
-                .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate)
-
-            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
-            kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
-            runCurrent()
-
-            assertThat(deviceEntryRestrictionReason)
-                .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate)
-        }
-
-    @Test
-    fun reportUserPresent_whenDeviceEntered() =
-        testScope.runTest {
-            val isDeviceEntered by collectLastValue(underTest.isDeviceEntered)
-            assertThat(isDeviceEntered).isFalse()
-            assertThat(kosmos.fakeDeviceEntryRepository.userPresentCount).isEqualTo(0)
-
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
-            runCurrent()
-            switchToScene(Scenes.Gone)
-            assertThat(isDeviceEntered).isTrue()
-            assertThat(kosmos.fakeDeviceEntryRepository.userPresentCount).isEqualTo(1)
-
-            switchToScene(Scenes.Lockscreen)
-            assertThat(isDeviceEntered).isFalse()
-            assertThat(kosmos.fakeDeviceEntryRepository.userPresentCount).isEqualTo(1)
-
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
-            switchToScene(Scenes.Gone)
-            assertThat(isDeviceEntered).isTrue()
-            assertThat(kosmos.fakeDeviceEntryRepository.userPresentCount).isEqualTo(2)
-        }
-
-    private fun TestScope.verifyRestrictionReasonsForAuthFlags(
-        vararg authFlagToDeviceEntryRestriction: Pair<Int, DeviceEntryRestrictionReason?>
-    ) {
-        val deviceEntryRestrictionReason by collectLastValue(underTest.deviceEntryRestrictionReason)
-
-        authFlagToDeviceEntryRestriction.forEach { (flag, expectedReason) ->
-            kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
-                AuthenticationFlags(userId = 1, flag = flag)
-            )
-            runCurrent()
-
-            if (expectedReason == null) {
-                assertThat(deviceEntryRestrictionReason).isNull()
-            } else {
-                assertThat(deviceEntryRestrictionReason).isEqualTo(expectedReason)
-            }
-        }
-    }
-
     private fun switchToScene(sceneKey: SceneKey) {
         sceneInteractor.changeScene(sceneKey, "reason")
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index a7a7bea3..c2acc5f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -19,17 +19,20 @@
 import android.content.pm.UserInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
-import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
+import com.android.systemui.flags.fakeSystemPropertiesHelper
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeTrustRepository
-import com.android.systemui.keyguard.domain.interactor.trustInteractor
+import com.android.systemui.keyguard.shared.model.AuthenticationFlags
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
@@ -40,6 +43,7 @@
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -54,18 +58,8 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val authenticationRepository = kosmos.fakeAuthenticationRepository
-    private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
 
-    val underTest =
-        DeviceUnlockedInteractor(
-            applicationScope = testScope.backgroundScope,
-            authenticationInteractor = kosmos.authenticationInteractor,
-            deviceEntryRepository = deviceEntryRepository,
-            trustInteractor = kosmos.trustInteractor,
-            faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor,
-            fingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor,
-            powerInteractor = kosmos.powerInteractor,
-        )
+    val underTest = kosmos.deviceUnlockedInteractor
 
     @Before
     fun setup() {
@@ -100,6 +94,30 @@
         }
 
     @Test
+    fun deviceUnlockStatus_whenUnlockedAndAuthMethodIsPinAndInLockdown_isFalse() =
+        testScope.runTest {
+            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+            val isInLockdown by collectLastValue(underTest.isInLockdown)
+
+            authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
+                AuthenticationFlags(
+                    userId = 1,
+                    flag =
+                        LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN,
+                )
+            )
+            runCurrent()
+            assertThat(isInLockdown).isTrue()
+
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+            assertThat(deviceUnlockStatus?.deviceUnlockSource).isNull()
+        }
+
+    @Test
     fun deviceUnlockStatus_whenUnlockedAndAuthMethodIsSim_isFalse() =
         testScope.runTest {
             val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
@@ -221,6 +239,218 @@
             assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
         }
 
+    @Test
+    fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_alwaysNull() =
+        testScope.runTest {
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
+            runCurrent()
+
+            verifyRestrictionReasonsForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to null,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null,
+                LockPatternUtils.StrongAuthTracker
+                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+                    null,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    null,
+            )
+        }
+
+    @Test
+    fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_whenLockdown() =
+        testScope.runTest {
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
+            runCurrent()
+
+            verifyRestrictionReasonsForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    DeviceEntryRestrictionReason.UserLockdown,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    DeviceEntryRestrictionReason.PolicyLockdown
+            )
+        }
+
+    @Test
+    fun deviceEntryRestrictionReason_whenFaceIsEnrolledAndEnabled_mapsToAuthFlagsState() =
+        testScope.runTest {
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
+            kosmos.fakeSystemPropertiesHelper.set(
+                DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
+                "not mainline reboot"
+            )
+            runCurrent()
+
+            verifyRestrictionReasonsForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to
+                    DeviceEntryRestrictionReason.AdaptiveAuthRequest,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to
+                    DeviceEntryRestrictionReason.BouncerLockedOut,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    DeviceEntryRestrictionReason.SecurityTimeout,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    DeviceEntryRestrictionReason.UserLockdown,
+                LockPatternUtils.StrongAuthTracker
+                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    DeviceEntryRestrictionReason.UnattendedUpdate,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    DeviceEntryRestrictionReason.PolicyLockdown,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+                    null,
+            )
+        }
+
+    @Test
+    fun deviceEntryRestrictionReason_whenFingerprintIsEnrolledAndEnabled_mapsToAuthFlagsState() =
+        testScope.runTest {
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
+            kosmos.fakeSystemPropertiesHelper.set(
+                DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
+                "not mainline reboot"
+            )
+            runCurrent()
+
+            verifyRestrictionReasonsForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to
+                    DeviceEntryRestrictionReason.AdaptiveAuthRequest,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to
+                    DeviceEntryRestrictionReason.BouncerLockedOut,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    DeviceEntryRestrictionReason.SecurityTimeout,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    DeviceEntryRestrictionReason.UserLockdown,
+                LockPatternUtils.StrongAuthTracker
+                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    DeviceEntryRestrictionReason.UnattendedUpdate,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    DeviceEntryRestrictionReason.PolicyLockdown,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+                    null,
+            )
+        }
+
+    @Test
+    fun deviceEntryRestrictionReason_whenTrustAgentIsEnabled_mapsToAuthFlagsState() =
+        testScope.runTest {
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
+            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
+            kosmos.fakeSystemPropertiesHelper.set(
+                DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
+                "not mainline reboot"
+            )
+            runCurrent()
+
+            verifyRestrictionReasonsForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST to
+                    DeviceEntryRestrictionReason.AdaptiveAuthRequest,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to
+                    DeviceEntryRestrictionReason.BouncerLockedOut,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    DeviceEntryRestrictionReason.SecurityTimeout,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    DeviceEntryRestrictionReason.UserLockdown,
+                LockPatternUtils.StrongAuthTracker
+                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    DeviceEntryRestrictionReason.UnattendedUpdate,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    DeviceEntryRestrictionReason.PolicyLockdown,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+                    DeviceEntryRestrictionReason.TrustAgentDisabled,
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+                    DeviceEntryRestrictionReason.TrustAgentDisabled,
+            )
+        }
+
+    @Test
+    fun deviceEntryRestrictionReason_whenDeviceRebootedForMainlineUpdate_mapsToTheCorrectReason() =
+        testScope.runTest {
+            val deviceEntryRestrictionReason by
+                collectLastValue(underTest.deviceEntryRestrictionReason)
+            kosmos.fakeSystemPropertiesHelper.set(
+                DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
+                DeviceUnlockedInteractor.REBOOT_MAINLINE_UPDATE
+            )
+            kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
+                AuthenticationFlags(
+                    userId = 1,
+                    flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
+                )
+            )
+            runCurrent()
+
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
+            runCurrent()
+
+            assertThat(deviceEntryRestrictionReason).isNull()
+
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+            runCurrent()
+
+            assertThat(deviceEntryRestrictionReason)
+                .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate)
+
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            runCurrent()
+
+            assertThat(deviceEntryRestrictionReason)
+                .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate)
+
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
+            runCurrent()
+
+            assertThat(deviceEntryRestrictionReason)
+                .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate)
+        }
+
+    private fun TestScope.verifyRestrictionReasonsForAuthFlags(
+        vararg authFlagToDeviceEntryRestriction: Pair<Int, DeviceEntryRestrictionReason?>
+    ) {
+        val deviceEntryRestrictionReason by collectLastValue(underTest.deviceEntryRestrictionReason)
+
+        authFlagToDeviceEntryRestriction.forEach { (flag, expectedReason) ->
+            kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
+                AuthenticationFlags(userId = 1, flag = flag)
+            )
+            runCurrent()
+
+            if (expectedReason == null) {
+                assertThat(deviceEntryRestrictionReason).isNull()
+            } else {
+                assertThat(deviceEntryRestrictionReason).isEqualTo(expectedReason)
+            }
+        }
+    }
+
     companion object {
         private const val primaryUserId = 1
         private val primaryUser = UserInfo(primaryUserId, "test user", UserInfo.FLAG_PRIMARY)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index 444f63a..60c9bb0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -41,6 +41,7 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.ambient.touch.TouchMonitor
 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
@@ -118,11 +119,11 @@
 
     @Mock
     lateinit var mDreamComplicationComponentFactory:
-            com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory
+        com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory
 
     @Mock
     lateinit var mDreamComplicationComponent:
-            com.android.systemui.dreams.complication.dagger.ComplicationComponent
+        com.android.systemui.dreams.complication.dagger.ComplicationComponent
 
     @Mock lateinit var mHideComplicationTouchHandler: HideComplicationTouchHandler
 
@@ -202,8 +203,12 @@
         whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController)
         whenever(mLazyViewCapture.value).thenReturn(viewCaptureSpy)
         mWindowParams = WindowManager.LayoutParams()
-        mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager(mWindowManager,
-                mLazyViewCapture, isViewCaptureEnabled = false)
+        mViewCaptureAwareWindowManager =
+            ViewCaptureAwareWindowManager(
+                mWindowManager,
+                mLazyViewCapture,
+                isViewCaptureEnabled = false
+            )
         mService =
             DreamOverlayService(
                 mContext,
@@ -257,7 +262,7 @@
         mMainExecutor.runAllReady()
         verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START)
         verify(mUiEventLogger)
-                .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
+            .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
     }
 
     @Test
@@ -634,7 +639,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT)
+    @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB)
     @kotlin.Throws(RemoteException::class)
     fun testTransitionToGlanceableHub() =
         testScope.runTest {
@@ -658,7 +663,7 @@
         }
 
     @Test
-    @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT)
+    @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB)
     @Throws(RemoteException::class)
     fun testRedirectExit() =
         testScope.runTest {
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 4a5342a..3a4b14b 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
@@ -28,6 +28,8 @@
 import com.android.systemui.shared.education.GestureType.BACK_GESTURE
 import com.google.common.truth.Truth.assertThat
 import java.io.File
+import java.time.Clock
+import java.time.Instant
 import javax.inject.Provider
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.test.TestScope
@@ -48,6 +50,7 @@
     private val dsScopeProvider: Provider<CoroutineScope> = Provider {
         TestScope(kosmos.testDispatcher).backgroundScope
     }
+    private val clock: Clock = FakeEduClock(Instant.ofEpochMilli(1000))
     private val testUserId = 1111
 
     // For deleting any test files created after the test
@@ -59,7 +62,7 @@
         // needed before calling TemporaryFolder.newFolder().
         val testContext = TestContext(context, tmpFolder.newFolder())
         val userRepository = UserContextualEducationRepository(testContext, dsScopeProvider)
-        underTest = ContextualEducationRepository(userRepository)
+        underTest = ContextualEducationRepositoryImpl(clock, userRepository)
         underTest.setUser(testUserId)
     }
 
@@ -85,6 +88,15 @@
             assertThat(model?.signalCount).isEqualTo(1)
         }
 
+    @Test
+    fun dataAddedOnUpdateShortcutTriggerTime() =
+        testScope.runTest {
+            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK_GESTURE))
+            assertThat(model?.lastShortcutTriggeredTime).isNull()
+            underTest.updateShortcutTriggerTime(BACK_GESTURE)
+            assertThat(model?.lastShortcutTriggeredTime).isEqualTo(clock.instant())
+        }
+
     /** Test context which allows overriding getFilesDir path */
     private class TestContext(context: Context, private val folder: File) :
         SysuiTestableContext(context) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index ec4fd79..b885800 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -123,7 +123,7 @@
         }
 
     @Test
-    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, Flags.FLAG_COMMUNAL_HUB)
     fun testTransitionToLockscreen_onPowerButtonPress_canDream_glanceableHubAvailable() =
         testScope.runTest {
             whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index 50772ee..3075c54 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -37,6 +38,7 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.testKosmos
 import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
 import com.android.systemui.util.mockito.whenever
@@ -124,7 +126,50 @@
     fun areNotificationsVisible_splitShadeTrue_true() =
         with(kosmos) {
             testScope.runTest {
-                val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible)
+                val areNotificationsVisible by
+                    collectLastValue(underTest.areNotificationsVisible(Scenes.Lockscreen))
+                shadeRepository.setShadeLayoutWide(true)
+                fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE)
+
+                assertThat(areNotificationsVisible).isTrue()
+            }
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun areNotificationsVisible_dualShadeWideOnLockscreen_true() =
+        with(kosmos) {
+            testScope.runTest {
+                val areNotificationsVisible by
+                    collectLastValue(underTest.areNotificationsVisible(Scenes.Lockscreen))
+                shadeRepository.setShadeLayoutWide(true)
+                fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE)
+
+                assertThat(areNotificationsVisible).isTrue()
+            }
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun areNotificationsVisible_dualShadeWideOnNotificationsShade_false() =
+        with(kosmos) {
+            testScope.runTest {
+                val areNotificationsVisible by
+                    collectLastValue(underTest.areNotificationsVisible(Scenes.NotificationsShade))
+                shadeRepository.setShadeLayoutWide(true)
+                fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE)
+
+                assertThat(areNotificationsVisible).isFalse()
+            }
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun areNotificationsVisible_dualShadeWideOnQuickSettingsShade_true() =
+        with(kosmos) {
+            testScope.runTest {
+                val areNotificationsVisible by
+                    collectLastValue(underTest.areNotificationsVisible(Scenes.QuickSettingsShade))
                 shadeRepository.setShadeLayoutWide(true)
                 fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE)
 
@@ -137,7 +182,8 @@
     fun areNotificationsVisible_withSmallClock_true() =
         with(kosmos) {
             testScope.runTest {
-                val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible)
+                val areNotificationsVisible by
+                    collectLastValue(underTest.areNotificationsVisible(Scenes.Lockscreen))
                 fakeKeyguardClockRepository.setClockSize(ClockSize.SMALL)
                 assertThat(areNotificationsVisible).isTrue()
             }
@@ -148,7 +194,8 @@
     fun areNotificationsVisible_withLargeClock_false() =
         with(kosmos) {
             testScope.runTest {
-                val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible)
+                val areNotificationsVisible by
+                    collectLastValue(underTest.areNotificationsVisible(Scenes.Lockscreen))
                 fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE)
                 assertThat(areNotificationsVisible).isFalse()
             }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
index 1c3021e..73a0039 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
@@ -82,6 +82,20 @@
         TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() }
     }
 
+    @Test
+    fun onMoveOutOfBounds_removeMovingTileFromCurrentList() {
+        val movingTileSpec = TestEditTiles[0].tileSpec
+
+        // Start the drag movement
+        underTest.onStarted(movingTileSpec)
+
+        // Move the tile outside of the list
+        underTest.movedOutOfBounds()
+
+        // Asserts the moving tile is not current
+        assertThat(listState.tiles.first { it.tileSpec == movingTileSpec }.isCurrent).isFalse()
+    }
+
     companion object {
         private fun createEditTile(tileSpec: String): EditTileViewModel {
             return EditTileViewModel(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..9b9e584
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.qs.tiles.impl.modes.domain.interactor
+
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(android.app.Flags.FLAG_MODES_UI)
+class ModesTileUserActionInteractorTest : SysuiTestCase() {
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+
+    val underTest = ModesTileUserActionInteractor(inputHandler)
+
+    @Test
+    fun handleLongClick() = runTest {
+        underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false)))
+
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+            Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index f14c96ded..71cd95f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -64,7 +64,6 @@
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.whenever
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -77,6 +76,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
+import org.mockito.kotlin.whenever
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
@@ -213,9 +213,11 @@
         }
 
     @Test
-    fun validatePaddingTop() =
+    fun validatePaddingTopInNonSplitShade_usesLargeScreenHeader() =
         testScope.runTest {
+            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10)
             overrideResource(R.bool.config_use_split_notification_shade, false)
+            overrideResource(R.bool.config_use_large_screen_shade_header, true)
             overrideResource(R.dimen.large_screen_shade_header_height, 10)
             overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
 
@@ -223,6 +225,21 @@
 
             configurationRepository.onAnyConfigurationChange()
 
+            assertThat(paddingTop).isEqualTo(10)
+        }
+
+    @Test
+    fun validatePaddingTopInNonSplitShade_doesNotUseLargeScreenHeader() =
+        testScope.runTest {
+            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10)
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            overrideResource(R.bool.config_use_large_screen_shade_header, false)
+            overrideResource(R.dimen.large_screen_shade_header_height, 10)
+            overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
+
+            val paddingTop by collectLastValue(underTest.paddingTopDimen)
+
+            configurationRepository.onAnyConfigurationChange()
             assertThat(paddingTop).isEqualTo(0)
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
index ccd78ee..59e8ea6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
@@ -40,6 +40,7 @@
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.plugins.ActivityStarter.OnDismissAction
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shade.ShadeController
 import com.android.systemui.shade.data.repository.FakeShadeRepository
@@ -136,7 +137,9 @@
                 communalSceneInteractor = communalSceneInteractor,
             )
         `when`(userTracker.userHandle).thenReturn(UserHandle.OWNER)
+        `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(false))
         `when`(communalSceneInteractor.isIdleOnCommunal).thenReturn(MutableStateFlow(false))
+        `when`(communalSceneInteractor.isLaunchingWidget).thenReturn(MutableStateFlow(false))
     }
 
     @Test
@@ -335,6 +338,102 @@
             )
     }
 
+    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
+    @Test
+    fun startPendingIntentDismissingKeyguard_transitionAnimator_animateCommunal() {
+        val parent = FrameLayout(context)
+        val view =
+            object : View(context), LaunchableView {
+                override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+            }
+        parent.addView(view)
+        val controller = ActivityTransitionAnimator.Controller.fromView(view)
+        val pendingIntent = mock(PendingIntent::class.java)
+        `when`(pendingIntent.isActivity).thenReturn(true)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
+        `when`(keyguardStateController.isOccluded).thenReturn(true)
+        `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(true))
+        `when`(communalSceneInteractor.isLaunchingWidget).thenReturn(MutableStateFlow(true))
+        `when`(activityIntentHelper.wouldPendingLaunchResolverActivity(eq(pendingIntent), anyInt()))
+            .thenReturn(false)
+        `when`(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt()))
+            .thenReturn(false)
+
+        underTest.startPendingIntentDismissingKeyguard(
+            intent = pendingIntent,
+            dismissShade = false,
+            animationController = controller,
+            showOverLockscreen = true,
+            skipLockscreenChecks = false
+        )
+        mainExecutor.runAllReady()
+
+        val actionCaptor = argumentCaptor<OnDismissAction>()
+        verify(statusBarKeyguardViewManager)
+            .dismissWithAction(actionCaptor.capture(), eq(null), anyBoolean(), eq(null))
+        actionCaptor.firstValue.onDismiss()
+        mainExecutor.runAllReady()
+
+        verify(activityTransitionAnimator)
+            .startPendingIntentWithAnimation(
+                nullable(ActivityTransitionAnimator.Controller::class.java),
+                eq(true),
+                nullable(String::class.java),
+                eq(false),
+                any(),
+            )
+    }
+
+    @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
+    @Test
+    fun startPendingIntentDismissingKeyguard_transitionAnimator_doNotAnimateCommunal() {
+        val parent = FrameLayout(context)
+        val view =
+            object : View(context), LaunchableView {
+                override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+            }
+        parent.addView(view)
+        val controller = ActivityTransitionAnimator.Controller.fromView(view)
+        val pendingIntent = mock(PendingIntent::class.java)
+        `when`(pendingIntent.isActivity).thenReturn(true)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
+        `when`(keyguardStateController.isOccluded).thenReturn(true)
+        `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(true))
+        `when`(communalSceneInteractor.isLaunchingWidget).thenReturn(MutableStateFlow(true))
+        `when`(activityIntentHelper.wouldPendingLaunchResolverActivity(eq(pendingIntent), anyInt()))
+            .thenReturn(false)
+        `when`(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt()))
+            .thenReturn(false)
+
+        underTest.startPendingIntentDismissingKeyguard(
+            intent = pendingIntent,
+            dismissShade = false,
+            animationController = controller,
+            showOverLockscreen = true,
+            skipLockscreenChecks = false
+        )
+        mainExecutor.runAllReady()
+
+        val actionCaptor = argumentCaptor<OnDismissAction>()
+        verify(statusBarKeyguardViewManager)
+            .dismissWithAction(actionCaptor.capture(), eq(null), anyBoolean(), eq(null))
+        actionCaptor.firstValue.onDismiss()
+        mainExecutor.runAllReady()
+
+        val runnableCaptor = argumentCaptor<Runnable>()
+        verify(statusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(runnableCaptor.capture())
+        runnableCaptor.firstValue.run()
+
+        verify(activityTransitionAnimator)
+            .startPendingIntentWithAnimation(
+                nullable(ActivityTransitionAnimator.Controller::class.java),
+                eq(false),
+                nullable(String::class.java),
+                eq(false),
+                any(),
+            )
+    }
+
     @Test
     fun startActivity_noUserHandleProvided_getUserHandle() {
         val intent = mock(Intent::class.java)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt
new file mode 100644
index 0000000..142631e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.volume.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.data.repository.audioSharingRepository
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class AudioSharingInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    lateinit var underTest: AudioSharingInteractor
+
+    @Before
+    fun setUp() {
+        with(kosmos) { underTest = audioSharingInteractor }
+    }
+
+    @Test
+    fun volumeChanges_returnVolume() {
+        with(kosmos) {
+            testScope.runTest {
+                with(audioSharingRepository) {
+                    setSecondaryGroupId(TEST_GROUP_ID)
+                    setVolumeMap(mapOf(TEST_GROUP_ID to TEST_VOLUME))
+                }
+                val volume by collectLastValue(underTest.volume)
+                runCurrent()
+
+                Truth.assertThat(volume).isEqualTo(TEST_VOLUME)
+            }
+        }
+    }
+
+    @Test
+    fun volumeChanges_returnNull() {
+        with(kosmos) {
+            testScope.runTest {
+                with(audioSharingRepository) {
+                    setSecondaryGroupId(TEST_GROUP_ID_INVALID)
+                    setVolumeMap(mapOf(TEST_GROUP_ID to TEST_VOLUME))
+                }
+                val volume by collectLastValue(underTest.volume)
+                runCurrent()
+
+                Truth.assertThat(volume).isNull()
+            }
+        }
+    }
+
+    @Test
+    fun volumeChanges_returnDefaultVolume() {
+        with(kosmos) {
+            testScope.runTest {
+                with(audioSharingRepository) {
+                    setSecondaryGroupId(TEST_GROUP_ID)
+                    setVolumeMap(emptyMap())
+                }
+                val volume by collectLastValue(underTest.volume)
+                runCurrent()
+
+                Truth.assertThat(volume).isEqualTo(TEST_VOLUME_DEFAULT)
+            }
+        }
+    }
+
+    private companion object {
+        const val TEST_GROUP_ID = 1
+        const val TEST_GROUP_ID_INVALID = -1
+        const val TEST_VOLUME = 10
+        const val TEST_VOLUME_DEFAULT = 20
+    }
+}
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 7f7e634..30f23bf 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -287,7 +287,8 @@
     <integer name="doze_small_icon_alpha">222</integer><!-- 87% of 0xff -->
 
     <!-- Doze: Table that translates sensor values from the doze_brightness_sensor_type sensor
-               to brightness values; -1 means keeping the current brightness. -->
+               to brightness values in the integer scale [1, 255]; -1 means keeping the current
+               brightness. -->
     <integer-array name="config_doze_brightness_sensor_to_brightness">
         <item>-1</item> <!-- 0: OFF -->
         <item>2</item> <!-- 1: NIGHT -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 090033d..5d804cc 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -120,7 +120,7 @@
     oneway void notifyTaskbarAutohideSuspend(boolean suspend) = 48;
 
     /**
-     * Notifies SystemUI to invoke IME Switcher.
+     * Notifies that the IME switcher button has been pressed.
      */
     oneway void onImeSwitcherPressed() = 49;
 
@@ -167,5 +167,10 @@
      */
     oneway void toggleQuickSettingsPanel() = 56;
 
-    // Next id = 57
+    /**
+     * Notifies that the IME Switcher button has been long pressed.
+     */
+    oneway void onImeSwitcherLongPress() = 57;
+
+    // Next id = 58
 }
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
index 56273eb..6e25744 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
@@ -33,6 +33,7 @@
 import android.view.animation.PathInterpolator;
 import android.widget.FrameLayout;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.assist.AssistLogger;
@@ -66,7 +67,7 @@
     protected InvocationLightsView mInvocationLightsView;
     protected final AssistLogger mAssistLogger;
 
-    private final WindowManager mWindowManager;
+    private final ViewCaptureAwareWindowManager mWindowManager;
     private final MetricsLogger mMetricsLogger;
     private final Lazy<AssistManager> mAssistManagerLazy;
     private final WindowManager.LayoutParams mLayoutParams;
@@ -80,12 +81,12 @@
 
     @Inject
     public DefaultUiController(Context context, AssistLogger assistLogger,
-            WindowManager windowManager, MetricsLogger metricsLogger,
-            Lazy<AssistManager> assistManagerLazy,
+            ViewCaptureAwareWindowManager viewCaptureAwareWindowManager,
+            MetricsLogger metricsLogger, Lazy<AssistManager> assistManagerLazy,
             NavigationBarController navigationBarController) {
         mAssistLogger = assistLogger;
         mRoot = new FrameLayout(context);
-        mWindowManager = windowManager;
+        mWindowManager = viewCaptureAwareWindowManager;
         mMetricsLogger = metricsLogger;
         mAssistManagerLazy = assistManagerLazy;
 
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index a9f985f..468737d 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -288,6 +288,7 @@
     override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) {
         withContext(backgroundDispatcher) {
             if (isSuccessful) {
+                lockPatternUtils.userPresent(selectedUserId)
                 lockPatternUtils.reportSuccessfulPasswordAttempt(selectedUserId)
                 _hasLockoutOccurred.value = false
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt
new file mode 100644
index 0000000..1685f49
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.biometrics
+
+import android.util.Log
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+
+/**
+ * Debounces face help messages with parameters:
+ * - window: Window of time (in milliseconds) to analyze face acquired messages)
+ * - startWindow: Window of time on start required before showing the first help message
+ * - shownFaceMessageFrequencyBoost: Frequency boost given to messages that are currently shown to
+ *   the user
+ */
+class FaceHelpMessageDebouncer(
+    private val window: Long = DEFAULT_WINDOW_MS,
+    private val startWindow: Long = window,
+    private val shownFaceMessageFrequencyBoost: Int = 4,
+) {
+    private val TAG = "FaceHelpMessageDebouncer"
+    private var startTime = 0L
+    private var helpFaceAuthStatuses: MutableList<HelpFaceAuthenticationStatus> = mutableListOf()
+    private var lastMessageIdShown: Int? = null
+
+    /** Remove messages that are outside of the time [window]. */
+    private fun removeOldMessages(currTimestamp: Long) {
+        var numToRemove = 0
+        // This works under the assumption that timestamps are ordered from first to last
+        // in chronological order
+        for (index in helpFaceAuthStatuses.indices) {
+            if ((helpFaceAuthStatuses[index].createdAt + window) >= currTimestamp) {
+                break // all timestamps from here and on are within the window
+            }
+            numToRemove += 1
+        }
+
+        // Remove all outside time window
+        repeat(numToRemove) { helpFaceAuthStatuses.removeFirst() }
+
+        if (numToRemove > 0) {
+            Log.v(TAG, "removedFirst=$numToRemove")
+        }
+    }
+
+    private fun getMostFrequentHelpMessage(): HelpFaceAuthenticationStatus? {
+        // freqMap: msgId => frequency
+        val freqMap = helpFaceAuthStatuses.groupingBy { it.msgId }.eachCount().toMutableMap()
+
+        // Give shownFaceMessageFrequencyBoost to lastMessageIdShown
+        if (lastMessageIdShown != null) {
+            freqMap.computeIfPresent(lastMessageIdShown!!) { _, value ->
+                value + shownFaceMessageFrequencyBoost
+            }
+        }
+        // Go through all msgId keys & find the highest frequency msgId
+        val msgIdWithHighestFrequency =
+            freqMap.entries
+                .maxWithOrNull { (msgId1, freq1), (msgId2, freq2) ->
+                    // ties are broken by more recent message
+                    if (freq1 == freq2) {
+                        helpFaceAuthStatuses
+                            .findLast { it.msgId == msgId1 }!!
+                            .createdAt
+                            .compareTo(
+                                helpFaceAuthStatuses.findLast { it.msgId == msgId2 }!!.createdAt
+                            )
+                    } else {
+                        freq1.compareTo(freq2)
+                    }
+                }
+                ?.key
+        return helpFaceAuthStatuses.findLast { it.msgId == msgIdWithHighestFrequency }
+    }
+
+    fun addMessage(helpFaceAuthStatus: HelpFaceAuthenticationStatus) {
+        helpFaceAuthStatuses.add(helpFaceAuthStatus)
+        Log.v(TAG, "added message=$helpFaceAuthStatus")
+    }
+
+    fun getMessageToShow(atTimestamp: Long): HelpFaceAuthenticationStatus? {
+        if (helpFaceAuthStatuses.isEmpty() || (atTimestamp - startTime) < startWindow) {
+            // there's not enough time that has passed to determine whether to show anything yet
+            Log.v(TAG, "No message; haven't made initial threshold window OR no messages")
+            return null
+        }
+        removeOldMessages(atTimestamp)
+        val messageToShow = getMostFrequentHelpMessage()
+        if (lastMessageIdShown != messageToShow?.msgId) {
+            Log.v(
+                TAG,
+                "showMessage previousLastMessageId=$lastMessageIdShown" +
+                    "\n\tmessageToShow=$messageToShow " +
+                    "\n\thelpFaceAuthStatusesSize=${helpFaceAuthStatuses.size}" +
+                    "\n\thelpFaceAuthStatuses=$helpFaceAuthStatuses"
+            )
+            lastMessageIdShown = messageToShow?.msgId
+        }
+        return messageToShow
+    }
+
+    fun startNewFaceAuthSession(faceAuthStartedTime: Long) {
+        Log.d(TAG, "startNewFaceAuthSession at startTime=$startTime")
+        startTime = faceAuthStartedTime
+        helpFaceAuthStatuses.clear()
+        lastMessageIdShown = null
+    }
+
+    companion object {
+        const val DEFAULT_WINDOW_MS = 200L
+    }
+}
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 c868d01..430887d 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
@@ -94,8 +94,16 @@
 
         val textColorError =
             view.resources.getColor(R.color.biometric_dialog_error, view.context.theme)
+
+        val attributes =
+            view.context.obtainStyledAttributes(
+                R.style.TextAppearance_AuthCredential_Indicator,
+                intArrayOf(android.R.attr.textColor)
+            )
         val textColorHint =
-            view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
+            if (constraintBp()) attributes.getColor(0, 0)
+            else view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
+        attributes.recycle()
 
         val logoView = view.requireViewById<ImageView>(R.id.logo)
         val logoDescriptionView = view.requireViewById<TextView>(R.id.logo_description)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
index 6cb9b16..810b6d1 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
@@ -32,7 +32,7 @@
 import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
 import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
 import com.android.systemui.deviceentry.shared.model.FaceFailureMessage
 import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage
@@ -75,7 +75,7 @@
     private val clock: SystemClock,
     private val biometricMessageInteractor: BiometricMessageInteractor,
     private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
-    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
     private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
     flags: ComposeBouncerFlags,
 ) {
@@ -119,7 +119,7 @@
                         }
                     } else if (authMethod.isSecure) {
                         combine(
-                            deviceEntryInteractor.deviceEntryRestrictionReason,
+                            deviceUnlockedInteractor.deviceEntryRestrictionReason,
                             lockoutMessage,
                             fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer,
                             resetToDefault,
@@ -413,7 +413,7 @@
         clock: SystemClock,
         biometricMessageInteractor: BiometricMessageInteractor,
         faceAuthInteractor: DeviceEntryFaceAuthInteractor,
-        deviceEntryInteractor: DeviceEntryInteractor,
+        deviceUnlockedInteractor: DeviceUnlockedInteractor,
         fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
         flags: ComposeBouncerFlags,
         userSwitcherViewModel: UserSwitcherViewModel,
@@ -427,7 +427,7 @@
             clock = clock,
             biometricMessageInteractor = biometricMessageInteractor,
             faceAuthInteractor = faceAuthInteractor,
-            deviceEntryInteractor = deviceEntryInteractor,
+            deviceUnlockedInteractor = deviceUnlockedInteractor,
             fingerprintInteractor = fingerprintInteractor,
             flags = flags,
             selectedUser = userSwitcherViewModel.selectedUser,
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index bde6f42..6b58c07 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -105,7 +105,13 @@
             .mapLatest(::determineSceneAfterTransition)
             .filterNotNull()
             .onEach { (nextScene, nextTransition) ->
-                communalSceneInteractor.changeScene(nextScene, nextTransition)
+                if (!communalSceneInteractor.isLaunchingWidget.value) {
+                    // When launching a widget, we don't want to animate the scene change or the
+                    // Communal Hub will reveal the wallpaper even though it shouldn't. Instead we
+                    // snap to the new scene as part of the launch animation, once the activity
+                    // launch is done, so we don't change scene here.
+                    communalSceneInteractor.changeScene(nextScene, nextTransition)
+                }
             }
             .launchIn(applicationScope)
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
index e3ef6bb..748c4fa 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
@@ -20,6 +20,7 @@
 import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL
 import android.content.IntentFilter
 import android.content.pm.UserInfo
+import android.os.UserHandle
 import android.provider.Settings
 import com.android.systemui.Flags.communalHub
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -102,7 +103,10 @@
             .broadcastFlow(
                 filter =
                     IntentFilter(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
-                user = user.userHandle
+                // In COPE management mode, the restriction from the managed profile may
+                // propagate to the main profile. Therefore listen to this broadcast across
+                // all users and update the state each time it changes.
+                user = UserHandle.ALL,
             )
             .emitOnStart()
             .map { devicePolicyManager.areKeyguardWidgetsAllowed(user.id) }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
index fd540c4..122f9647 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
@@ -48,6 +48,15 @@
     @Application private val applicationScope: CoroutineScope,
     private val communalSceneRepository: CommunalSceneRepository,
 ) {
+    val _isLaunchingWidget = MutableStateFlow(false)
+
+    /** Whether a widget launch is currently in progress. */
+    val isLaunchingWidget: StateFlow<Boolean> = _isLaunchingWidget.asStateFlow()
+
+    fun setIsLaunchingWidget(launching: Boolean) {
+        _isLaunchingWidget.value = launching
+    }
+
     /**
      * Asks for an asynchronous scene witch to [newScene], which will use the corresponding
      * installed transition or the one specified by [transitionKey], if provided.
diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
index a88b777..4e3d3ff 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
@@ -22,21 +22,25 @@
 import android.view.View
 import android.widget.RemoteViews
 import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.util.InteractionHandlerDelegate
 import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView
 import com.android.systemui.plugins.ActivityStarter
 import javax.inject.Inject
 
-/**
- * Handles interactions on smartspace elements on the hub.
- */
-class SmartspaceInteractionHandler @Inject constructor(
+/** Handles interactions on smartspace elements on the hub. */
+class SmartspaceInteractionHandler
+@Inject
+constructor(
     private val activityStarter: ActivityStarter,
+    communalSceneInteractor: CommunalSceneInteractor,
 ) : RemoteViews.InteractionHandler {
-    private val delegate = InteractionHandlerDelegate(
-        findViewToAnimate = { view -> view is SmartspaceAppWidgetHostView },
-        intentStarter = this::startIntent,
-    )
+    private val delegate =
+        InteractionHandlerDelegate(
+            communalSceneInteractor,
+            findViewToAnimate = { view -> view is SmartspaceAppWidgetHostView },
+            intentStarter = this::startIntent,
+        )
 
     override fun onInteraction(
         view: View,
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 02ecfe1..1e087f7 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
@@ -17,6 +17,7 @@
 package com.android.systemui.communal.ui.viewmodel
 
 import android.content.res.Resources
+import android.os.Bundle
 import android.view.View
 import android.view.accessibility.AccessibilityNodeInfo
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -209,6 +210,20 @@
                     )
                 )
             }
+
+            override fun performAccessibilityAction(
+                host: View,
+                action: Int,
+                args: Bundle?
+            ): Boolean {
+                when (action) {
+                    AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id -> {
+                        onOpenWidgetEditor()
+                        return true
+                    }
+                }
+                return super.performAccessibilityAction(host, action, args)
+            }
         }
 
     private val _isEnableWidgetDialogShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
index 40b182d..51a5fcd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
@@ -24,17 +24,17 @@
 import androidx.core.util.component1
 import androidx.core.util.component2
 import com.android.systemui.animation.ActivityTransitionAnimator
-
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController
 
 /** A delegate that can be used to launch activities from [RemoteViews] */
 class InteractionHandlerDelegate(
+    private val communalSceneInteractor: CommunalSceneInteractor,
     private val findViewToAnimate: (View) -> Boolean,
     private val intentStarter: IntentStarter,
 ) : RemoteViews.InteractionHandler {
 
-    /**
-     * Responsible for starting the pending intent for launching activities.
-     */
+    /** Responsible for starting the pending intent for launching activities. */
     fun interface IntentStarter {
         fun startPendingIntent(
             intent: PendingIntent,
@@ -57,7 +57,10 @@
                 // activities.
                 val hostView = getNearestParent(view)
                 val animationController =
-                    hostView?.let(ActivityTransitionAnimator.Controller::fromView)
+                    hostView?.let(ActivityTransitionAnimator.Controller::fromView)?.let {
+                        communalSceneInteractor.setIsLaunchingWidget(true)
+                        CommunalTransitionAnimatorController(it, communalSceneInteractor)
+                    }
                 val (fillInIntent, activityOptions) = launchOptions
                 intentStarter.startPendingIntent(
                     pendingIntent,
@@ -66,7 +69,6 @@
                     animationController
                 )
             }
-
             else -> RemoteViews.startPendingIntent(view, pendingIntent, launchOptions)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt
new file mode 100644
index 0000000..4efaf87
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.communal.widgets
+
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.DelegateTransitionAnimatorController
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
+
+/**
+ * An [ActivityTransitionAnimator.Controller] that takes care of updating the state of the Communal
+ * Hub at the right time.
+ */
+class CommunalTransitionAnimatorController(
+    delegate: ActivityTransitionAnimator.Controller,
+    private val communalSceneInteractor: CommunalSceneInteractor,
+) : DelegateTransitionAnimatorController(delegate) {
+    override fun onIntentStarted(willAnimate: Boolean) {
+        if (!willAnimate) {
+            // Other callbacks won't happen, so reset the state here.
+            communalSceneInteractor.setIsLaunchingWidget(false)
+        }
+        delegate.onIntentStarted(willAnimate)
+    }
+
+    override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+        communalSceneInteractor.setIsLaunchingWidget(false)
+        delegate.onTransitionAnimationCancelled(newKeyguardOccludedState)
+    }
+
+    override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+        communalSceneInteractor.snapToScene(CommunalScenes.Blank)
+        communalSceneInteractor.setIsLaunchingWidget(false)
+        delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
index 72f9180..519903e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
@@ -22,6 +22,7 @@
 import android.view.View
 import android.widget.RemoteViews
 import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.util.InteractionHandlerDelegate
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.ActivityStarter
@@ -32,10 +33,12 @@
 @Inject
 constructor(
     private val activityStarter: ActivityStarter,
+    private val communalSceneInteractor: CommunalSceneInteractor
 ) : RemoteViews.InteractionHandler {
 
     private val delegate =
         InteractionHandlerDelegate(
+            communalSceneInteractor,
             findViewToAnimate = { view -> view is CommunalAppWidgetHostView },
             intentStarter = this::startIntent,
         )
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
index fa52dad..9460eaf 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -57,16 +57,13 @@
 import com.android.systemui.log.SessionTracker
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.repository.UserRepository
 import com.google.errorprone.annotations.CompileTimeConstant
 import java.io.PrintWriter
-import java.util.Arrays
 import java.util.concurrent.Executor
-import java.util.stream.Collectors
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -170,7 +167,6 @@
 ) : DeviceEntryFaceAuthRepository, Dumpable {
     private var authCancellationSignal: CancellationSignal? = null
     private var detectCancellationSignal: CancellationSignal? = null
-    private var faceAcquiredInfoIgnoreList: Set<Int>
     private var retryCount = 0
 
     private var pendingAuthenticateRequest = MutableStateFlow<AuthenticationRequest?>(null)
@@ -240,14 +236,6 @@
             faceManager?.addLockoutResetCallback(faceLockoutResetCallback)
             faceAuthLogger.addLockoutResetCallbackDone()
         }
-        faceAcquiredInfoIgnoreList =
-            Arrays.stream(
-                    context.resources.getIntArray(
-                        R.array.config_face_acquire_device_entry_ignorelist
-                    )
-                )
-                .boxed()
-                .collect(Collectors.toSet())
         dumpManager.registerCriticalDumpable("DeviceEntryFaceAuthRepositoryImpl", this)
 
         canRunFaceAuth =
@@ -485,10 +473,8 @@
             }
 
             override fun onAuthenticationHelp(code: Int, helpStr: CharSequence?) {
-                if (faceAcquiredInfoIgnoreList.contains(code)) {
-                    return
-                }
-                _authenticationStatus.value = HelpFaceAuthenticationStatus(code, helpStr.toString())
+                _authenticationStatus.value =
+                    HelpFaceAuthenticationStatus(code, helpStr?.toString())
             }
 
             override fun onAuthenticationSucceeded(result: FaceManager.AuthenticationResult) {
@@ -731,7 +717,6 @@
         pw.println("  _pendingAuthenticateRequest: ${pendingAuthenticateRequest.value}")
         pw.println("  authCancellationSignal: $authCancellationSignal")
         pw.println("  detectCancellationSignal: $detectCancellationSignal")
-        pw.println("  faceAcquiredInfoIgnoreList: $faceAcquiredInfoIgnoreList")
         pw.println("  _authenticationStatus: ${_authenticationStatus.value}")
         pw.println("  _detectionStatus: ${_detectionStatus.value}")
         pw.println("  currentUserId: $currentUserId")
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
index 0f18978..e2ad774 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
@@ -39,12 +39,6 @@
      * the lockscreen.
      */
     val isBypassEnabled: StateFlow<Boolean>
-
-    /**
-     * Reports, to system server, that the user is "present" now. This is a signal that system
-     * server uses to know that the device has been entered.
-     */
-    suspend fun reportUserPresent()
 }
 
 /** Encapsulates application state for device entry. */
@@ -84,17 +78,6 @@
                 SharingStarted.Eagerly,
                 initialValue = keyguardBypassController.bypassEnabled,
             )
-
-    override suspend fun reportUserPresent() {
-        withContext(backgroundDispatcher) {
-            val selectedUserId = userRepository.selectedUser.value.userInfo.id
-            lockPatternUtils.userPresent(selectedUserId)
-        }
-    }
-
-    companion object {
-        private const val TAG = "DeviceEntryRepositoryImpl"
-    }
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractor.kt
new file mode 100644
index 0000000..34b1544
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractor.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.deviceentry.domain.interactor
+
+import android.content.res.Resources
+import android.hardware.biometrics.BiometricFaceConstants
+import com.android.systemui.biometrics.FaceHelpMessageDebouncer
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.deviceentry.shared.model.AcquiredFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.res.R
+import java.util.Arrays
+import java.util.stream.Collectors
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transform
+
+/**
+ * Process face authentication statuses.
+ * - Ignores face help messages based on R.array.config_face_acquire_device_entry_ignorelist.
+ * - Uses FaceHelpMessageDebouncer to debounce flickery help messages.
+ */
+@SysUISingleton
+class DeviceEntryFaceAuthStatusInteractor
+@Inject
+constructor(
+    repository: DeviceEntryFaceAuthRepository,
+    @Main private val resources: Resources,
+    @Application private val applicationScope: CoroutineScope,
+) {
+    private val faceHelpMessageDebouncer = FaceHelpMessageDebouncer()
+    private var faceAcquiredInfoIgnoreList: Set<Int> =
+        Arrays.stream(resources.getIntArray(R.array.config_face_acquire_device_entry_ignorelist))
+            .boxed()
+            .collect(Collectors.toSet())
+
+    val authenticationStatus: StateFlow<FaceAuthenticationStatus?> =
+        repository.authenticationStatus
+            .transform { authenticationStatus ->
+                if (authenticationStatus is AcquiredFaceAuthenticationStatus) {
+                    if (
+                        authenticationStatus.acquiredInfo ==
+                            BiometricFaceConstants.FACE_ACQUIRED_START
+                    ) {
+                        faceHelpMessageDebouncer.startNewFaceAuthSession(
+                            authenticationStatus.createdAt
+                        )
+                    }
+                }
+
+                if (authenticationStatus is HelpFaceAuthenticationStatus) {
+                    if (!faceAcquiredInfoIgnoreList.contains(authenticationStatus.msgId)) {
+                        faceHelpMessageDebouncer.addMessage(authenticationStatus)
+                    }
+
+                    val messageToShow =
+                        faceHelpMessageDebouncer.getMessageToShow(
+                            atTimestamp = authenticationStatus.createdAt,
+                        )
+                    if (messageToShow != null) {
+                        emit(messageToShow)
+                    }
+
+                    return@transform
+                }
+
+                emit(authenticationStatus)
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = null,
+            )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 425bb96..ea0e59b 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -16,33 +16,24 @@
 
 package com.android.systemui.deviceentry.domain.interactor
 
-import androidx.annotation.VisibleForTesting
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
-import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
-import com.android.systemui.flags.SystemPropertiesHelper
-import com.android.systemui.keyguard.domain.interactor.TrustInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.util.kotlin.Quad
 import com.android.systemui.utils.coroutines.flow.mapLatestConflated
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
@@ -61,12 +52,7 @@
     private val repository: DeviceEntryRepository,
     private val authenticationInteractor: AuthenticationInteractor,
     private val sceneInteractor: SceneInteractor,
-    faceAuthInteractor: DeviceEntryFaceAuthInteractor,
-    private val fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
-    private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor,
-    private val trustInteractor: TrustInteractor,
     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
-    private val systemPropertiesHelper: SystemPropertiesHelper,
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
 ) {
     /**
@@ -109,11 +95,6 @@
                     false
                 }
             }
-            .onEach { isDeviceEntered ->
-                if (isDeviceEntered) {
-                    repository.reportUserPresent()
-                }
-            }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.Eagerly,
@@ -156,70 +137,6 @@
                 initialValue = null,
             )
 
-    private val faceEnrolledAndEnabled = biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled
-    private val fingerprintEnrolledAndEnabled =
-        biometricSettingsInteractor.isFingerprintAuthEnrolledAndEnabled
-    private val trustAgentEnabled = trustInteractor.isEnrolledAndEnabled
-
-    private val faceOrFingerprintOrTrustEnabled: Flow<Triple<Boolean, Boolean, Boolean>> =
-        combine(faceEnrolledAndEnabled, fingerprintEnrolledAndEnabled, trustAgentEnabled, ::Triple)
-
-    /**
-     * Reason why device entry is restricted to certain authentication methods for the current user.
-     *
-     * Emits null when there are no device entry restrictions active.
-     */
-    val deviceEntryRestrictionReason: Flow<DeviceEntryRestrictionReason?> =
-        faceOrFingerprintOrTrustEnabled.flatMapLatest {
-            (faceEnabled, fingerprintEnabled, trustEnabled) ->
-            if (faceEnabled || fingerprintEnabled || trustEnabled) {
-                combine(
-                        biometricSettingsInteractor.authenticationFlags,
-                        faceAuthInteractor.isLockedOut,
-                        fingerprintAuthInteractor.isLockedOut,
-                        trustInteractor.isTrustAgentCurrentlyAllowed,
-                        ::Quad
-                    )
-                    .map { (authFlags, isFaceLockedOut, isFingerprintLockedOut, trustManaged) ->
-                        when {
-                            authFlags.isPrimaryAuthRequiredAfterReboot &&
-                                wasRebootedForMainlineUpdate ->
-                                DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate
-                            authFlags.isPrimaryAuthRequiredAfterReboot ->
-                                DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot
-                            authFlags.isPrimaryAuthRequiredAfterDpmLockdown ->
-                                DeviceEntryRestrictionReason.PolicyLockdown
-                            authFlags.isInUserLockdown -> DeviceEntryRestrictionReason.UserLockdown
-                            authFlags.isPrimaryAuthRequiredForUnattendedUpdate ->
-                                DeviceEntryRestrictionReason.UnattendedUpdate
-                            authFlags.isPrimaryAuthRequiredAfterTimeout ->
-                                DeviceEntryRestrictionReason.SecurityTimeout
-                            authFlags.isPrimaryAuthRequiredAfterLockout ->
-                                DeviceEntryRestrictionReason.BouncerLockedOut
-                            isFingerprintLockedOut ->
-                                DeviceEntryRestrictionReason.StrongBiometricsLockedOut
-                            isFaceLockedOut && faceAuthInteractor.isFaceAuthStrong() ->
-                                DeviceEntryRestrictionReason.StrongBiometricsLockedOut
-                            isFaceLockedOut -> DeviceEntryRestrictionReason.NonStrongFaceLockedOut
-                            authFlags.isSomeAuthRequiredAfterAdaptiveAuthRequest ->
-                                DeviceEntryRestrictionReason.AdaptiveAuthRequest
-                            (trustEnabled && !trustManaged) &&
-                                (authFlags.someAuthRequiredAfterTrustAgentExpired ||
-                                    authFlags.someAuthRequiredAfterUserRequest) ->
-                                DeviceEntryRestrictionReason.TrustAgentDisabled
-                            authFlags.strongerAuthRequiredAfterNonStrongBiometricsTimeout ->
-                                DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout
-                            else -> null
-                        }
-                    }
-            } else {
-                flowOf(null)
-            }
-        }
-
-    /** Whether the device is in lockdown mode, where bouncer input is required to unlock. */
-    val isInLockdown: Flow<Boolean> = deviceEntryRestrictionReason.map { it.isInLockdown() }
-
     /**
      * Attempt to enter the device and dismiss the lockscreen. If authentication is required to
      * unlock the device it will transition to bouncer.
@@ -268,27 +185,6 @@
         return repository.isLockscreenEnabled()
     }
 
-    fun DeviceEntryRestrictionReason?.isInLockdown(): Boolean {
-        return when (this) {
-            DeviceEntryRestrictionReason.UserLockdown -> true
-            DeviceEntryRestrictionReason.PolicyLockdown -> true
-
-            // Add individual enum value instead of using "else" so new reasons are guaranteed
-            // to be added here at compile-time.
-            null -> false
-            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -> false
-            DeviceEntryRestrictionReason.BouncerLockedOut -> false
-            DeviceEntryRestrictionReason.AdaptiveAuthRequest -> false
-            DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -> false
-            DeviceEntryRestrictionReason.TrustAgentDisabled -> false
-            DeviceEntryRestrictionReason.StrongBiometricsLockedOut -> false
-            DeviceEntryRestrictionReason.SecurityTimeout -> false
-            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate -> false
-            DeviceEntryRestrictionReason.UnattendedUpdate -> false
-            DeviceEntryRestrictionReason.NonStrongFaceLockedOut -> false
-        }
-    }
-
     /**
      * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
      * dismissed once the authentication challenge is completed. For example, completing a biometric
@@ -296,12 +192,4 @@
      * lockscreen.
      */
     val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
-
-    private val wasRebootedForMainlineUpdate
-        get() = systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE
-
-    companion object {
-        @VisibleForTesting const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last"
-        @VisibleForTesting const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update"
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
index 5141690..e17e530 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
@@ -16,20 +16,26 @@
 
 package com.android.systemui.deviceentry.domain.interactor
 
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
+import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus
+import com.android.systemui.flags.SystemPropertiesHelper
 import com.android.systemui.keyguard.domain.interactor.TrustInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
@@ -49,6 +55,8 @@
     faceAuthInteractor: DeviceEntryFaceAuthInteractor,
     fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
     private val powerInteractor: PowerInteractor,
+    private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor,
+    private val systemPropertiesHelper: SystemPropertiesHelper,
 ) {
 
     private val deviceUnlockSource =
@@ -69,6 +77,75 @@
                 .map { DeviceUnlockSource.BouncerInput }
         )
 
+    private val faceEnrolledAndEnabled = biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled
+    private val fingerprintEnrolledAndEnabled =
+        biometricSettingsInteractor.isFingerprintAuthEnrolledAndEnabled
+    private val trustAgentEnabled = trustInteractor.isEnrolledAndEnabled
+
+    private val faceOrFingerprintOrTrustEnabled: Flow<Triple<Boolean, Boolean, Boolean>> =
+        combine(faceEnrolledAndEnabled, fingerprintEnrolledAndEnabled, trustAgentEnabled, ::Triple)
+
+    /**
+     * Reason why device entry is restricted to certain authentication methods for the current user.
+     *
+     * Emits null when there are no device entry restrictions active.
+     */
+    val deviceEntryRestrictionReason: Flow<DeviceEntryRestrictionReason?> =
+        faceOrFingerprintOrTrustEnabled.flatMapLatest {
+            (faceEnabled, fingerprintEnabled, trustEnabled) ->
+            if (faceEnabled || fingerprintEnabled || trustEnabled) {
+                combine(
+                    biometricSettingsInteractor.authenticationFlags,
+                    faceAuthInteractor.isLockedOut,
+                    fingerprintAuthInteractor.isLockedOut,
+                    trustInteractor.isTrustAgentCurrentlyAllowed,
+                ) { authFlags, isFaceLockedOut, isFingerprintLockedOut, trustManaged ->
+                    when {
+                        authFlags.isPrimaryAuthRequiredAfterReboot &&
+                            wasRebootedForMainlineUpdate() ->
+                            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate
+                        authFlags.isPrimaryAuthRequiredAfterReboot ->
+                            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot
+                        authFlags.isPrimaryAuthRequiredAfterDpmLockdown ->
+                            DeviceEntryRestrictionReason.PolicyLockdown
+                        authFlags.isInUserLockdown -> DeviceEntryRestrictionReason.UserLockdown
+                        authFlags.isPrimaryAuthRequiredForUnattendedUpdate ->
+                            DeviceEntryRestrictionReason.UnattendedUpdate
+                        authFlags.isPrimaryAuthRequiredAfterTimeout ->
+                            DeviceEntryRestrictionReason.SecurityTimeout
+                        authFlags.isPrimaryAuthRequiredAfterLockout ->
+                            DeviceEntryRestrictionReason.BouncerLockedOut
+                        isFingerprintLockedOut ->
+                            DeviceEntryRestrictionReason.StrongBiometricsLockedOut
+                        isFaceLockedOut && faceAuthInteractor.isFaceAuthStrong() ->
+                            DeviceEntryRestrictionReason.StrongBiometricsLockedOut
+                        isFaceLockedOut -> DeviceEntryRestrictionReason.NonStrongFaceLockedOut
+                        authFlags.isSomeAuthRequiredAfterAdaptiveAuthRequest ->
+                            DeviceEntryRestrictionReason.AdaptiveAuthRequest
+                        (trustEnabled && !trustManaged) &&
+                            (authFlags.someAuthRequiredAfterTrustAgentExpired ||
+                                authFlags.someAuthRequiredAfterUserRequest) ->
+                            DeviceEntryRestrictionReason.TrustAgentDisabled
+                        authFlags.strongerAuthRequiredAfterNonStrongBiometricsTimeout ->
+                            DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout
+                        else -> null
+                    }
+                }
+            } else {
+                biometricSettingsInteractor.authenticationFlags.map { authFlags ->
+                    when {
+                        authFlags.isInUserLockdown -> DeviceEntryRestrictionReason.UserLockdown
+                        authFlags.isPrimaryAuthRequiredAfterDpmLockdown ->
+                            DeviceEntryRestrictionReason.PolicyLockdown
+                        else -> null
+                    }
+                }
+            }
+        }
+
+    /** Whether the device is in lockdown mode, where bouncer input is required to unlock. */
+    val isInLockdown: Flow<Boolean> = deviceEntryRestrictionReason.map { it.isInLockdown() }
+
     /**
      * Whether the device is unlocked or not, along with the information about the authentication
      * method that was used to unlock the device.
@@ -90,13 +167,18 @@
                     // Device is locked if SIM is locked.
                     flowOf(DeviceUnlockStatus(false, null))
                 } else {
-                    powerInteractor.isAsleep.flatMapLatest { isAsleep ->
-                        if (isAsleep) {
-                            flowOf(DeviceUnlockStatus(false, null))
-                        } else {
-                            deviceUnlockSource.map { DeviceUnlockStatus(true, it) }
+                    combine(
+                            powerInteractor.isAsleep,
+                            isInLockdown,
+                            ::Pair,
+                        )
+                        .flatMapLatestConflated { (isAsleep, isInLockdown) ->
+                            if (isAsleep || isInLockdown) {
+                                flowOf(DeviceUnlockStatus(false, null))
+                            } else {
+                                deviceUnlockSource.map { DeviceUnlockStatus(true, it) }
+                            }
                         }
-                    }
                 }
             }
             .stateIn(
@@ -104,4 +186,34 @@
                 started = SharingStarted.Eagerly,
                 initialValue = DeviceUnlockStatus(false, null),
             )
+
+    private fun DeviceEntryRestrictionReason?.isInLockdown(): Boolean {
+        return when (this) {
+            DeviceEntryRestrictionReason.UserLockdown -> true
+            DeviceEntryRestrictionReason.PolicyLockdown -> true
+
+            // Add individual enum value instead of using "else" so new reasons are guaranteed
+            // to be added here at compile-time.
+            null -> false
+            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -> false
+            DeviceEntryRestrictionReason.BouncerLockedOut -> false
+            DeviceEntryRestrictionReason.AdaptiveAuthRequest -> false
+            DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -> false
+            DeviceEntryRestrictionReason.TrustAgentDisabled -> false
+            DeviceEntryRestrictionReason.StrongBiometricsLockedOut -> false
+            DeviceEntryRestrictionReason.SecurityTimeout -> false
+            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate -> false
+            DeviceEntryRestrictionReason.UnattendedUpdate -> false
+            DeviceEntryRestrictionReason.NonStrongFaceLockedOut -> false
+        }
+    }
+
+    private fun wasRebootedForMainlineUpdate(): Boolean {
+        return systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE
+    }
+
+    companion object {
+        @VisibleForTesting const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last"
+        @VisibleForTesting const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
index d12ea45..c536d6b 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
@@ -90,6 +90,7 @@
     private val powerInteractor: PowerInteractor,
     private val biometricSettingsRepository: BiometricSettingsRepository,
     private val trustManager: TrustManager,
+    deviceEntryFaceAuthStatusInteractor: DeviceEntryFaceAuthStatusInteractor,
 ) : DeviceEntryFaceAuthInteractor {
 
     private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf()
@@ -276,9 +277,13 @@
     }
 
     private val faceAuthenticationStatusOverride = MutableStateFlow<FaceAuthenticationStatus?>(null)
+
     /** Provide the status of face authentication */
     override val authenticationStatus =
-        merge(faceAuthenticationStatusOverride.filterNotNull(), repository.authenticationStatus)
+        merge(
+            faceAuthenticationStatusOverride.filterNotNull(),
+            deviceEntryFaceAuthStatusInteractor.authenticationStatus.filterNotNull(),
+        )
 
     /** Provide the status of face detection */
     override val detectionStatus = repository.detectionStatus
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 7ae8409..e07b5c2 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -48,6 +48,7 @@
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.UiEventLoggerImpl;
+import com.android.systemui.Flags;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.plugins.SensorManagerPlugin;
 import com.android.systemui.statusbar.phone.DozeParameters;
@@ -426,7 +427,11 @@
         }
 
         if (!anyListening) {
-            mSecureSettings.unregisterContentObserverSync(mSettingsObserver);
+            if (Flags.registerContentObserversAsync()) {
+                mSecureSettings.unregisterContentObserverAsync(mSettingsObserver);
+            } else {
+                mSecureSettings.unregisterContentObserverSync(mSettingsObserver);
+            }
         } else if (!mSettingRegistered) {
             for (TriggerSensor s : mTriggerSensors) {
                 s.registerSettingsObserver(mSettingsObserver);
@@ -750,8 +755,13 @@
 
         public void registerSettingsObserver(ContentObserver settingsObserver) {
             if (mConfigured && !TextUtils.isEmpty(mSetting)) {
-                mSecureSettings.registerContentObserverForUserSync(
-                        mSetting, mSettingsObserver, UserHandle.USER_ALL);
+                if (Flags.registerContentObserversAsync()) {
+                    mSecureSettings.registerContentObserverForUserAsync(
+                            mSetting, mSettingsObserver, UserHandle.USER_ALL);
+                } else {
+                    mSecureSettings.registerContentObserverForUserSync(
+                            mSetting, mSettingsObserver, UserHandle.USER_ALL);
+                }
             }
         }
 
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 e2bcb6b..53b9261 100644
--- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
@@ -17,8 +17,12 @@
 package com.android.systemui.education.dagger
 
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.education.data.repository.ContextualEducationRepository
+import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl
+import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import java.time.Clock
 import javax.inject.Qualifier
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -26,8 +30,15 @@
 
 @Module
 interface ContextualEducationModule {
+    @Binds
+    fun bindContextualEducationRepository(
+        impl: ContextualEducationRepositoryImpl
+    ): ContextualEducationRepository
+
     @Qualifier annotation class EduDataStoreScope
 
+    @Qualifier annotation class EduClock
+
     companion object {
         @EduDataStoreScope
         @Provides
@@ -36,5 +47,11 @@
         ): CoroutineScope {
             return CoroutineScope(bgDispatcher + SupervisorJob())
         }
+
+        @EduClock
+        @Provides
+        fun provideEduClock(): Clock {
+            return Clock.systemUTC()
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt
index af35e8c..9f6cb4d 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt
@@ -16,11 +16,14 @@
 
 package com.android.systemui.education.data.model
 
+import java.time.Instant
+
 /**
  * Model to store education data related to each gesture (e.g. Back, Home, All Apps, Overview). Each
  * gesture stores its own model separately.
  */
 data class GestureEduModel(
-    val signalCount: Int,
-    val educationShownCount: Int,
+    val signalCount: Int = 0,
+    val educationShownCount: Int = 0,
+    val lastShortcutTriggeredTime: Instant? = null,
 )
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 c9dd833..248b7a5 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,26 +17,50 @@
 package com.android.systemui.education.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
+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
+
+/** Encapsulates the functions of ContextualEducationRepository. */
+interface ContextualEducationRepository {
+    fun setUser(userId: Int)
+
+    fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel>
+
+    suspend fun incrementSignalCount(gestureType: GestureType)
+
+    suspend fun updateShortcutTriggerTime(gestureType: GestureType)
+}
 
 /**
  * Provide methods to read and update on field level and allow setting datastore when user is
  * changed
  */
 @SysUISingleton
-class ContextualEducationRepository
+class ContextualEducationRepositoryImpl
 @Inject
-constructor(private val userEduRepository: UserContextualEducationRepository) {
+constructor(
+    @EduClock private val clock: Clock,
+    private val userEduRepository: UserContextualEducationRepository
+) : ContextualEducationRepository {
     /** To change data store when user is changed */
-    fun setUser(userId: Int) = userEduRepository.setUser(userId)
+    override fun setUser(userId: Int) = userEduRepository.setUser(userId)
 
-    fun readGestureEduModelFlow(gestureType: GestureType) =
+    override fun readGestureEduModelFlow(gestureType: GestureType) =
         userEduRepository.readGestureEduModelFlow(gestureType)
 
-    suspend fun incrementSignalCount(gestureType: GestureType) {
+    override suspend fun incrementSignalCount(gestureType: GestureType) {
         userEduRepository.updateGestureEduModel(gestureType) {
             it.copy(signalCount = it.signalCount + 1)
         }
     }
+
+    override suspend fun updateShortcutTriggerTime(gestureType: GestureType) {
+        userEduRepository.updateGestureEduModel(gestureType) {
+            it.copy(lastShortcutTriggeredTime = clock.instant())
+        }
+    }
 }
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 229511a..b7fc773 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
@@ -18,16 +18,19 @@
 
 import android.content.Context
 import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.MutablePreferences
 import androidx.datastore.preferences.core.PreferenceDataStoreFactory
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.edit
 import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.longPreferencesKey
 import androidx.datastore.preferences.preferencesDataStoreFile
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 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
 import kotlinx.coroutines.CoroutineScope
@@ -55,6 +58,7 @@
     companion object {
         const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT"
         const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN"
+        const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME"
 
         const val DATASTORE_DIR = "education/USER%s_ContextualEducation"
     }
@@ -91,6 +95,10 @@
         return GestureEduModel(
             signalCount = preferences[getSignalCountKey(gestureType)] ?: 0,
             educationShownCount = preferences[getEducationShownCountKey(gestureType)] ?: 0,
+            lastShortcutTriggeredTime =
+                preferences[getLastShortcutTriggeredTimeKey(gestureType)]?.let {
+                    Instant.ofEpochMilli(it)
+                },
         )
     }
 
@@ -103,6 +111,11 @@
             val updatedModel = transform(currentModel)
             preferences[getSignalCountKey(gestureType)] = updatedModel.signalCount
             preferences[getEducationShownCountKey(gestureType)] = updatedModel.educationShownCount
+            updateTimeByInstant(
+                preferences,
+                updatedModel.lastShortcutTriggeredTime,
+                getLastShortcutTriggeredTimeKey(gestureType)
+            )
         }
     }
 
@@ -111,4 +124,19 @@
 
     private fun getEducationShownCountKey(gestureType: GestureType): Preferences.Key<Int> =
         intPreferencesKey(gestureType.name + NUMBER_OF_EDU_SHOWN_SUFFIX)
+
+    private fun getLastShortcutTriggeredTimeKey(gestureType: GestureType): Preferences.Key<Long> =
+        longPreferencesKey(gestureType.name + LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX)
+
+    private fun updateTimeByInstant(
+        preferences: MutablePreferences,
+        instant: Instant?,
+        key: Preferences.Key<Long>
+    ) {
+        if (instant != null) {
+            preferences[key] = instant.toEpochMilli()
+        } else {
+            preferences.remove(key)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 1e4fb4f..493afde 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -64,7 +64,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.service.dreams.IDreamManager;
 import android.sysprop.TelephonyProperties;
 import android.telecom.TelecomManager;
 import android.telephony.ServiceState;
@@ -197,7 +196,6 @@
     private final Context mContext;
     private final GlobalActionsManager mWindowManagerFuncs;
     private final AudioManager mAudioManager;
-    private final IDreamManager mDreamManager;
     private final DevicePolicyManager mDevicePolicyManager;
     private final LockPatternUtils mLockPatternUtils;
     private final SelectedUserInteractor mSelectedUserInteractor;
@@ -345,7 +343,6 @@
             Context context,
             GlobalActionsManager windowManagerFuncs,
             AudioManager audioManager,
-            IDreamManager iDreamManager,
             DevicePolicyManager devicePolicyManager,
             LockPatternUtils lockPatternUtils,
             BroadcastDispatcher broadcastDispatcher,
@@ -382,7 +379,6 @@
         mContext = context;
         mWindowManagerFuncs = windowManagerFuncs;
         mAudioManager = audioManager;
-        mDreamManager = iDreamManager;
         mDevicePolicyManager = devicePolicyManager;
         mLockPatternUtils = lockPatternUtils;
         mTelephonyListenerManager = telephonyListenerManager;
@@ -510,20 +506,7 @@
         mHandler.sendEmptyMessage(MESSAGE_DISMISS);
     }
 
-    protected void awakenIfNecessary() {
-        if (mDreamManager != null) {
-            try {
-                if (mDreamManager.isDreaming()) {
-                    mDreamManager.awaken();
-                }
-            } catch (RemoteException e) {
-                // we tried
-            }
-        }
-    }
-
     protected void handleShow(@Nullable Expandable expandable) {
-        awakenIfNecessary();
         mDialog = createDialog();
         prepareDialog();
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
index 49817b2..495e8f3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
@@ -233,7 +233,7 @@
             Log.wtf(TAG, "Unsupported modifiers remaining: $remainingModifiers")
             return null
         }
-        if (info.keycode != 0) {
+        if (info.keycode != 0 || info.baseCharacter > Char.MIN_VALUE) {
             keys += toShortcutKey(keyCharacterMap, info.keycode, info.baseCharacter) ?: return null
         }
         if (keys.isEmpty()) {
@@ -253,7 +253,7 @@
             return ShortcutKey.Icon(iconResId)
         }
         if (baseCharacter > Char.MIN_VALUE) {
-            return ShortcutKey.Text(baseCharacter.toString())
+            return ShortcutKey.Text(baseCharacter.uppercase())
         }
         val specialKeyLabel = ShortcutHelperKeys.specialKeyLabels[keyCode]
         if (specialKeyLabel != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSource.kt
index d7cb7db..d6c6d5b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSource.kt
@@ -16,92 +16,29 @@
 
 package com.android.systemui.keyboard.shortcut.data.source
 
-import android.content.Intent
-import android.content.res.Resources
-import android.view.KeyEvent
 import android.view.KeyboardShortcutGroup
-import android.view.KeyboardShortcutInfo
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.res.R
-import com.android.systemui.util.icons.AppCategoryIconProvider
+import android.view.WindowManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.extensions.copy
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
 
 class AppCategoriesShortcutsSource
 @Inject
 constructor(
-    private val appCategoryIconProvider: AppCategoryIconProvider,
-    @Main private val resources: Resources,
+    private val windowManager: WindowManager,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) : KeyboardShortcutGroupsSource {
 
-    override suspend fun shortcutGroups(deviceId: Int) =
-        listOf(
-            KeyboardShortcutGroup(
-                /* label = */ resources.getString(R.string.keyboard_shortcut_group_applications),
-                /* items = */ shortcuts()
-            )
-        )
-
-    private suspend fun shortcuts(): List<KeyboardShortcutInfo> =
-        listOfNotNull(
-                assistantAppShortcutInfo(),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_BROWSER,
-                    R.string.keyboard_shortcut_group_applications_browser,
-                    KeyEvent.KEYCODE_B
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_CONTACTS,
-                    R.string.keyboard_shortcut_group_applications_contacts,
-                    KeyEvent.KEYCODE_C
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_EMAIL,
-                    R.string.keyboard_shortcut_group_applications_email,
-                    KeyEvent.KEYCODE_E
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_CALENDAR,
-                    R.string.keyboard_shortcut_group_applications_calendar,
-                    KeyEvent.KEYCODE_K
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_MAPS,
-                    R.string.keyboard_shortcut_group_applications_maps,
-                    KeyEvent.KEYCODE_M
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_MUSIC,
-                    R.string.keyboard_shortcut_group_applications_music,
-                    KeyEvent.KEYCODE_P
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_MESSAGING,
-                    R.string.keyboard_shortcut_group_applications_sms,
-                    KeyEvent.KEYCODE_S
-                ),
-                appCategoryShortcutInfo(
-                    Intent.CATEGORY_APP_CALCULATOR,
-                    R.string.keyboard_shortcut_group_applications_calculator,
-                    KeyEvent.KEYCODE_U
-                ),
-            )
-            .sortedBy { it.label!!.toString().lowercase() }
-
-    private suspend fun assistantAppShortcutInfo(): KeyboardShortcutInfo? {
-        val assistantIcon = appCategoryIconProvider.assistantAppIcon() ?: return null
-        return KeyboardShortcutInfo(
-            /* label = */ resources.getString(R.string.keyboard_shortcut_group_applications_assist),
-            /* icon = */ assistantIcon,
-            /* keycode = */ KeyEvent.KEYCODE_A,
-            /* modifiers = */ KeyEvent.META_META_ON,
-        )
-    }
-
-    private suspend fun appCategoryShortcutInfo(category: String, labelResId: Int, keycode: Int) =
-        KeyboardShortcutInfo(
-            /* label = */ resources.getString(labelResId),
-            /* icon = */ appCategoryIconProvider.categoryAppIcon(category),
-            /* keycode = */ keycode,
-            /* modifiers = */ KeyEvent.META_META_ON,
-        )
+    override suspend fun shortcutGroups(deviceId: Int): List<KeyboardShortcutGroup> =
+        withContext(backgroundDispatcher) {
+            val group = windowManager.getApplicationLaunchKeyboardShortcuts(deviceId)
+            return@withContext if (group == null) {
+                emptyList()
+            } else {
+                val sortedShortcutItems = group.items.sortedBy { it.label!!.toString().lowercase() }
+                listOf(group.copy(items = sortedShortcutItems))
+            }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/extensions/KeyboardShortcutGroupExtensions.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/extensions/KeyboardShortcutGroupExtensions.kt
new file mode 100644
index 0000000..3a120bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/extensions/KeyboardShortcutGroupExtensions.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.keyboard.shortcut.extensions
+
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+
+fun KeyboardShortcutGroup.copy(
+    label: CharSequence = getLabel(),
+    items: List<KeyboardShortcutInfo> = getItems(),
+    isSystemGroup: Boolean = isSystemGroup(),
+    packageName: CharSequence? = getPackageName(),
+) = KeyboardShortcutGroup(label, items, isSystemGroup).also { it.packageName = packageName }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index bbc3d76..89c7178 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -168,8 +168,7 @@
      */
     @Deprecated("Will be merged into maybeStartTransitionToOccludedOrInsecureCamera")
     suspend fun maybeHandleInsecurePowerGesture(): Boolean {
-        // TODO(b/336576536): Check if adaptation for scene framework is needed
-        if (SceneContainerFlag.isEnabled) return true
+        if (SceneContainerFlag.isEnabled) return false
         if (keyguardOcclusionInteractor.shouldTransitionFromPowerButtonGesture()) {
             if (keyguardInteractor.isKeyguardDismissible.value) {
                 startTransitionTo(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt
index 853f176..1fd609d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt
@@ -26,6 +26,7 @@
 import android.view.WindowManager
 import android.widget.ProgressBar
 import androidx.core.view.isGone
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
 import javax.inject.Inject
@@ -37,7 +38,7 @@
 @Inject
 constructor(
     private val layoutInflater: LayoutInflater,
-    private val windowManager: WindowManager,
+    private val windowManager: ViewCaptureAwareWindowManager,
 ) {
     private var overlayView: View? = null
 
@@ -90,7 +91,7 @@
     ) {
         if (overlayView == null) {
             overlayView = layoutInflater.inflate(R.layout.sidefps_progress_bar, null, false)
-            windowManager.addView(overlayView, overlayViewParams)
+            windowManager.addView(requireNotNull(overlayView), overlayViewParams)
             progressBar?.pivotX = 0.0f
             progressBar?.pivotY = 0.0f
         }
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 754ed6c..1ee0368 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
@@ -25,6 +25,7 @@
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.emptyFlow
@@ -45,6 +46,13 @@
             edge = Edge.create(from = DREAMING, to = AOD),
         )
 
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = 300.milliseconds,
+            onStep = { it },
+        )
+
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
 
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 7c46807..350ceb4 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
@@ -91,8 +91,9 @@
     private val dozingToGoneTransitionViewModel: DozingToGoneTransitionViewModel,
     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
     private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
-    private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+    private val dreamingToAodTransitionViewModel: DreamingToAodTransitionViewModel,
     private val dreamingToGoneTransitionViewModel: DreamingToGoneTransitionViewModel,
+    private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
     private val glanceableHubToLockscreenTransitionViewModel:
         GlanceableHubToLockscreenTransitionViewModel,
     private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
@@ -243,6 +244,7 @@
                         dozingToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         dozingToLockscreenTransitionViewModel.lockscreenAlpha,
                         dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
+                        dreamingToAodTransitionViewModel.lockscreenAlpha,
                         dreamingToGoneTransitionViewModel.lockscreenAlpha,
                         dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
                         glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 3b337fc..4bfefda 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.content.res.Resources
+import com.android.compose.animation.scene.SceneKey
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.SysUISingleton
@@ -26,14 +27,17 @@
 import com.android.systemui.keyguard.shared.model.ClockSize
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
@@ -57,19 +61,6 @@
 
     val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide
 
-    val areNotificationsVisible: StateFlow<Boolean> =
-        combine(
-                clockSize,
-                shadeInteractor.isShadeLayoutWide,
-            ) { clockSize, isShadeLayoutWide ->
-                clockSize == ClockSize.SMALL || isShadeLayoutWide
-            }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = false,
-            )
-
     /** Amount of horizontal translation that should be applied to elements in the scene. */
     val unfoldTranslations: StateFlow<UnfoldTranslations> =
         combine(
@@ -97,6 +88,25 @@
                 initialValue = true,
             )
 
+    /**
+     * Returns a flow that indicates whether lockscreen notifications should be rendered in the
+     * given [sceneKey].
+     */
+    fun areNotificationsVisible(sceneKey: SceneKey): Flow<Boolean> {
+        // `Scenes.NotificationsShade` renders its own separate notifications stack, so when it's
+        // open we avoid rendering the lockscreen notifications stack.
+        if (sceneKey == Scenes.NotificationsShade) {
+            return flowOf(false)
+        }
+
+        return combine(
+            clockSize,
+            shadeInteractor.isShadeLayoutWide,
+        ) { clockSize, isShadeLayoutWide ->
+            clockSize == ClockSize.SMALL || isShadeLayoutWide
+        }
+    }
+
     fun getSmartSpacePaddingTop(resources: Resources): Int {
         return if (clockSize.value == ClockSize.LARGE) {
             resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionLog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionLog.kt
new file mode 100644
index 0000000..a80bc09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 javax.inject.Qualifier
+
+/** Logs for media projection related events. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class MediaProjectionLog
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt
index 3489459..7fd77a9 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt
@@ -16,12 +16,25 @@
 
 package com.android.systemui.mediaprojection
 
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.mediaprojection.data.repository.MediaProjectionManagerRepository
 import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 
 @Module
 interface MediaProjectionModule {
     @Binds fun mediaRepository(impl: MediaProjectionManagerRepository): MediaProjectionRepository
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        @MediaProjectionLog
+        fun provideMediaProjectionLogBuffer(factory: LogBufferFactory): LogBuffer {
+            return factory.create("MediaProjection", 50)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
index c90f197..5704e80 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
@@ -21,7 +21,6 @@
 import android.media.projection.MediaProjectionInfo
 import android.media.projection.MediaProjectionManager
 import android.os.Handler
-import android.util.Log
 import android.view.ContentRecordingSession
 import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -29,6 +28,9 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.mediaprojection.MediaProjectionLog
 import com.android.systemui.mediaprojection.MediaProjectionServiceHelper
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository
@@ -56,22 +58,27 @@
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val tasksRepository: TasksRepository,
     private val mediaProjectionServiceHelper: MediaProjectionServiceHelper,
+    @MediaProjectionLog private val logger: LogBuffer,
 ) : MediaProjectionRepository {
 
     override suspend fun switchProjectedTask(task: RunningTaskInfo) {
         withContext(backgroundDispatcher) {
             if (mediaProjectionServiceHelper.updateTaskRecordingSession(task.token)) {
-                Log.d(TAG, "Successfully switched projected task")
+                logger.log(TAG, LogLevel.DEBUG, {}, { "Successfully switched projected task" })
             } else {
-                Log.d(TAG, "Failed to switch projected task")
+                logger.log(TAG, LogLevel.WARNING, {}, { "Failed to switch projected task" })
             }
         }
     }
 
     override suspend fun stopProjecting() {
         withContext(backgroundDispatcher) {
-            // TODO(b/332662551): Convert Logcat to LogBuffer.
-            Log.d(TAG, "Requesting MediaProjectionManager#stopActiveProjection")
+            logger.log(
+                TAG,
+                LogLevel.DEBUG,
+                {},
+                { "Requesting MediaProjectionManager#stopActiveProjection" },
+            )
             mediaProjectionManager.stopActiveProjection()
         }
     }
@@ -81,12 +88,22 @@
                 val callback =
                     object : MediaProjectionManager.Callback() {
                         override fun onStart(info: MediaProjectionInfo?) {
-                            Log.d(TAG, "MediaProjectionManager.Callback#onStart")
+                            logger.log(
+                                TAG,
+                                LogLevel.DEBUG,
+                                {},
+                                { "MediaProjectionManager.Callback#onStart" },
+                            )
                             trySendWithFailureLogging(CallbackEvent.OnStart, TAG)
                         }
 
                         override fun onStop(info: MediaProjectionInfo?) {
-                            Log.d(TAG, "MediaProjectionManager.Callback#onStop")
+                            logger.log(
+                                TAG,
+                                LogLevel.DEBUG,
+                                {},
+                                { "MediaProjectionManager.Callback#onStop" },
+                            )
                             trySendWithFailureLogging(CallbackEvent.OnStop, TAG)
                         }
 
@@ -94,7 +111,12 @@
                             info: MediaProjectionInfo,
                             session: ContentRecordingSession?
                         ) {
-                            Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session")
+                            logger.log(
+                                TAG,
+                                LogLevel.DEBUG,
+                                { str1 = session.toString() },
+                                { "MediaProjectionManager.Callback#onSessionStarted: $str1" },
+                            )
                             trySendWithFailureLogging(
                                 CallbackEvent.OnRecordingSessionSet(info, session),
                                 TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 18358a7..d8c13b6 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -33,6 +33,7 @@
 import androidx.core.os.postDelayed
 import androidx.core.view.isVisible
 import androidx.dynamicanimation.animation.DynamicAnimation
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
 import com.android.internal.jank.Cuj
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.util.LatencyTracker
@@ -83,7 +84,7 @@
 class BackPanelController
 internal constructor(
     context: Context,
-    private val windowManager: WindowManager,
+    private val windowManager: ViewCaptureAwareWindowManager,
     private val viewConfiguration: ViewConfiguration,
     private val mainHandler: Handler,
     private val systemClock: SystemClock,
@@ -102,7 +103,7 @@
     class Factory
     @Inject
     constructor(
-        private val windowManager: WindowManager,
+        private val windowManager: ViewCaptureAwareWindowManager,
         private val viewConfiguration: ViewConfiguration,
         @BackPanelUiThread private val uiThreadContext: UiThreadContext,
         private val systemClock: SystemClock,
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 c9be993..947336d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -91,6 +91,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.util.concurrency.BackPanelUiThread;
 import com.android.systemui.util.concurrency.UiThreadContext;
@@ -297,6 +298,7 @@
     private Date mTmpLogDate = new Date();
 
     private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver;
+    private final NotificationShadeWindowController mNotificationShadeWindowController;
 
     private final NavigationEdgeBackPlugin.BackCallback mBackCallback =
             new NavigationEdgeBackPlugin.BackCallback() {
@@ -423,7 +425,8 @@
             Optional<DesktopMode> desktopModeOptional,
             FalsingManager falsingManager,
             Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider,
-            Provider<LightBarController> lightBarControllerProvider) {
+            Provider<LightBarController> lightBarControllerProvider,
+            NotificationShadeWindowController notificationShadeWindowController) {
         mContext = context;
         mDisplayId = context.getDisplayId();
         mUiThreadContext = uiThreadContext;
@@ -479,6 +482,7 @@
                 this::onNavigationSettingsChanged);
 
         updateCurrentUserResources();
+        mNotificationShadeWindowController = notificationShadeWindowController;
     }
 
     public void setStateChangeCallback(Runnable callback) {
@@ -1297,6 +1301,9 @@
         mBackAnimation.setPilferPointerCallback(() -> {
             pilferPointers();
         });
+        mBackAnimation.setTopUiRequestCallback(
+                (requestTopUi, tag) -> mUiThreadContext.getExecutor().execute(() ->
+                        mNotificationShadeWindowController.setRequestTopUi(requestTopUi, tag)));
         updateBackAnimationThresholds();
         if (mLightBarControllerProvider.get() != null) {
             mBackAnimation.setStatusBarCustomizer((appearance) -> {
@@ -1333,6 +1340,7 @@
         private final Provider<BackGestureTfClassifierProvider>
                 mBackGestureTfClassifierProviderProvider;
         private final Provider<LightBarController> mLightBarControllerProvider;
+        private final NotificationShadeWindowController mNotificationShadeWindowController;
 
         @Inject
         public Factory(OverviewProxyService overviewProxyService,
@@ -1353,7 +1361,8 @@
                         FalsingManager falsingManager,
                         Provider<BackGestureTfClassifierProvider>
                                 backGestureTfClassifierProviderProvider,
-                        Provider<LightBarController> lightBarControllerProvider) {
+                Provider<LightBarController> lightBarControllerProvider,
+                NotificationShadeWindowController notificationShadeWindowController) {
             mOverviewProxyService = overviewProxyService;
             mSysUiState = sysUiState;
             mPluginManager = pluginManager;
@@ -1372,6 +1381,7 @@
             mFalsingManager = falsingManager;
             mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
             mLightBarControllerProvider = lightBarControllerProvider;
+            mNotificationShadeWindowController = notificationShadeWindowController;
         }
 
         /** Construct a {@link EdgeBackGestureHandler}. */
@@ -1396,7 +1406,8 @@
                             mDesktopModeOptional,
                             mFalsingManager,
                             mBackGestureTfClassifierProviderProvider,
-                            mLightBarControllerProvider));
+                            mLightBarControllerProvider,
+                            mNotificationShadeWindowController));
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
index e832abb..afdfa59 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
@@ -40,6 +40,8 @@
 import static com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
 import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen;
 import static com.android.systemui.shared.rotation.RotationButtonController.DEBUG_ROTATION;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
@@ -48,8 +50,6 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
-import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE;
-import static com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode;
 import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG_WINDOW_STATE;
 import static com.android.systemui.statusbar.phone.CentralSurfaces.dumpBarTransitions;
 import static com.android.systemui.util.Utils.isGesturalModeOnDefaultDisplay;
@@ -97,6 +97,7 @@
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodManager;
 
 import androidx.annotation.Nullable;
@@ -117,17 +118,17 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
-import com.android.systemui.navigationbar.views.buttons.NavBarButtonClickLogger;
 import com.android.systemui.navigationbar.NavBarHelper;
 import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener;
+import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
+import com.android.systemui.navigationbar.gestural.QuickswitchOrientedNavHandle;
 import com.android.systemui.navigationbar.views.buttons.ButtonDispatcher;
 import com.android.systemui.navigationbar.views.buttons.DeadZone;
 import com.android.systemui.navigationbar.views.buttons.KeyButtonView;
+import com.android.systemui.navigationbar.views.buttons.NavBarButtonClickLogger;
 import com.android.systemui.navigationbar.views.buttons.NavbarOrientationTrackingLogger;
-import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
-import com.android.systemui.navigationbar.gestural.QuickswitchOrientedNavHandle;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.recents.Recents;
@@ -1348,6 +1349,9 @@
 
         ButtonDispatcher imeSwitcherButton = mView.getImeSwitchButton();
         imeSwitcherButton.setOnClickListener(this::onImeSwitcherClick);
+        if (Flags.imeSwitcherRevamp()) {
+            imeSwitcherButton.setOnLongClickListener(this::onImeSwitcherLongClick);
+        }
 
         updateScreenPinningGestures();
     }
@@ -1501,12 +1505,29 @@
         mCommandQueue.toggleRecentApps();
     }
 
-    private void onImeSwitcherClick(View v) {
+    @VisibleForTesting
+    void onImeSwitcherClick(View v) {
+        mNavBarButtonClickLogger.logImeSwitcherClick();
+        if (Flags.imeSwitcherRevamp()) {
+            mInputMethodManager.onImeSwitchButtonClickFromSystem(mDisplayId);
+        } else {
+            mInputMethodManager.showInputMethodPickerFromSystem(
+                    true /* showAuxiliarySubtypes */, mDisplayId);
+        }
+        mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
+    }
+
+    @VisibleForTesting
+    boolean onImeSwitcherLongClick(View v) {
+        if (!Flags.imeSwitcherRevamp()) {
+            return false;
+        }
         mNavBarButtonClickLogger.logImeSwitcherClick();
         mInputMethodManager.showInputMethodPickerFromSystem(
                 true /* showAuxiliarySubtypes */, mDisplayId);
-        mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
-    };
+        mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+        return true;
+    }
 
     private boolean onLongPressBackHome(View v) {
         return onLongPressNavigationButtons(v, R.id.back, R.id.home);
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
index 0f36097..1dbd500 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
@@ -19,7 +19,6 @@
 import static android.inputmethodservice.InputMethodService.canImeRenderGesturalNavButtons;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
 
-import static com.android.systemui.Flags.enableViewCaptureTracing;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SEARCH_DISABLED;
@@ -38,7 +37,6 @@
 import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.media.permission.SafeCloseable;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.util.AttributeSet;
@@ -61,19 +59,18 @@
 import androidx.annotation.Nullable;
 
 import com.android.app.animation.Interpolators;
-import com.android.app.viewcapture.ViewCaptureFactory;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.settingslib.Utils;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.ScreenPinningNotify;
+import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.navigationbar.views.buttons.ButtonDispatcher;
 import com.android.systemui.navigationbar.views.buttons.ContextualButton;
 import com.android.systemui.navigationbar.views.buttons.ContextualButtonGroup;
 import com.android.systemui.navigationbar.views.buttons.DeadZone;
 import com.android.systemui.navigationbar.views.buttons.KeyButtonDrawable;
 import com.android.systemui.navigationbar.views.buttons.NearestTouchFrame;
-import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.res.R;
 import com.android.systemui.settings.DisplayTracker;
@@ -181,7 +178,6 @@
     private boolean mOverviewProxyEnabled;
     private boolean mShowSwipeUpUi;
     private UpdateActiveTouchRegionsCallback mUpdateActiveTouchRegionsCallback;
-    private SafeCloseable mViewCaptureCloseable;
 
     private class NavTransitionListener implements TransitionListener {
         private boolean mBackTransitioning;
@@ -1082,10 +1078,6 @@
         }
 
         updateNavButtonIcons();
-        if (enableViewCaptureTracing()) {
-            mViewCaptureCloseable = ViewCaptureFactory.getInstance(getContext())
-                    .startCapture(getRootView(), ".NavigationBarView");
-        }
     }
 
     @Override
@@ -1098,9 +1090,6 @@
             mFloatingRotationButton.hide();
             mRotationButtonController.unregisterListeners();
         }
-        if (mViewCaptureCloseable != null) {
-            mViewCaptureCloseable.close();
-        }
     }
 
     void dump(PrintWriter pw) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java
index 1e85d6c..133d14d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java
@@ -112,6 +112,9 @@
         @UiEvent(doc = "The overview button was long-pressed in the navigation bar.")
         NAVBAR_OVERVIEW_BUTTON_LONGPRESS(538),
 
+        @UiEvent(doc = "The ime switcher button was long-pressed in the navigation bar.")
+        NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS(1799),
+
         NONE(0);  // an event we should not log
 
         private final int mId;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java
index d68b22b..4d6cf78 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java
@@ -29,7 +29,6 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.server.display.feature.flags.Flags;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.settings.UserTracker;
@@ -81,10 +80,17 @@
                 mAvailable = true;
                 synchronized (mListeners) {
                     if (mListeners.size() > 0) {
-                        mSecureSettings.unregisterContentObserverSync(mContentObserver);
-                        mSecureSettings.registerContentObserverForUserSync(
-                                Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
-                                false, mContentObserver, newUser);
+                        if (com.android.systemui.Flags.registerContentObserversAsync()) {
+                            mSecureSettings.unregisterContentObserverAsync(mContentObserver);
+                            mSecureSettings.registerContentObserverForUserAsync(
+                                    Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
+                                    false, mContentObserver, newUser);
+                        } else {
+                            mSecureSettings.unregisterContentObserverSync(mContentObserver);
+                            mSecureSettings.registerContentObserverForUserSync(
+                                    Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
+                                    false, mContentObserver, newUser);
+                        }
                     }
                 }
             }
@@ -98,9 +104,15 @@
             if (!mListeners.contains(listener)) {
                 mListeners.add(listener);
                 if (mListeners.size() == 1) {
-                    mSecureSettings.registerContentObserverForUserSync(
-                            Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
-                            false, mContentObserver, mUserTracker.getUserId());
+                    if (com.android.systemui.Flags.registerContentObserversAsync()) {
+                        mSecureSettings.registerContentObserverForUserAsync(
+                                Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
+                                false, mContentObserver, mUserTracker.getUserId());
+                    } else {
+                        mSecureSettings.registerContentObserverForUserSync(
+                                Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
+                                false, mContentObserver, mUserTracker.getUserId());
+                    }
                 }
             }
         }
@@ -110,7 +122,11 @@
     public void removeCallback(@androidx.annotation.NonNull Listener listener) {
         synchronized (mListeners) {
             if (mListeners.remove(listener) && mListeners.size() == 0) {
-                mSecureSettings.unregisterContentObserverSync(mContentObserver);
+                if (com.android.systemui.Flags.registerContentObserversAsync()) {
+                    mSecureSettings.unregisterContentObserverAsync(mContentObserver);
+                } else {
+                    mSecureSettings.unregisterContentObserverSync(mContentObserver);
+                }
             }
         }
     }
@@ -139,7 +155,8 @@
 
     @Override
     public boolean isInUpgradeMode(Resources resources) {
-        return Flags.evenDimmer() && resources.getBoolean(
+        return com.android.server.display.feature.flags.Flags.evenDimmer()
+            && resources.getBoolean(
                 com.android.internal.R.bool.config_evenDimmerEnabled);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
index 295a998..782fb2a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
@@ -48,6 +48,9 @@
     val sourceSpec: MutableState<TileSpec?>,
     private val listState: EditTileListState
 ) {
+    val dragInProgress: Boolean
+        get() = sourceSpec.value != null
+
     /** Returns index of the dragged tile if it's present in the list. Returns -1 if not. */
     fun currentPosition(): Int {
         return sourceSpec.value?.let { listState.indexOf(it) } ?: -1
@@ -65,6 +68,12 @@
         sourceSpec.value?.let { listState.move(it, targetSpec) }
     }
 
+    fun movedOutOfBounds() {
+        // Removing the tiles from the current tile grid if it moves out of bounds. This clears
+        // the spacer and makes it apparent that dropping the tile at that point would remove it.
+        sourceSpec.value?.let { listState.removeFromCurrent(it) }
+    }
+
     fun onDrop() {
         sourceSpec.value = null
     }
@@ -112,6 +121,42 @@
 }
 
 /**
+ * Registers a composable as a [DragAndDropTarget] to receive drop events. Use this outside the tile
+ * grid to catch out of bounds drops.
+ *
+ * @param dragAndDropState The [DragAndDropState] using the tiles list
+ * @param onDrop Action to be executed when a [TileSpec] is dropped on the composable
+ */
+@Composable
+fun Modifier.dragAndDropRemoveZone(
+    dragAndDropState: DragAndDropState,
+    onDrop: (TileSpec) -> Unit,
+): Modifier {
+    val target =
+        remember(dragAndDropState) {
+            object : DragAndDropTarget {
+                override fun onDrop(event: DragAndDropEvent): Boolean {
+                    return dragAndDropState.sourceSpec.value?.let {
+                        onDrop(it)
+                        dragAndDropState.onDrop()
+                        true
+                    } ?: false
+                }
+
+                override fun onEntered(event: DragAndDropEvent) {
+                    dragAndDropState.movedOutOfBounds()
+                }
+            }
+        }
+    return dragAndDropTarget(
+        shouldStartDragAndDrop = { event ->
+            event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE)
+        },
+        target = target,
+    )
+}
+
+/**
  * Registers a tile list as a [DragAndDropTarget] to receive drop events. Use this on list
  * containers to catch drops outside of tiles.
  *
@@ -128,6 +173,10 @@
     val target =
         remember(dragAndDropState) {
             object : DragAndDropTarget {
+                override fun onEnded(event: DragAndDropEvent) {
+                    dragAndDropState.onDrop()
+                }
+
                 override fun onDrop(event: DragAndDropEvent): Boolean {
                     return dragAndDropState.sourceSpec.value?.let {
                         onDrop(it, dragAndDropState.currentPosition())
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
index 482c498..34876c4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
@@ -46,6 +46,18 @@
         tiles.apply { add(toIndex, removeAt(fromIndex).copy(isCurrent = isMovingToCurrent)) }
     }
 
+    /**
+     * Sets the [TileSpec] as a non-current tile. Use this when a tile is dragged out of the current
+     * tile grid.
+     */
+    fun removeFromCurrent(tileSpec: TileSpec) {
+        val fromIndex = indexOf(tileSpec)
+        if (fromIndex >= 0 && fromIndex < tiles.size) {
+            // Mark the moving tile as non-current
+            tiles[fromIndex] = tiles[fromIndex].copy(isCurrent = false)
+        }
+    }
+
     fun indexOf(tileSpec: TileSpec): Int {
         return tiles.indexOfFirst { it.tileSpec == tileSpec }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index ada774d..add830e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -84,7 +84,7 @@
         DefaultEditTileGrid(
             tiles = tiles,
             isIconOnly = isIcon,
-            columns = GridCells.Fixed(columns),
+            columns = columns,
             modifier = modifier,
             onAddTile = onAddTile,
             onRemoveTile = onRemoveTile,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
index 8ca91d8..6c84edd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
@@ -305,9 +305,9 @@
                     largeTiles,
                     ClickAction.ADD,
                     addTileToEnd,
-                    onDoubleTap,
                     isIconOnly,
                     dragAndDropState,
+                    onDoubleTap = onDoubleTap,
                     acceptDrops = { true },
                     onDrop = onDrop,
                 )
@@ -318,10 +318,10 @@
                     smallTiles,
                     ClickAction.ADD,
                     addTileToEnd,
-                    onDoubleTap,
                     isIconOnly,
+                    dragAndDropState,
+                    onDoubleTap = onDoubleTap,
                     showLabels = showLabels,
-                    dragAndDropState = dragAndDropState,
                     acceptDrops = { true },
                     onDrop = onDrop,
                 )
@@ -332,10 +332,10 @@
                     tilesCustom,
                     ClickAction.ADD,
                     addTileToEnd,
-                    onDoubleTap,
                     isIconOnly,
+                    dragAndDropState,
+                    onDoubleTap = onDoubleTap,
                     showLabels = showLabels,
-                    dragAndDropState = dragAndDropState,
                     acceptDrops = { true },
                     onDrop = onDrop,
                 )
@@ -372,11 +372,6 @@
         }
     }
 
-    private fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp {
-        val rows = (nTiles + columns - 1) / columns
-        return ((tileHeight + padding) * rows) - padding
-    }
-
     /** Fill up the rest of the row if it's not complete. */
     private fun LazyGridScope.fillUpRow(nTiles: Int, columns: Int) {
         if (nTiles % columns != 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
index 770d4412..3e48245 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
@@ -100,7 +100,7 @@
         DefaultEditTileGrid(
             tiles = tiles,
             isIconOnly = iconTilesViewModel::isIconTile,
-            columns = GridCells.Fixed(columns),
+            columns = columns,
             modifier = modifier,
             onAddTile = onAddTile,
             onRemoveTile = onRemoveTile,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index 3fdd7f7..bd7956d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -23,13 +23,19 @@
 import android.service.quicksettings.Tile.STATE_INACTIVE
 import android.text.TextUtils
 import androidx.appcompat.content.res.AppCompatResources
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
 import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
 import androidx.compose.animation.graphics.res.animatedVectorResource
 import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
 import androidx.compose.animation.graphics.vector.AnimatedImageVector
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.Image
+import androidx.compose.foundation.LocalOverscrollConfiguration
 import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.border
 import androidx.compose.foundation.combinedClickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement.spacedBy
@@ -37,20 +43,31 @@
 import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridItemSpan
 import androidx.compose.foundation.lazy.grid.LazyGridScope
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -80,6 +97,8 @@
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.common.ui.compose.load
 import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.TileRow
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.toUiState
@@ -269,7 +288,7 @@
 fun DefaultEditTileGrid(
     tiles: List<EditTileViewModel>,
     isIconOnly: (TileSpec) -> Boolean,
-    columns: GridCells,
+    columns: Int,
     modifier: Modifier,
     onAddTile: (TileSpec, Int) -> Unit,
     onRemoveTile: (TileSpec) -> Unit,
@@ -277,84 +296,264 @@
 ) {
     val currentListState = rememberEditListState(tiles)
     val dragAndDropState = rememberDragAndDropState(currentListState)
-
     val (currentTiles, otherTiles) = currentListState.tiles.partition { it.isCurrent }
-    val (otherTilesStock, otherTilesCustom) =
-        otherTiles
-            .filter { !dragAndDropState.isMoving(it.tileSpec) }
-            .partition { it.appName == null }
+
     val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
         onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
     }
-
     val onDropAdd: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, position ->
         onAddTile(tileSpec, position)
     }
-    val onDropRemove: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, _ ->
-        onRemoveTile(tileSpec)
-    }
     val onDoubleTap: (TileSpec) -> Unit by rememberUpdatedState { tileSpec ->
         onResize(tileSpec, !isIconOnly(tileSpec))
     }
+    val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical)
 
-    TileLazyGrid(
-        modifier = modifier.dragAndDropTileList(dragAndDropState, { true }, onDropAdd),
-        columns = columns
+    CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
+        Column(
+            verticalArrangement =
+                spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
+            modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState())
+        ) {
+            AnimatedContent(
+                targetState = dragAndDropState.dragInProgress,
+                modifier = Modifier.wrapContentSize()
+            ) { dragIsInProgress ->
+                EditGridHeader(Modifier.dragAndDropRemoveZone(dragAndDropState, onRemoveTile)) {
+                    if (dragIsInProgress) {
+                        RemoveTileTarget()
+                    } else {
+                        Text(text = "Hold and drag to rearrange tiles.")
+                    }
+                }
+            }
+
+            CurrentTilesGrid(
+                currentTiles,
+                columns,
+                tilePadding,
+                isIconOnly,
+                onRemoveTile,
+                onDoubleTap,
+                dragAndDropState,
+                onDropAdd,
+            )
+
+            // Hide available tiles when dragging
+            AnimatedVisibility(
+                visible = !dragAndDropState.dragInProgress,
+                enter = fadeIn(),
+                exit = fadeOut()
+            ) {
+                Column(
+                    verticalArrangement =
+                        spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
+                    modifier = modifier.fillMaxSize()
+                ) {
+                    EditGridHeader { Text(text = "Hold and drag to add tiles.") }
+
+                    AvailableTileGrid(
+                        otherTiles,
+                        columns,
+                        tilePadding,
+                        addTileToEnd,
+                        dragAndDropState,
+                    )
+                }
+            }
+
+            // Drop zone to remove tiles dragged out of the tile grid
+            Spacer(
+                modifier =
+                    Modifier.fillMaxWidth()
+                        .weight(1f)
+                        .dragAndDropRemoveZone(dragAndDropState, onRemoveTile)
+            )
+        }
+    }
+}
+
+@Composable
+private fun EditGridHeader(
+    modifier: Modifier = Modifier,
+    content: @Composable BoxScope.() -> Unit
+) {
+    CompositionLocalProvider(
+        LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f)
     ) {
-        // These Text are just placeholders to see the different sections. Not final UI.
-        item(span = { GridItemSpan(maxLineSpan) }) { Text("Current tiles", color = Color.White) }
+        Box(
+            contentAlignment = Alignment.Center,
+            modifier = modifier.fillMaxWidth().height(TileDefaults.EditGridHeaderHeight)
+        ) {
+            content()
+        }
+    }
+}
 
-        editTiles(
-            currentTiles,
-            ClickAction.REMOVE,
-            onRemoveTile,
-            onDoubleTap,
-            isIconOnly,
-            indicatePosition = true,
-            dragAndDropState = dragAndDropState,
-            acceptDrops = { true },
-            onDrop = onDropAdd,
-        )
+@Composable
+private fun RemoveTileTarget() {
+    Row(
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = tileHorizontalArrangement(),
+        modifier =
+            Modifier.fillMaxHeight()
+                .border(1.dp, LocalContentColor.current, shape = TileDefaults.TileShape)
+                .padding(10.dp)
+    ) {
+        Icon(imageVector = Icons.Default.Clear, contentDescription = null)
+        Text(text = "Remove")
+    }
+}
 
-        item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) }
+@Composable
+private fun CurrentTilesContainer(content: @Composable () -> Unit) {
+    Box(
+        Modifier.fillMaxWidth()
+            .border(
+                width = 1.dp,
+                color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
+                shape = RoundedCornerShape(48.dp),
+            )
+            .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
+    ) {
+        content()
+    }
+}
 
+@Composable
+private fun CurrentTilesGrid(
+    tiles: List<EditTileViewModel>,
+    columns: Int,
+    tilePadding: Dp,
+    isIconOnly: (TileSpec) -> Boolean,
+    onClick: (TileSpec) -> Unit,
+    onDoubleTap: (TileSpec) -> Unit,
+    dragAndDropState: DragAndDropState,
+    onDrop: (TileSpec, Int) -> Unit
+) {
+    val tileHeight = tileHeight()
+    val currentRows =
+        remember(tiles) {
+            calculateRows(
+                tiles.map {
+                    SizedTile(
+                        it,
+                        if (isIconOnly(it.tileSpec)) {
+                            1
+                        } else {
+                            2
+                        }
+                    )
+                },
+                columns
+            )
+        }
+    val currentGridHeight = gridHeight(currentRows, tileHeight, tilePadding)
+    // Current tiles
+    CurrentTilesContainer {
+        TileLazyGrid(
+            modifier =
+                Modifier.height(currentGridHeight)
+                    .dragAndDropTileList(dragAndDropState, { true }, onDrop),
+            columns = GridCells.Fixed(columns)
+        ) {
+            editTiles(
+                tiles,
+                ClickAction.REMOVE,
+                onClick,
+                isIconOnly,
+                dragAndDropState,
+                onDoubleTap = onDoubleTap,
+                indicatePosition = true,
+                acceptDrops = { true },
+                onDrop = onDrop,
+            )
+        }
+    }
+}
+
+@Composable
+private fun AvailableTileGrid(
+    tiles: List<EditTileViewModel>,
+    columns: Int,
+    tilePadding: Dp,
+    onClick: (TileSpec) -> Unit,
+    dragAndDropState: DragAndDropState,
+) {
+    val (otherTilesStock, otherTilesCustom) =
+        tiles.filter { !dragAndDropState.isMoving(it.tileSpec) }.partition { it.appName == null }
+    val availableTileHeight = tileHeight(true)
+    val availableGridHeight = gridHeight(tiles.size, availableTileHeight, columns, tilePadding)
+
+    // Available tiles
+    TileLazyGrid(
+        modifier =
+            Modifier.height(availableGridHeight)
+                .dragAndDropTileList(dragAndDropState, { false }, { _, _ -> }),
+        columns = GridCells.Fixed(columns)
+    ) {
         editTiles(
             otherTilesStock,
             ClickAction.ADD,
-            addTileToEnd,
-            onDoubleTap,
-            isIconOnly,
+            onClick,
+            isIconOnly = { true },
             dragAndDropState = dragAndDropState,
-            acceptDrops = { true },
-            onDrop = onDropRemove,
+            acceptDrops = { false },
+            showLabels = true,
         )
-
-        item(span = { GridItemSpan(maxLineSpan) }) {
-            Text("Custom tiles to add", color = Color.White)
-        }
-
         editTiles(
             otherTilesCustom,
             ClickAction.ADD,
-            addTileToEnd,
-            onDoubleTap,
-            isIconOnly,
+            onClick,
+            isIconOnly = { true },
             dragAndDropState = dragAndDropState,
-            acceptDrops = { true },
-            onDrop = onDropRemove,
+            acceptDrops = { false },
+            showLabels = true,
         )
     }
 }
 
+fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp {
+    val rows = (nTiles + columns - 1) / columns
+    return gridHeight(rows, tileHeight, padding)
+}
+
+fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp {
+    return ((tileHeight + padding) * rows) - padding
+}
+
+private fun calculateRows(tiles: List<SizedTile<EditTileViewModel>>, columns: Int): Int {
+    val row = TileRow<EditTileViewModel>(columns)
+    var count = 0
+
+    for (tile in tiles) {
+        if (row.maybeAddTile(tile)) {
+            if (row.isFull()) {
+                // Row is full, no need to stretch tiles
+                count += 1
+                row.clear()
+            }
+        } else {
+            count += 1
+            row.clear()
+            row.maybeAddTile(tile)
+        }
+    }
+    if (row.tiles.isNotEmpty()) {
+        count += 1
+    }
+    return count
+}
+
 fun LazyGridScope.editTiles(
     tiles: List<EditTileViewModel>,
     clickAction: ClickAction,
     onClick: (TileSpec) -> Unit,
-    onDoubleTap: (TileSpec) -> Unit,
     isIconOnly: (TileSpec) -> Boolean,
     dragAndDropState: DragAndDropState,
     acceptDrops: (TileSpec) -> Boolean,
-    onDrop: (TileSpec, Int) -> Unit,
+    onDoubleTap: (TileSpec) -> Unit = {},
+    onDrop: (TileSpec, Int) -> Unit = { _, _ -> },
     showLabels: Boolean = false,
     indicatePosition: Boolean = false,
 ) {
@@ -534,6 +733,7 @@
 private object TileDefaults {
     val TileShape = CircleShape
     val IconTileWithLabelHeight = 140.dp
+    val EditGridHeaderHeight = 60.dp
 
     @Composable
     fun activeTileColors(): TileColors =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
index 62bfc72..ef2c8bf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
@@ -151,12 +151,27 @@
      * present, it will be moved to the new position.
      */
     fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) {
-        // Removing tile if it's already present to insert it at the new index.
-        if (currentTilesInteractor.currentTilesSpecs.contains(tileSpec)) {
-            removeTile(tileSpec)
+        val specs = currentTilesInteractor.currentTilesSpecs.toMutableList()
+        val currentPosition = specs.indexOf(tileSpec)
+
+        if (currentPosition != -1) {
+            // No operation needed if the element is already in the list at the right position
+            if (currentPosition == position) {
+                return
+            }
+            // Removing tile if it's present at a different position to insert it at the new index.
+            specs.removeAt(currentPosition)
         }
 
-        currentTilesInteractor.addTile(tileSpec, position)
+        if (position >= 0 && position < specs.size) {
+            specs.add(position, tileSpec)
+        } else {
+            specs.add(tileSpec)
+        }
+
+        // Setting the new tiles as one operation to avoid UI jank with tiles disappearing and
+        // reappearing
+        currentTilesInteractor.setTiles(specs)
     }
 
     /** Immediately removes [tileSpec] from the current tiles. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
new file mode 100644
index 0000000..b91891c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.qs.tiles
+
+import android.app.Flags
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.coroutineScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.logging.MetricsLogger
+import com.android.systemui.animation.Expandable
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.QSTile.BooleanState
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileDataInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.launch
+
+class ModesTile
+@Inject
+constructor(
+    host: QSHost,
+    uiEventLogger: QsEventLogger,
+    @Background backgroundLooper: Looper,
+    @Main mainHandler: Handler,
+    falsingManager: FalsingManager,
+    metricsLogger: MetricsLogger,
+    statusBarStateController: StatusBarStateController,
+    activityStarter: ActivityStarter,
+    qsLogger: QSLogger,
+    qsTileConfigProvider: QSTileConfigProvider,
+    dataInteractor: ModesTileDataInteractor,
+    private val tileMapper: ModesTileMapper,
+    private val userActionInteractor: ModesTileUserActionInteractor,
+) :
+    QSTileImpl<BooleanState>(
+        host,
+        uiEventLogger,
+        backgroundLooper,
+        mainHandler,
+        falsingManager,
+        metricsLogger,
+        statusBarStateController,
+        activityStarter,
+        qsLogger
+    ) {
+
+    private lateinit var tileState: QSTileState
+    private val config = qsTileConfigProvider.getConfig(TILE_SPEC)
+
+    init {
+        lifecycle.coroutineScope.launch {
+            lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+                dataInteractor.tileData().collect { refreshState(it) }
+            }
+        }
+    }
+
+    override fun isAvailable(): Boolean = Flags.modesUi()
+
+    override fun getTileLabel(): CharSequence = tileState.label
+
+    override fun newTileState() = BooleanState()
+
+    override fun handleClick(expandable: Expandable?) {
+        // TODO(b/346519570) open dialog
+    }
+
+    override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent
+
+    override fun handleUpdateState(booleanState: BooleanState?, arg: Any?) {
+        if (arg is ModesTileModel) {
+            tileState = tileMapper.map(config, arg)
+
+            booleanState?.apply {
+                state = tileState.activationState.legacyState
+                icon = ResourceIcon.get(tileState.iconRes ?: R.drawable.qs_dnd_icon_off)
+                label = tileLabel
+                secondaryLabel = tileState.secondaryLabel
+                contentDescription = tileState.contentDescription
+            }
+        }
+    }
+
+    companion object {
+        const val TILE_SPEC = "modes"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index df7430a..158eb6e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -65,6 +65,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
@@ -184,7 +185,7 @@
     private GlobalSettings mGlobalSettings;
     private int mDefaultDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     private ConnectivityManager.NetworkCallback mConnectivityManagerNetworkCallback;
-    private WindowManager mWindowManager;
+    private ViewCaptureAwareWindowManager mWindowManager;
     private ToastFactory mToastFactory;
     private SignalDrawable mSignalDrawable;
     private SignalDrawable mSecondarySignalDrawable; // For the secondary mobile data sub in DSDS
@@ -246,7 +247,7 @@
             @Main Handler handler, @Main Executor mainExecutor,
             BroadcastDispatcher broadcastDispatcher, KeyguardUpdateMonitor keyguardUpdateMonitor,
             GlobalSettings globalSettings, KeyguardStateController keyguardStateController,
-            WindowManager windowManager, ToastFactory toastFactory,
+            ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, ToastFactory toastFactory,
             @Background Handler workerHandler,
             CarrierConfigTracker carrierConfigTracker,
             LocationController locationController,
@@ -278,7 +279,7 @@
         mAccessPointController = accessPointController;
         mWifiIconInjector = new WifiUtils.InternetIconInjector(mContext);
         mConnectivityManagerNetworkCallback = new DataConnectivityListener();
-        mWindowManager = windowManager;
+        mWindowManager = viewCaptureAwareWindowManager;
         mToastFactory = toastFactory;
         mSignalDrawable = new SignalDrawable(mContext);
         mSecondarySignalDrawable = new SignalDrawable(mContext);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
index 930109a..31e91aa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
@@ -38,9 +38,14 @@
     override fun tileData(
         user: UserHandle,
         triggers: Flow<DataUpdateTrigger>
-    ): Flow<ModesTileModel> {
-        return zenModeActive.map { ModesTileModel(isActivated = it) }
-    }
+    ): Flow<ModesTileModel> = tileData()
+
+    /**
+     * An adapted version of the base class' [tileData] method for use in an old-style tile.
+     *
+     * TODO(b/299909989): Remove after the transition.
+     */
+    fun tileData() = zenModeActive.map { ModesTileModel(isActivated = it) }
 
     override fun availability(user: UserHandle): Flow<Boolean> = flowOf(Flags.modesUi())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
index e2fea84..fd1f3d8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
@@ -16,14 +16,22 @@
 
 package com.android.systemui.qs.tiles.impl.modes.domain.interactor
 
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
 import com.android.systemui.qs.tiles.base.interactor.QSTileInput
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
 import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
 import javax.inject.Inject
 
-class ModesTileUserActionInteractor @Inject constructor() :
-    QSTileUserActionInteractor<ModesTileModel> {
+class ModesTileUserActionInteractor
+@Inject
+constructor(
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+) : QSTileUserActionInteractor<ModesTileModel> {
+    val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
+
     override suspend fun handleInput(input: QSTileInput<ModesTileModel>) {
         with(input) {
             when (action) {
@@ -31,7 +39,7 @@
                     // TODO(b/346519570) open dialog
                 }
                 is QSTileUserAction.LongClick -> {
-                    // TODO(b/346519570) open settings
+                    qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent)
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
index 07b393e..26b9a4c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
@@ -34,13 +34,17 @@
 ) : QSTileDataToStateMapper<ModesTileModel> {
     override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState =
         QSTileState.build(resources, theme, config.uiConfig) {
-            val iconRes =
+            iconRes =
                 if (data.isActivated) {
                     R.drawable.qs_dnd_icon_on
                 } else {
                     R.drawable.qs_dnd_icon_off
                 }
-            val icon = Icon.Loaded(resources.getDrawable(iconRes, theme), contentDescription = null)
+            val icon =
+                Icon.Loaded(
+                    resources.getDrawable(iconRes!!, theme),
+                    contentDescription = null,
+                )
             this.icon = { icon }
             if (data.isActivated) {
                 activationState = QSTileState.ActivationState.ACTIVE
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index b3624ad..371707d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -72,6 +72,7 @@
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodManager;
 
 import androidx.annotation.NonNull;
@@ -302,10 +303,29 @@
         public void onImeSwitcherPressed() {
             // TODO(b/204901476) We're intentionally using the default display for now since
             // Launcher/Taskbar isn't display aware.
+            if (Flags.imeSwitcherRevamp()) {
+                mContext.getSystemService(InputMethodManager.class)
+                        .onImeSwitchButtonClickFromSystem(mDisplayTracker.getDefaultDisplayId());
+            } else {
+                mContext.getSystemService(InputMethodManager.class)
+                        .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */,
+                                mDisplayTracker.getDefaultDisplayId());
+            }
+            mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
+        }
+
+        @Override
+        public void onImeSwitcherLongPress() {
+            if (!Flags.imeSwitcherRevamp()) {
+                return;
+            }
+            // TODO(b/204901476) We're intentionally using the default display for now since
+            // Launcher/Taskbar isn't display aware.
             mContext.getSystemService(InputMethodManager.class)
                     .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */,
                             mDisplayTracker.getDefaultDisplayId());
-            mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
+            mUiEventLogger.log(
+                    KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 1f4820a..8711e88 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -665,7 +665,7 @@
             keyguardEnabledInteractor.isKeyguardEnabled
                 .sample(
                     combine(
-                        deviceEntryInteractor.isInLockdown,
+                        deviceUnlockedInteractor.isInLockdown,
                         deviceEntryInteractor.isDeviceEntered,
                         ::Pair,
                     )
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index af1b6e1..46c5861 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -27,7 +27,6 @@
 import android.os.CountDownTimer;
 import android.os.Process;
 import android.os.UserHandle;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -60,8 +59,6 @@
 @SysUISingleton
 public class RecordingController
         implements CallbackController<RecordingController.RecordingStateChangeCallback> {
-    private static final String TAG = "RecordingController";
-
     private boolean mIsStarting;
     private boolean mIsRecording;
     private PendingIntent mStopIntent;
@@ -71,6 +68,7 @@
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final FeatureFlags mFlags;
     private final UserTracker mUserTracker;
+    private final RecordingControllerLogger mRecordingControllerLogger;
     private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
     private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate;
     private final ScreenRecordDialogDelegate.Factory mScreenRecordDialogFactory;
@@ -102,9 +100,10 @@
             if (intent != null && INTENT_UPDATE_STATE.equals(intent.getAction())) {
                 if (intent.hasExtra(EXTRA_STATE)) {
                     boolean state = intent.getBooleanExtra(EXTRA_STATE, false);
+                    mRecordingControllerLogger.logIntentStateUpdated(state);
                     updateState(state);
                 } else {
-                    Log.e(TAG, "Received update intent with no state");
+                    mRecordingControllerLogger.logIntentMissingState();
                 }
             }
         }
@@ -120,6 +119,7 @@
             FeatureFlags flags,
             Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver,
             UserTracker userTracker,
+            RecordingControllerLogger recordingControllerLogger,
             MediaProjectionMetricsLogger mediaProjectionMetricsLogger,
             ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate,
             ScreenRecordDialogDelegate.Factory screenRecordDialogFactory,
@@ -130,6 +130,7 @@
         mDevicePolicyResolver = devicePolicyResolver;
         mBroadcastDispatcher = broadcastDispatcher;
         mUserTracker = userTracker;
+        mRecordingControllerLogger = recordingControllerLogger;
         mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger;
         mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate;
         mScreenRecordDialogFactory = screenRecordDialogFactory;
@@ -212,9 +213,9 @@
                     IntentFilter stateFilter = new IntentFilter(INTENT_UPDATE_STATE);
                     mBroadcastDispatcher.registerReceiver(mStateChangeReceiver, stateFilter, null,
                             UserHandle.ALL);
-                    Log.d(TAG, "sent start intent");
+                    mRecordingControllerLogger.logSentStartIntent();
                 } catch (PendingIntent.CanceledException e) {
-                    Log.e(TAG, "Pending intent was cancelled: " + e.getMessage());
+                    mRecordingControllerLogger.logPendingIntentCancelled(e);
                 }
             }
         };
@@ -227,9 +228,10 @@
      */
     public void cancelCountdown() {
         if (mCountDownTimer != null) {
+            mRecordingControllerLogger.logCountdownCancelled();
             mCountDownTimer.cancel();
         } else {
-            Log.e(TAG, "Timer was null");
+            mRecordingControllerLogger.logCountdownCancelErrorNoTimer();
         }
         mIsStarting = false;
 
@@ -258,16 +260,16 @@
      * Stop the recording
      */
     public void stopRecording() {
-        // TODO(b/332662551): Convert Logcat to LogBuffer.
         try {
             if (mStopIntent != null) {
+                mRecordingControllerLogger.logRecordingStopped();
                 mStopIntent.send(mInteractiveBroadcastOption);
             } else {
-                Log.e(TAG, "Stop intent was null");
+                mRecordingControllerLogger.logRecordingStopErrorNoStopIntent();
             }
             updateState(false);
         } catch (PendingIntent.CanceledException e) {
-            Log.e(TAG, "Error stopping: " + e.getMessage());
+            mRecordingControllerLogger.logRecordingStopError(e);
         }
     }
 
@@ -276,6 +278,7 @@
      * @param isRecording
      */
     public synchronized void updateState(boolean isRecording) {
+        mRecordingControllerLogger.logStateUpdated(isRecording);
         if (!isRecording && mIsRecording) {
             // Unregister receivers if we have stopped recording
             mUserTracker.removeCallback(mUserChangedCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLog.kt
new file mode 100644
index 0000000..dd2baef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLog.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.screenrecord
+
+import javax.inject.Qualifier
+
+/**
+ * Logs for screen record events. See [com.android.systemui.screenrecord.RecordingController] and
+ * [com.android.systemui.screenrecord.RecordingControllerLogger].
+ */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class RecordingControllerLog
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLogger.kt
new file mode 100644
index 0000000..e16c010
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLogger.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.screenrecord
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import javax.inject.Inject
+
+/** Helper class for logging events to [RecordingControllerLog] from Java. */
+@SysUISingleton
+class RecordingControllerLogger
+@Inject
+constructor(
+    @RecordingControllerLog private val logger: LogBuffer,
+) {
+    fun logStateUpdated(isRecording: Boolean) =
+        logger.log(
+            TAG,
+            LogLevel.DEBUG,
+            { bool1 = isRecording },
+            { "Updating state. isRecording=$bool1" },
+        )
+
+    fun logIntentStateUpdated(isRecording: Boolean) =
+        logger.log(
+            TAG,
+            LogLevel.DEBUG,
+            { bool1 = isRecording },
+            { "Update intent has state. isRecording=$bool1" },
+        )
+
+    fun logIntentMissingState() =
+        logger.log(TAG, LogLevel.ERROR, {}, { "Received update intent with no state" })
+
+    fun logSentStartIntent() = logger.log(TAG, LogLevel.DEBUG, {}, { "Sent start intent" })
+
+    fun logPendingIntentCancelled(e: Exception) =
+        logger.log(TAG, LogLevel.ERROR, {}, { "Pending intent was cancelled" }, e)
+
+    fun logCountdownCancelled() =
+        logger.log(TAG, LogLevel.DEBUG, {}, { "Record countdown cancelled" })
+
+    fun logCountdownCancelErrorNoTimer() =
+        logger.log(TAG, LogLevel.ERROR, {}, { "Couldn't cancel countdown because timer was null" })
+
+    fun logRecordingStopped() = logger.log(TAG, LogLevel.DEBUG, {}, { "Stopping recording" })
+
+    fun logRecordingStopErrorNoStopIntent() =
+        logger.log(
+            TAG,
+            LogLevel.ERROR,
+            {},
+            { "Couldn't stop recording because stop intent was null" },
+        )
+
+    fun logRecordingStopError(e: Exception) =
+        logger.log(TAG, LogLevel.DEBUG, {}, { "Couldn't stop recording" }, e)
+
+    companion object {
+        private const val TAG = "RecordingController"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
index d7ddc50..a830e1b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
@@ -16,6 +16,9 @@
 
 package com.android.systemui.screenrecord
 
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tileimpl.QSTileImpl
@@ -53,7 +56,7 @@
     @IntoMap
     @StringKey(SCREEN_RECORD_TILE_SPEC)
     fun provideScreenRecordAvailabilityInteractor(
-            impl: ScreenRecordTileDataInteractor
+        impl: ScreenRecordTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     companion object {
@@ -89,5 +92,12 @@
                 stateInteractor,
                 mapper,
             )
+
+        @Provides
+        @SysUISingleton
+        @RecordingControllerLog
+        fun provideRecordingControllerLogBuffer(factory: LogBufferFactory): LogBuffer {
+            return factory.create("RecordingControllerLog", 50)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
index 49810762..8e53949 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
@@ -137,14 +137,24 @@
         public void startObserving() {
             if (!mObserving) {
                 mObserving = true;
-                mSecureSettings.registerContentObserverForUserSync(
-                        BRIGHTNESS_MODE_URI,
-                        false, this, UserHandle.USER_ALL);
+                if (Flags.registerContentObserversAsync()) {
+                    mSecureSettings.registerContentObserverForUserAsync(
+                            BRIGHTNESS_MODE_URI,
+                            false, this, UserHandle.USER_ALL);
+                } else {
+                    mSecureSettings.registerContentObserverForUserSync(
+                            BRIGHTNESS_MODE_URI,
+                            false, this, UserHandle.USER_ALL);
+                }
             }
         }
 
         public void stopObserving() {
-            mSecureSettings.unregisterContentObserverSync(this);
+            if (Flags.registerContentObserversAsync()) {
+                mSecureSettings.unregisterContentObserverAsync(this);
+            } else {
+                mSecureSettings.unregisterContentObserverSync(this);
+            }
             mObserving = false;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
index abf258c..693cc4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -485,9 +485,8 @@
             final boolean intersectsTopCutout = topDisplayCutout.intersects(
                     width - (windowWidth / 2), 0,
                     width + (windowWidth / 2), topDisplayCutout.bottom);
-            if (mClingWindow != null &&
-                    (windowWidth < 0 || (width > 0 && intersectsTopCutout))) {
-                final View iconView = mClingWindow.findViewById(R.id.immersive_cling_icon);
+            if (windowWidth < 0 || (width > 0 && intersectsTopCutout)) {
+                final View iconView = findViewById(R.id.immersive_cling_icon);
                 RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)
                         iconView.getLayoutParams();
                 lp.topMargin = topDisplayCutout.bottom;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 1789ad6..2081adc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -3519,7 +3519,7 @@
     // Only when scene container is enabled, mark that we are being dragged so that we start
     // dispatching the rest of the gesture to scene container.
     void startOverscrollAfterExpanding() {
-        SceneContainerFlag.isUnexpectedlyInLegacyMode();
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
         getExpandHelper().finishExpanding();
         setIsBeingDragged(true);
     }
@@ -3527,7 +3527,7 @@
     // Only when scene container is enabled, mark that we are being dragged so that we start
     // dispatching the rest of the gesture to scene container.
     void startDraggingOnHun() {
-        SceneContainerFlag.isUnexpectedlyInLegacyMode();
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
         setIsBeingDragged(true);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 726fdee..a072ea6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -2034,6 +2034,7 @@
                 hunWantsIt = mHeadsUpTouchHelper.onInterceptTouchEvent(ev);
                 if (hunWantsIt) {
                     mView.startDraggingOnHun();
+                    mHeadsUpManager.unpinAll(true);
                 }
             }
             boolean swipeWantsIt = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index eb1f778..1a7bc16 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -110,6 +110,11 @@
         interactor.setScrolledToTop(scrolledToTop)
     }
 
+    /** Sets whether the heads up notification is animating away. */
+    fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
+        headsUpNotificationInteractor.setHeadsUpAnimatingAway(animatingAway)
+    }
+
     /** Snooze the currently pinned HUN. */
     fun snoozeHun() {
         headsUpNotificationInteractor.snooze()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index d1d5d30..99f7a75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -187,7 +187,6 @@
         interactor.configurationBasedDimensions
             .map {
                 when {
-                    !it.useSplitShade -> 0
                     it.useLargeScreenHeader -> it.marginTopLargeScreen
                     else -> it.marginTop
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 6d76200..6f29f61 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -369,10 +369,12 @@
 
     private void releaseBiometricWakeLock() {
         if (mWakeLock != null) {
+            Trace.beginSection("release wake-and-unlock");
             mHandler.removeCallbacks(mReleaseBiometricWakeLockRunnable);
             mLogger.i("releasing biometric wakelock");
             mWakeLock.release();
             mWakeLock = null;
+            Trace.endSection();
         }
     }
 
@@ -398,7 +400,7 @@
             }
             mWakeLock = mPowerManager.newWakeLock(
                     PowerManager.PARTIAL_WAKE_LOCK, BIOMETRIC_WAKE_LOCK_NAME);
-            Trace.beginSection("acquiring wake-and-unlock");
+            Trace.beginSection("acquire wake-and-unlock");
             mWakeLock.acquire();
             Trace.endSection();
             mLogger.i("biometric acquired, grabbing biometric wakelock");
@@ -412,14 +414,13 @@
     public void onBiometricDetected(int userId, BiometricSourceType biometricSourceType,
             boolean isStrongBiometric) {
         Trace.beginSection("BiometricUnlockController#onBiometricDetected");
-        if (mUpdateMonitor.isGoingToSleep()) {
-            Trace.endSection();
-            return;
+        if (!mUpdateMonitor.isGoingToSleep()) {
+            startWakeAndUnlock(
+                    MODE_SHOW_BOUNCER,
+                    BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType)
+            );
         }
-        startWakeAndUnlock(
-                MODE_SHOW_BOUNCER,
-                BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType)
-        );
+        Trace.endSection();
     }
 
     @Override
@@ -451,6 +452,7 @@
         } else {
             mLogger.d("onBiometricUnlocked aborted by bypass controller");
         }
+        Trace.endSection();
     }
 
     /**
@@ -479,6 +481,7 @@
             @WakeAndUnlockMode int mode,
             BiometricUnlockSource biometricUnlockSource
     ) {
+        Trace.beginSection("BiometricUnlockController#startWakeAndUnlock");
         mLogger.logStartWakeAndUnlock(mode);
         boolean wasDeviceInteractive = mUpdateMonitor.isDeviceInteractive();
         mMode = mode;
@@ -501,9 +504,7 @@
                         "android.policy:BIOMETRIC"
                 );
             }
-            Trace.beginSection("release wake-and-unlock");
             releaseBiometricWakeLock();
-            Trace.endSection();
         };
 
         final boolean wakeInKeyguard = mMode == MODE_WAKE_AND_UNLOCK_FROM_DREAM
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
index 7f16e18..26bd7ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
@@ -124,7 +124,6 @@
                     mPanel.setHeadsUpDraggingStartingHeight(startHeight);
                     mPanel.startExpand(x, y, true /* startTracking */, startHeight);
 
-                    // TODO(b/340514839): Figure out where to move this side effect in flexiglass
                     if (!SceneContainerFlag.isEnabled()) {
                         // This call needs to be after the expansion start otherwise we will get a
                         // flicker of one frame as it's not expanded yet.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
index bcb613f..de76b10d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
@@ -276,10 +276,11 @@
                 statusBarController
             }
 
+        val isCommunalDismissLaunch = isCommunalWidgetLaunch() && !actuallyShowOverLockscreen
         // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we
         // run the animation on the keyguard). The animation will take care of (instantly)
         // collapsing the shade and hiding the keyguard once it is done.
-        val collapse = dismissShade && !animate
+        val collapse = (dismissShade || isCommunalDismissLaunch) && !animate
         val runnable = Runnable {
             try {
                 activityTransitionAnimator.startPendingIntentWithAnimation(
@@ -338,8 +339,9 @@
             postOnUiThread(delay = 0) {
                 executeRunnableDismissingKeyguard(
                     runnable = runnable,
-                    afterKeyguardGone = willLaunchResolverActivity,
                     dismissShade = collapse,
+                    afterKeyguardGone = willLaunchResolverActivity,
+                    deferred = isCommunalDismissLaunch,
                     willAnimateOnKeyguard = animate,
                     customMessage = customMessage,
                 )
@@ -461,7 +463,9 @@
                 override fun onDismiss(): Boolean {
                     if (runnable != null) {
                         if (
-                            keyguardStateController.isShowing && keyguardStateController.isOccluded
+                            keyguardStateController.isShowing &&
+                                keyguardStateController.isOccluded &&
+                                !isCommunalWidgetLaunch()
                         ) {
                             statusBarKeyguardViewManagerLazy
                                 .get()
@@ -473,17 +477,10 @@
                     if (dismissShade) {
                         shadeControllerLazy.get().collapseShadeForActivityStart()
                     }
-                    if (communalHub()) {
-                        communalSceneInteractor.changeSceneForActivityStartOnDismissKeyguard()
-                    }
                     return deferred
                 }
 
                 override fun willRunAnimationOnKeyguard(): Boolean {
-                    if (communalHub() && communalSceneInteractor.isIdleOnCommunal.value) {
-                        // Override to false when launching activity over the hub that requires auth
-                        return false
-                    }
                     return willAnimateOnKeyguard
                 }
             }
@@ -639,7 +636,8 @@
         showOverLockscreen: Boolean,
     ): Boolean {
         // TODO(b/294418322): always support launch animations when occluded.
-        val ignoreOcclusion = showOverLockscreen && mediaLockscreenLaunchAnimation()
+        val ignoreOcclusion =
+            (showOverLockscreen && mediaLockscreenLaunchAnimation()) || isCommunalWidgetLaunch()
         if (keyguardStateController.isOccluded && !ignoreOcclusion) {
             return false
         }
@@ -659,6 +657,12 @@
         return shouldAnimateLaunch(isActivityIntent, false)
     }
 
+    private fun isCommunalWidgetLaunch(): Boolean {
+        return communalHub() &&
+            communalSceneInteractor.isCommunalVisible.value &&
+            communalSceneInteractor.isLaunchingWidget.value
+    }
+
     private fun postOnUiThread(delay: Int = 0, runnable: Runnable) {
         mainExecutor.executeDelayed(runnable, delay.toLong())
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index 43ab337..40799583 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -91,7 +91,11 @@
     }
 
     /** Run or delay Runnable for given HeadsUpEntry */
-    fun update(entry: HeadsUpEntry?, runnable: Runnable, label: String) {
+    fun update(entry: HeadsUpEntry?, runnable: Runnable?, label: String) {
+        if (runnable == null) {
+            log { "Runnable is NULL, stop update." }
+            return
+        }
         if (!NotificationThrottleHun.isEnabled) {
             runnable.run()
             return
@@ -147,7 +151,11 @@
      * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete
      * all Runnables associated with that entry.
      */
-    fun delete(entry: HeadsUpEntry?, runnable: Runnable, label: String) {
+    fun delete(entry: HeadsUpEntry?, runnable: Runnable?, label: String) {
+        if (runnable == null) {
+            log { "Runnable is NULL, stop delete." }
+            return
+        }
         if (!NotificationThrottleHun.isEnabled) {
             runnable.run()
             return
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
index 994a0d0..7b82b56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
@@ -26,7 +26,6 @@
 import android.media.projection.MediaProjectionManager;
 import android.os.Handler;
 import android.util.ArrayMap;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
@@ -46,11 +45,11 @@
 /** Platform implementation of the cast controller. **/
 @SysUISingleton
 public class CastControllerImpl implements CastController {
-    public static final String TAG = "CastController";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final String TAG = "CastController";
 
     private final Context mContext;
     private final PackageManager mPackageManager;
+    private final CastControllerLogger mLogger;
     @GuardedBy("mCallbacks")
     private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
     private final MediaRouter mMediaRouter;
@@ -67,20 +66,22 @@
     public CastControllerImpl(
             Context context,
             PackageManager packageManager,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            CastControllerLogger logger) {
         mContext = context;
         mPackageManager = packageManager;
+        mLogger = logger;
         mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
         mMediaRouter.setRouterGroupId(MediaRouter.MIRRORING_GROUP_ID);
         mProjectionManager = (MediaProjectionManager)
                 context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
         mProjection = mProjectionManager.getActiveProjectionInfo();
         mProjectionManager.addCallback(mProjectionCallback, new Handler());
-        dumpManager.registerDumpable(TAG, this);
-        if (DEBUG) Log.d(TAG, "new CastController()");
+        dumpManager.registerNormalDumpable(TAG, this);
     }
 
-    public void dump(PrintWriter pw, String[] args) {
+    @Override
+    public void dump(PrintWriter pw, @NonNull String[] args) {
         pw.println("CastController state:");
         pw.print("  mDiscovering="); pw.println(mDiscovering);
         pw.print("  mCallbackRegistered="); pw.println(mCallbackRegistered);
@@ -88,7 +89,7 @@
         pw.print("  mRoutes.size="); pw.println(mRoutes.size());
         for (int i = 0; i < mRoutes.size(); i++) {
             final RouteInfo route = mRoutes.valueAt(i);
-            pw.print("    "); pw.println(routeToString(route));
+            pw.print("    "); pw.println(CastControllerLogger.Companion.toLogString(route));
         }
         pw.print("  mProjection="); pw.println(mProjection);
     }
@@ -119,7 +120,7 @@
         synchronized (mDiscoveringLock) {
             if (mDiscovering == request) return;
             mDiscovering = request;
-            if (DEBUG) Log.d(TAG, "setDiscovering: " + request);
+            mLogger.logDiscovering(request);
             handleDiscoveryChangeLocked();
         }
     }
@@ -166,7 +167,8 @@
                         CastDevice.Companion.toCastDevice(
                                 mProjection,
                                 mContext,
-                                mPackageManager));
+                                mPackageManager,
+                                mLogger));
             }
         }
 
@@ -177,7 +179,7 @@
     public void startCasting(CastDevice device) {
         if (device == null || device.getTag() == null) return;
         final RouteInfo route = (RouteInfo) device.getTag();
-        if (DEBUG) Log.d(TAG, "startCasting: " + routeToString(route));
+        mLogger.logStartCasting(route);
         mMediaRouter.selectRoute(ROUTE_TYPE_REMOTE_DISPLAY, route);
     }
 
@@ -185,15 +187,16 @@
     public void stopCasting(CastDevice device) {
         // TODO(b/332662551): Convert Logcat to LogBuffer.
         final boolean isProjection = device.getTag() instanceof MediaProjectionInfo;
-        if (DEBUG) Log.d(TAG, "stopCasting isProjection=" + isProjection);
+        mLogger.logStopCasting(isProjection);
         if (isProjection) {
             final MediaProjectionInfo projection = (MediaProjectionInfo) device.getTag();
             if (Objects.equals(mProjectionManager.getActiveProjectionInfo(), projection)) {
                 mProjectionManager.stopActiveProjection();
             } else {
-                Log.w(TAG, "Projection is no longer active: " + projection);
+                mLogger.logStopCastingNoProjection(projection);
             }
         } else {
+            mLogger.logStopCastingMediaRouter();
             mMediaRouter.getFallbackRoute().select();
         }
     }
@@ -218,7 +221,7 @@
             }
         }
         if (changed) {
-            if (DEBUG) Log.d(TAG, "setProjection: " + oldProjection + " -> " + mProjection);
+            mLogger.logSetProjection(oldProjection, mProjection);
             fireOnCastDevicesChanged();
         }
     }
@@ -265,42 +268,30 @@
         callback.onCastDevicesChanged();
     }
 
-    private static String routeToString(RouteInfo route) {
-        if (route == null) return null;
-        final StringBuilder sb = new StringBuilder().append(route.getName()).append('/')
-                .append(route.getDescription()).append('@').append(route.getDeviceAddress())
-                .append(",status=").append(route.getStatus());
-        if (route.isDefault()) sb.append(",default");
-        if (route.isEnabled()) sb.append(",enabled");
-        if (route.isConnecting()) sb.append(",connecting");
-        if (route.isSelected()) sb.append(",selected");
-        return sb.append(",id=").append(route.getTag()).toString();
-    }
-
     private final MediaRouter.SimpleCallback mMediaCallback = new MediaRouter.SimpleCallback() {
         @Override
         public void onRouteAdded(MediaRouter router, RouteInfo route) {
-            if (DEBUG) Log.d(TAG, "onRouteAdded: " + routeToString(route));
+            mLogger.logRouteAdded(route);
             updateRemoteDisplays();
         }
         @Override
         public void onRouteChanged(MediaRouter router, RouteInfo route) {
-            if (DEBUG) Log.d(TAG, "onRouteChanged: " + routeToString(route));
+            mLogger.logRouteChanged(route);
             updateRemoteDisplays();
         }
         @Override
         public void onRouteRemoved(MediaRouter router, RouteInfo route) {
-            if (DEBUG) Log.d(TAG, "onRouteRemoved: " + routeToString(route));
+            mLogger.logRouteRemoved(route);
             updateRemoteDisplays();
         }
         @Override
         public void onRouteSelected(MediaRouter router, int type, RouteInfo route) {
-            if (DEBUG) Log.d(TAG, "onRouteSelected(" + type + "): " + routeToString(route));
+            mLogger.logRouteSelected(route, type);
             updateRemoteDisplays();
         }
         @Override
         public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
-            if (DEBUG) Log.d(TAG, "onRouteUnselected(" + type + "): " + routeToString(route));
+            mLogger.logRouteUnselected(route, type);
             updateRemoteDisplays();
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerLogger.kt
new file mode 100644
index 0000000..9a3a244
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerLogger.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.policy
+
+import android.media.MediaRouter.RouteInfo
+import android.media.projection.MediaProjectionInfo
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.MessageInitializer
+import com.android.systemui.log.core.MessagePrinter
+import com.android.systemui.statusbar.policy.dagger.CastControllerLog
+import javax.inject.Inject
+
+/** Helper class for logging events to [CastControllerLog] from Java. */
+@SysUISingleton
+class CastControllerLogger
+@Inject
+constructor(
+    @CastControllerLog val logger: LogBuffer,
+) {
+    /** Passthrough to [logger]. */
+    inline fun log(
+        tag: String,
+        level: LogLevel,
+        messageInitializer: MessageInitializer,
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+    ) {
+        logger.log(tag, level, messageInitializer, messagePrinter, exception)
+    }
+
+    fun logDiscovering(isDiscovering: Boolean) =
+        logger.log(TAG, LogLevel.DEBUG, { bool1 = isDiscovering }, { "setDiscovering: $bool1" })
+
+    fun logStartCasting(route: RouteInfo) =
+        logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "startCasting: $str1" })
+
+    fun logStopCasting(isProjection: Boolean) =
+        logger.log(
+            TAG,
+            LogLevel.DEBUG,
+            { bool1 = isProjection },
+            { "stopCasting. isProjection=$bool1" },
+        )
+
+    fun logStopCastingNoProjection(projection: MediaProjectionInfo) =
+        logger.log(
+            TAG,
+            LogLevel.WARNING,
+            { str1 = projection.toString() },
+            { "stopCasting failed because projection is no longer active: $str1" },
+        )
+
+    fun logStopCastingMediaRouter() =
+        logger.log(
+            TAG,
+            LogLevel.DEBUG,
+            {},
+            { "stopCasting is selecting fallback route in MediaRouter" },
+        )
+
+    fun logSetProjection(oldInfo: MediaProjectionInfo?, newInfo: MediaProjectionInfo?) =
+        logger.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = oldInfo.toString()
+                str2 = newInfo.toString()
+            },
+            { "setProjection: $str1 -> $str2" },
+        )
+
+    fun logRouteAdded(route: RouteInfo) =
+        logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "onRouteAdded: $str1" })
+
+    fun logRouteChanged(route: RouteInfo) =
+        logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "onRouteChanged: $str1" })
+
+    fun logRouteRemoved(route: RouteInfo) =
+        logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "onRouteRemoved: $str1" })
+
+    fun logRouteSelected(route: RouteInfo, type: Int) =
+        logger.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = route.toLogString()
+                int1 = type
+            },
+            { "onRouteSelected($int1): $str1" },
+        )
+
+    fun logRouteUnselected(route: RouteInfo, type: Int) =
+        logger.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = route.toLogString()
+                int1 = type
+            },
+            { "onRouteUnselected($int1): $str1" },
+        )
+
+    companion object {
+        @JvmStatic
+        fun RouteInfo?.toLogString(): String? {
+            if (this == null) return null
+            val sb =
+                StringBuilder()
+                    .append(name)
+                    .append('/')
+                    .append(description)
+                    .append('@')
+                    .append(deviceAddress)
+                    .append(",status=")
+                    .append(status)
+            if (isDefault) sb.append(",default")
+            if (isEnabled) sb.append(",enabled")
+            if (isConnecting) sb.append(",connecting")
+            if (isSelected) sb.append(",selected")
+            return sb.append(",id=").append(this.tag).toString()
+        }
+
+        private const val TAG = "CastController"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt
index 68edd75..a787f7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt
@@ -20,7 +20,7 @@
 import android.media.MediaRouter
 import android.media.projection.MediaProjectionInfo
 import android.text.TextUtils
-import android.util.Log
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.res.R
 import com.android.systemui.util.Utils
 
@@ -64,11 +64,12 @@
         /** Creates a [CastDevice] based on the provided information from MediaProjection. */
         fun MediaProjectionInfo.toCastDevice(
             context: Context,
-            packageManager: PackageManager
+            packageManager: PackageManager,
+            logger: CastControllerLogger,
         ): CastDevice {
             return CastDevice(
                 id = this.packageName,
-                name = getAppName(this.packageName, packageManager),
+                name = getAppName(this.packageName, packageManager, logger),
                 description = context.getString(R.string.quick_settings_casting),
                 state = CastState.Connected,
                 tag = this,
@@ -76,7 +77,11 @@
             )
         }
 
-        private fun getAppName(packageName: String, packageManager: PackageManager): String {
+        private fun getAppName(
+            packageName: String,
+            packageManager: PackageManager,
+            logger: CastControllerLogger,
+        ): String {
             if (Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)) {
                 return ""
             }
@@ -86,9 +91,20 @@
                 if (!TextUtils.isEmpty(label)) {
                     return label.toString()
                 }
-                Log.w(CastControllerImpl.TAG, "No label found for package: $packageName")
+                logger.log(
+                    "#getAppName",
+                    LogLevel.WARNING,
+                    { str1 = packageName },
+                    { "No label found for package: $str1" },
+                )
             } catch (e: PackageManager.NameNotFoundException) {
-                Log.w(CastControllerImpl.TAG, "Error getting appName for package: $packageName", e)
+                logger.log(
+                    "#getAppName",
+                    LogLevel.WARNING,
+                    { str1 = packageName },
+                    { "Error getting appName for package=$str1" },
+                    e,
+                )
             }
             return packageName
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index 81e41d67..cf9a78f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.qs.tiles.FlashlightTile
 import com.android.systemui.qs.tiles.LocationTile
 import com.android.systemui.qs.tiles.MicrophoneToggleTile
+import com.android.systemui.qs.tiles.ModesTile
 import com.android.systemui.qs.tiles.UiModeNightTile
 import com.android.systemui.qs.tiles.WorkModeTile
 import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
@@ -79,6 +80,12 @@
     /** Inject DndTile into tileMap in QSModule */
     @Binds @IntoMap @StringKey(DndTile.TILE_SPEC) fun bindDndTile(dndTile: DndTile): QSTileImpl<*>
 
+    /** Inject ModesTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(ModesTile.TILE_SPEC)
+    fun bindModesTile(modesTile: ModesTile): QSTileImpl<*>
+
     /** Inject WorkModeTile into tileMap in QSModule */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/CastControllerLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/CastControllerLog.kt
new file mode 100644
index 0000000..23aade6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/CastControllerLog.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.dagger
+
+import javax.inject.Qualifier
+
+/**
+ * Logs for cast events. See [com.android.systemui.statusbar.policy.CastControllerImpl] and
+ * [com.android.systemui.statusbar.policy.CastControllerLogger].
+ */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class CastControllerLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index e08e4d7..71bcdfcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -228,4 +228,12 @@
     static LogBuffer provideBatteryControllerLog(LogBufferFactory factory) {
         return factory.create(BatteryControllerLogger.TAG, 30);
     }
+
+    /** Provides a log buffer for CastControllerImpl */
+    @Provides
+    @SysUISingleton
+    @CastControllerLog
+    static LogBuffer provideCastControllerLog(LogBufferFactory factory) {
+        return factory.create("CastControllerLog", 50);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
index 025354b..848a6e6 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
@@ -16,12 +16,12 @@
 package com.android.systemui.util.settings
 
 import android.annotation.UserIdInt
+import android.annotation.WorkerThread
 import android.content.ContentResolver
 import android.database.ContentObserver
 import android.net.Uri
 import android.os.UserHandle
 import android.provider.Settings.SettingNotFoundException
-import androidx.annotation.WorkerThread
 import com.android.app.tracing.TraceUtils.trace
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloat
@@ -67,6 +67,7 @@
         } else userTracker.userId
     }
 
+    @WorkerThread
     override fun registerContentObserverSync(uri: Uri, settingsObserver: ContentObserver) {
         registerContentObserverForUserSync(uri, settingsObserver, userId)
     }
@@ -83,6 +84,7 @@
         }
 
     /** Convenience wrapper around [ContentResolver.registerContentObserver].' */
+    @WorkerThread
     override fun registerContentObserverSync(
         uri: Uri,
         notifyForDescendants: Boolean,
@@ -120,6 +122,7 @@
      *
      * Implicitly calls [getUriFor] on the passed in name.
      */
+    @WorkerThread
     fun registerContentObserverForUserSync(
         name: String,
         settingsObserver: ContentObserver,
@@ -160,6 +163,7 @@
         }
 
     /** Convenience wrapper around [ContentResolver.registerContentObserver] */
+    @WorkerThread
     fun registerContentObserverForUserSync(
         uri: Uri,
         settingsObserver: ContentObserver,
@@ -222,6 +226,7 @@
      *
      * Implicitly calls [getUriFor] on the passed in name.
      */
+    @WorkerThread
     fun registerContentObserverForUserSync(
         name: String,
         notifyForDescendants: Boolean,
@@ -281,6 +286,7 @@
     }
 
     /** Convenience wrapper around [ContentResolver.registerContentObserver] */
+    @WorkerThread
     fun registerContentObserverForUserSync(
         uri: Uri,
         notifyForDescendants: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index de8b9b1..eb2f71a1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -73,10 +73,19 @@
         @Provides
         @SysUISingleton
         fun provideAudioSharingRepository(
+            @Application context: Context,
+            contentResolver: ContentResolver,
             localBluetoothManager: LocalBluetoothManager?,
+            @Application coroutineScope: CoroutineScope,
             @Background coroutineContext: CoroutineContext,
         ): AudioSharingRepository =
-            AudioSharingRepositoryImpl(localBluetoothManager, coroutineContext)
+            AudioSharingRepositoryImpl(
+                context,
+                contentResolver,
+                localBluetoothManager,
+                coroutineScope,
+                coroutineContext
+            )
 
         @Provides
         @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt
new file mode 100644
index 0000000..2904092
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dagger
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.volume.domain.interactor.AudioSharingInteractor
+import com.android.systemui.volume.domain.interactor.AudioSharingInteractorEmptyImpl
+import dagger.Module
+import dagger.Provides
+
+/** Dagger module for empty audio sharing impl for unnecessary volume overlay */
+@Module
+interface AudioSharingEmptyImplModule {
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun provideAudioSharingInteractor(): AudioSharingInteractor =
+            AudioSharingInteractorEmptyImpl()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingModule.kt
new file mode 100644
index 0000000..9f1e60e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingModule.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.volume.dagger
+
+import com.android.settingslib.volume.data.repository.AudioSharingRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.volume.domain.interactor.AudioSharingInteractor
+import com.android.systemui.volume.domain.interactor.AudioSharingInteractorImpl
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineScope
+
+/** Dagger module for audio sharing code in the volume package */
+@Module
+interface AudioSharingModule {
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun provideAudioSharingInteractor(
+            @Application coroutineScope: CoroutineScope,
+            repository: AudioSharingRepository
+        ): AudioSharingInteractor = AudioSharingInteractorImpl(coroutineScope, repository)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 5420988..ebb9ce9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -59,6 +59,7 @@
 @Module(
         includes = {
                 AudioModule.class,
+                AudioSharingModule.class,
                 AncModule.class,
                 CaptioningModule.class,
                 MediaDevicesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt
new file mode 100644
index 0000000..4d29788
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.volume.domain.interactor
+
+import android.bluetooth.BluetoothCsipSetCoordinator
+import androidx.annotation.IntRange
+import com.android.settingslib.volume.data.repository.AudioSharingRepository
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.launch
+
+interface AudioSharingInteractor {
+    /** Audio sharing secondary headset volume changes. */
+    val volume: Flow<Int?>
+
+    /** Audio sharing secondary headset min volume. */
+    val volumeMin: Int
+
+    /** Audio sharing secondary headset max volume. */
+    val volumeMax: Int
+
+    /** Set the volume of the secondary headset in audio sharing. */
+    fun setStreamVolume(
+        @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
+        level: Int
+    )
+}
+
+@SysUISingleton
+class AudioSharingInteractorImpl
+@Inject
+constructor(
+    @Application private val coroutineScope: CoroutineScope,
+    private val audioSharingRepository: AudioSharingRepository
+) : AudioSharingInteractor {
+
+    override val volume: Flow<Int?> =
+        combine(audioSharingRepository.secondaryGroupId, audioSharingRepository.volumeMap) {
+            secondaryGroupId,
+            volumeMap ->
+            if (secondaryGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) null
+            else volumeMap.getOrDefault(secondaryGroupId, DEFAULT_VOLUME)
+        }
+
+    override val volumeMin: Int = AUDIO_SHARING_VOLUME_MIN
+
+    override val volumeMax: Int = AUDIO_SHARING_VOLUME_MAX
+
+    override fun setStreamVolume(level: Int) {
+        coroutineScope.launch { audioSharingRepository.setSecondaryVolume(level) }
+    }
+
+    private companion object {
+        const val DEFAULT_VOLUME = 20
+    }
+}
+
+@SysUISingleton
+class AudioSharingInteractorEmptyImpl : AudioSharingInteractor {
+    override val volume: Flow<Int?> = emptyFlow()
+    override val volumeMin: Int = EMPTY_VOLUME
+    override val volumeMax: Int = EMPTY_VOLUME
+
+    override fun setStreamVolume(level: Int) {}
+
+    private companion object {
+        const val EMPTY_VOLUME = 0
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt
new file mode 100644
index 0000000..baef620
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt
@@ -0,0 +1,221 @@
+/*
+ * 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.biometrics
+
+import android.hardware.biometrics.BiometricFaceConstants
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class FaceHelpMessageDebouncerTest : SysuiTestCase() {
+    private lateinit var underTest: FaceHelpMessageDebouncer
+    private val window = 9L
+    private val startWindow = 4L
+    private val shownFaceMessageFrequencyBoost = 2
+
+    @Before
+    fun setUp() {
+        underTest =
+            FaceHelpMessageDebouncer(
+                window = window,
+                startWindow = startWindow,
+                shownFaceMessageFrequencyBoost = shownFaceMessageFrequencyBoost,
+            )
+    }
+
+    @Test
+    fun getMessageBeforeStartWindow_null() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "testTooClose",
+                0
+            )
+        )
+        assertThat(underTest.getMessageToShow(0)).isNull()
+    }
+
+    @Test
+    fun getMessageAfterStartWindow() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                0
+            )
+        )
+
+        assertThat(underTest.getMessageToShow(startWindow)?.msgId)
+            .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE)
+        assertThat(underTest.getMessageToShow(startWindow)?.msg).isEqualTo("tooClose")
+    }
+
+    @Test
+    fun getMessageAfterMessagesCleared_null() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                0
+            )
+        )
+        underTest.startNewFaceAuthSession(0)
+
+        assertThat(underTest.getMessageToShow(startWindow)).isNull()
+    }
+
+    @Test
+    fun messagesBeforeWindowRemoved() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                0
+            )
+        )
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                0
+            )
+        )
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                window - 1
+            )
+        )
+        val lastMessage =
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT,
+                "tooBright",
+                window
+            )
+        underTest.addMessage(lastMessage)
+
+        assertThat(underTest.getMessageToShow(window + 1)).isEqualTo(lastMessage)
+    }
+
+    @Test
+    fun getMessageTieGoesToMostRecent() {
+        for (i in 1..window step 2) {
+            underTest.addMessage(
+                HelpFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                    "tooClose",
+                    i
+                )
+            )
+            underTest.addMessage(
+                HelpFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT,
+                    "tooBright",
+                    i + 1
+                )
+            )
+        }
+
+        assertThat(underTest.getMessageToShow(window)?.msgId)
+            .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT)
+        assertThat(underTest.getMessageToShow(window)?.msg).isEqualTo("tooBright")
+    }
+
+    @Test
+    fun boostCurrentlyShowingMessage() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT,
+                "tooBright",
+                0
+            )
+        )
+
+        val lastMessageShown = underTest.getMessageToShow(startWindow)
+        assertThat(lastMessageShown?.msgId)
+            .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT)
+
+        for (i in 1..<shownFaceMessageFrequencyBoost) {
+            underTest.addMessage(
+                HelpFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                    "tooClose",
+                    startWindow
+                )
+            )
+        }
+
+        // although technically there's a different msgId with a higher frequency count now, the
+        // shownFaceMessageFrequencyBoost causes the last message shown to get a "boost"
+        // to keep showing
+        assertThat(underTest.getMessageToShow(startWindow)).isEqualTo(lastMessageShown)
+    }
+
+    @Test
+    fun overcomeBoostedCurrentlyShowingMessage() {
+        // Comments are assuming shownFaceMessageFrequencyBoost = 2
+        // [B], weights: B=1
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT,
+                "tooBright",
+                0
+            )
+        )
+
+        // [B], showing messageB, weights: B=3
+        val messageB = underTest.getMessageToShow(startWindow)
+
+        // [B, C, C], showing messageB, weights: B=3, C=2
+        for (i in 1..shownFaceMessageFrequencyBoost) {
+            underTest.addMessage(
+                HelpFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                    "tooClose",
+                    startWindow
+                )
+            )
+        }
+        // messageB is getting boosted to continue to show
+        assertThat(underTest.getMessageToShow(startWindow)).isEqualTo(messageB)
+
+        // receive one more FACE_ACQUIRED_TOO_CLOSE acquired info to pass the boost
+        // [C, C, C], showing messageB, weights: B=2, C=3
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                startWindow
+            )
+        )
+
+        // Now FACE_ACQUIRED_TOO_CLOSE has surpassed the boosted messageB frequency
+        // [C, C, C], showing messageC, weights: C=5
+        assertThat(underTest.getMessageToShow(startWindow)?.msgId)
+            .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt
index 431fef6..6fd8660 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt
@@ -24,6 +24,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FaceHelpMessageDebouncer
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
 import com.android.systemui.biometrics.domain.faceHelpMessageDeferral
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
@@ -45,6 +46,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -264,11 +266,20 @@
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
 
-            // WHEN authentication status help
+            // WHEN authentication status help past debouncer
             faceAuthRepository.setAuthenticationStatus(
                 HelpFaceAuthenticationStatus(
                     msg = "Move left",
                     msgId = FACE_ACQUIRED_TOO_RIGHT,
+                    createdAt = 0L,
+                )
+            )
+            runCurrent()
+            faceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(
+                    msg = "Move left",
+                    msgId = FACE_ACQUIRED_TOO_RIGHT,
+                    createdAt = FaceHelpMessageDebouncer.DEFAULT_WINDOW_MS,
                 )
             )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
index 529cd6e..0b7a3ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
@@ -90,6 +90,7 @@
     private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor
     private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig
     private val trustManager = kosmos.trustManager
+    private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor
 
     @Before
     fun setup() {
@@ -112,6 +113,7 @@
                 powerInteractor,
                 fakeBiometricSettingsRepository,
                 trustManager,
+                deviceEntryFaceAuthStatusInteractor,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorTest.kt
new file mode 100644
index 0000000..6022d9c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorTest.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.deviceentry.domain.interactor
+
+import android.hardware.biometrics.BiometricFaceConstants
+import android.hardware.face.FaceManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FaceHelpMessageDebouncer.Companion.DEFAULT_WINDOW_MS
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.shared.model.AcquiredFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DeviceEntryFaceAuthStatusInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope: TestScope = kosmos.testScope
+    private lateinit var underTest: DeviceEntryFaceAuthStatusInteractor
+    private val ignoreHelpMessageId = 1
+
+    @Before
+    fun setup() {
+        overrideResource(
+            R.array.config_face_acquire_device_entry_ignorelist,
+            intArrayOf(ignoreHelpMessageId)
+        )
+        underTest = kosmos.deviceEntryFaceAuthStatusInteractor
+    }
+
+    @Test
+    fun successAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val successStatus =
+                SuccessFaceAuthenticationStatus(
+                    successResult = mock(FaceManager.AuthenticationResult::class.java)
+                )
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(successStatus)
+            assertThat(authenticationStatus).isEqualTo(successStatus)
+        }
+
+    @Test
+    fun acquiredFaceAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val acquiredStatus = AcquiredFaceAuthenticationStatus(acquiredInfo = 0)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(acquiredStatus)
+            assertThat(authenticationStatus).isEqualTo(acquiredStatus)
+        }
+
+    @Test
+    fun failedFaceAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val failedStatus = FailedFaceAuthenticationStatus()
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(failedStatus)
+            assertThat(authenticationStatus).isEqualTo(failedStatus)
+        }
+
+    @Test
+    fun errorFaceAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val errorStatus = ErrorFaceAuthenticationStatus(0, "test")
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(errorStatus)
+            assertThat(authenticationStatus).isEqualTo(errorStatus)
+        }
+
+    @Test
+    fun firstHelpFaceAuthenticationStatus_noUpdate() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                AcquiredFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_START,
+                    createdAt = 0
+                )
+            )
+            val helpMessage = HelpFaceAuthenticationStatus(0, "test", 1)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(helpMessage)
+            assertThat(authenticationStatus).isNull()
+        }
+
+    @Test
+    fun helpFaceAuthenticationStatus_afterWindow() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(0, "test1", 0)
+            )
+            runCurrent()
+            val helpMessage = HelpFaceAuthenticationStatus(0, "test2", DEFAULT_WINDOW_MS)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(helpMessage)
+            runCurrent()
+            assertThat(authenticationStatus).isEqualTo(helpMessage)
+        }
+
+    @Test
+    fun helpFaceAuthenticationStatus_onlyIgnoredHelpMessages_afterWindow() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(ignoreHelpMessageId, "ignoredMsg", 0)
+            )
+            runCurrent()
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(ignoreHelpMessageId, "ignoredMsg", DEFAULT_WINDOW_MS)
+            )
+            runCurrent()
+            assertThat(authenticationStatus).isNull()
+        }
+
+    @Test
+    fun helpFaceAuthenticationStatus_afterWindow_onIgnoredMessage_showsOtherMessageInstead() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val validHelpMessage = HelpFaceAuthenticationStatus(0, "validHelpMsg", 0)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(validHelpMessage)
+            runCurrent()
+            // help message that should be ignored
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(ignoreHelpMessageId, "ignoredMsg", DEFAULT_WINDOW_MS)
+            )
+            runCurrent()
+            assertThat(authenticationStatus).isEqualTo(validHelpMessage)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index e2cca38..ae635b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -41,7 +41,6 @@
 import android.os.Handler;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.service.dreams.IDreamManager;
 import android.testing.TestableLooper;
 import android.view.GestureDetector;
 import android.view.IWindowManager;
@@ -106,7 +105,6 @@
 
     @Mock private GlobalActions.GlobalActionsManager mWindowManagerFuncs;
     @Mock private AudioManager mAudioManager;
-    @Mock private IDreamManager mDreamManager;
     @Mock private DevicePolicyManager mDevicePolicyManager;
     @Mock private LockPatternUtils mLockPatternUtils;
     @Mock private BroadcastDispatcher mBroadcastDispatcher;
@@ -165,7 +163,6 @@
         mGlobalActionsDialogLite = new GlobalActionsDialogLite(mContext,
                 mWindowManagerFuncs,
                 mAudioManager,
-                mDreamManager,
                 mDevicePolicyManager,
                 mLockPatternUtils,
                 mBroadcastDispatcher,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSourceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSourceTest.kt
index e49e2b49..5d59208 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSourceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/AppCategoriesShortcutsSourceTest.kt
@@ -16,21 +16,17 @@
 
 package com.android.systemui.keyboard.shortcut.data.source
 
-import android.content.Intent.CATEGORY_APP_BROWSER
-import android.content.Intent.CATEGORY_APP_CALCULATOR
-import android.content.Intent.CATEGORY_APP_CALENDAR
-import android.content.Intent.CATEGORY_APP_CONTACTS
-import android.content.Intent.CATEGORY_APP_EMAIL
-import android.content.Intent.CATEGORY_APP_MAPS
-import android.content.Intent.CATEGORY_APP_MESSAGING
-import android.content.Intent.CATEGORY_APP_MUSIC
+import android.view.KeyEvent
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+import android.view.mockWindowManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
-import com.android.systemui.util.icons.fakeAppCategoryIconProvider
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -43,185 +39,43 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val defaultAppIconsProvider = kosmos.fakeAppCategoryIconProvider
-    private val source = kosmos.shortcutHelperAppCategoriesShortcutsSource
+    private val mockWindowManager = kosmos.mockWindowManager
+    private val source =
+        AppCategoriesShortcutsSource(kosmos.mockWindowManager, kosmos.testDispatcher)
+
+    private var appCategoriesGroup: KeyboardShortcutGroup? = null
 
     @Before
     fun setUp() {
-        categoryApps.forEach { categoryAppIcon ->
-            defaultAppIconsProvider.installCategoryApp(
-                categoryAppIcon.category,
-                categoryAppIcon.packageName,
-                categoryAppIcon.iconResId
-            )
-        }
+        whenever(mockWindowManager.getApplicationLaunchKeyboardShortcuts(TEST_DEVICE_ID))
+            .thenAnswer { appCategoriesGroup }
     }
 
     @Test
-    fun shortcutGroups_returnsSingleGroup() =
-        testScope.runTest { assertThat(source.shortcutGroups(TEST_DEVICE_ID)).hasSize(1) }
+    fun shortcutGroups_nullResult_returnsEmptyList() =
+        testScope.runTest {
+            appCategoriesGroup = null
+
+            assertThat(source.shortcutGroups(TEST_DEVICE_ID)).isEmpty()
+        }
 
     @Test
-    fun shortcutGroups_hasAssistantIcon() =
+    fun shortcutGroups_returnsSortedList() =
         testScope.runTest {
-            defaultAppIconsProvider.installAssistantApp(ASSISTANT_PACKAGE, ASSISTANT_ICON_RES_ID)
+            val testItems =
+                listOf(
+                    KeyboardShortcutInfo("Info 2", KeyEvent.KEYCODE_E, KeyEvent.META_META_ON),
+                    KeyboardShortcutInfo("Info 1", KeyEvent.KEYCODE_E, KeyEvent.META_META_ON),
+                    KeyboardShortcutInfo("Info 3", KeyEvent.KEYCODE_E, KeyEvent.META_META_ON),
+                )
+            appCategoriesGroup = KeyboardShortcutGroup("Test Group", testItems)
 
             val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Assistant" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(ASSISTANT_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(ASSISTANT_ICON_RES_ID)
+            val shortcutLabels = shortcuts.map { it.label.toString() }
+            assertThat(shortcutLabels).containsExactly("Info 1", "Info 2", "Info 3").inOrder()
         }
 
-    @Test
-    fun shortcutGroups_hasBrowserIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Browser" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(BROWSER_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(BROWSER_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasContactsIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Contacts" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(CONTACTS_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(CONTACTS_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasEmailIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Email" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(EMAIL_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(EMAIL_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasCalendarIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Calendar" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(CALENDAR_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(CALENDAR_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasMapsIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Maps" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(MAPS_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(MAPS_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasMessagingIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "SMS" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(MESSAGING_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(MESSAGING_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasMusicIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Music" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(MUSIC_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(MUSIC_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_hasCalculatorIcon() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutInfo = shortcuts.first { it.label == "Calculator" }
-
-            assertThat(shortcutInfo.icon!!.resPackage).isEqualTo(CALCULATOR_PACKAGE)
-            assertThat(shortcutInfo.icon!!.resId).isEqualTo(CALCULATOR_ICON_RES_ID)
-        }
-
-    @Test
-    fun shortcutGroups_shortcutsSortedByLabelIgnoringCase() =
-        testScope.runTest {
-            val shortcuts = source.shortcutGroups(TEST_DEVICE_ID).first().items
-
-            val shortcutLabels = shortcuts.map { it.label!!.toString() }
-            assertThat(shortcutLabels).isEqualTo(shortcutLabels.sortedBy { it.lowercase() })
-        }
-
-    @Test
-    fun shortcutGroups_noAssistantApp_excludesAssistantFromShortcuts() =
-        testScope.runTest {
-            val shortcutLabels =
-                source.shortcutGroups(TEST_DEVICE_ID).first().items.map { it.label!!.toString() }
-
-            assertThat(shortcutLabels).doesNotContain("Assistant")
-        }
-
-    private companion object {
-        private const val ASSISTANT_PACKAGE = "the.assistant.app"
-        private const val ASSISTANT_ICON_RES_ID = 123
-
-        private const val BROWSER_PACKAGE = "com.test.browser"
-        private const val BROWSER_ICON_RES_ID = 1
-
-        private const val CONTACTS_PACKAGE = "app.test.contacts"
-        private const val CONTACTS_ICON_RES_ID = 234
-
-        private const val EMAIL_PACKAGE = "email.app.test"
-        private const val EMAIL_ICON_RES_ID = 351
-
-        private const val CALENDAR_PACKAGE = "app.test.calendar"
-        private const val CALENDAR_ICON_RES_ID = 411
-
-        private const val MAPS_PACKAGE = "maps.app.package"
-        private const val MAPS_ICON_RES_ID = 999
-
-        private const val MUSIC_PACKAGE = "com.android.music"
-        private const val MUSIC_ICON_RES_ID = 101
-
-        private const val MESSAGING_PACKAGE = "my.sms.app"
-        private const val MESSAGING_ICON_RES_ID = 9191
-
-        private const val CALCULATOR_PACKAGE = "that.calculator.app"
-        private const val CALCULATOR_ICON_RES_ID = 314
-
-        private val categoryApps =
-            listOf(
-                CategoryApp(CATEGORY_APP_BROWSER, BROWSER_PACKAGE, BROWSER_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_CONTACTS, CONTACTS_PACKAGE, CONTACTS_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_EMAIL, EMAIL_PACKAGE, EMAIL_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_CALENDAR, CALENDAR_PACKAGE, CALENDAR_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_MAPS, MAPS_PACKAGE, MAPS_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_MUSIC, MUSIC_PACKAGE, MUSIC_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_MESSAGING, MESSAGING_PACKAGE, MESSAGING_ICON_RES_ID),
-                CategoryApp(CATEGORY_APP_CALCULATOR, CALCULATOR_PACKAGE, CALCULATOR_ICON_RES_ID),
-            )
-
+    companion object {
         private const val TEST_DEVICE_ID = 123
     }
-
-    private class CategoryApp(val category: String, val packageName: String, val iconResId: Int)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index 5db8981..785d5a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
@@ -273,6 +274,7 @@
                     applicationScope = kosmos.applicationCoroutineScope,
                     backgroundDispatcher = kosmos.testDispatcher,
                     mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
+                    logger = logcatLogBuffer("TestMediaProjection"),
                 )
 
             val state by collectLastValue(repoWithTimingControl.mediaProjectionState)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index b169cc1..b4db6da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -27,6 +27,7 @@
 import android.view.WindowManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
 import com.android.internal.jank.Cuj
 import com.android.internal.util.LatencyTracker
 import com.android.systemui.SysuiTestCase
@@ -63,7 +64,7 @@
     private var triggerThreshold: Float = 0.0f
     private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop
     @Mock private lateinit var vibratorHelper: VibratorHelper
-    @Mock private lateinit var windowManager: WindowManager
+    @Mock private lateinit var viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager
     @Mock private lateinit var configurationController: ConfigurationController
     @Mock private lateinit var latencyTracker: LatencyTracker
     private val interactionJankMonitor by lazy { kosmos.interactionJankMonitor }
@@ -78,7 +79,7 @@
         mBackPanelController =
             BackPanelController(
                 context,
-                windowManager,
+                viewCaptureAwareWindowManager,
                 ViewConfiguration.get(context),
                 Handler.createAsync(testableLooper.looper),
                 systemClock,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
index 98ff6c9..45d77f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
@@ -29,6 +29,8 @@
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS;
 import static com.android.systemui.assist.AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
 import static com.android.systemui.navigationbar.views.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS;
+import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS;
+import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -38,6 +40,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
@@ -70,6 +73,7 @@
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodManager;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -162,6 +166,8 @@
     @Mock
     ButtonDispatcher mImeSwitchButton;
     @Mock
+    KeyButtonView mImeSwitchButtonView;
+    @Mock
     ButtonDispatcher mBackButton;
     @Mock
     NavigationBarTransitions mNavigationBarTransitions;
@@ -433,6 +439,45 @@
     }
 
     @Test
+    public void testImeSwitcherClick() {
+        mNavigationBar.init();
+        mNavigationBar.onViewAttached();
+        mNavigationBar.onImeSwitcherClick(mImeSwitchButtonView);
+
+        verify(mUiEventLogger).log(NAVBAR_IME_SWITCHER_BUTTON_TAP);
+        verify(mUiEventLogger, never()).log(NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+        if (Flags.imeSwitcherRevamp()) {
+            verify(mInputMethodManager)
+                    .onImeSwitchButtonClickFromSystem(mNavigationBar.mDisplayId);
+            verify(mInputMethodManager, never()).showInputMethodPickerFromSystem(
+                    anyBoolean() /* showAuxiliarySubtypes */, anyInt() /* displayId */);
+        } else {
+            verify(mInputMethodManager, never())
+                    .onImeSwitchButtonClickFromSystem(anyInt() /* displayId */);
+            verify(mInputMethodManager).showInputMethodPickerFromSystem(
+                    true /* showAuxiliarySubtypes */, mNavigationBar.mDisplayId);
+        }
+    }
+
+    @Test
+    public void testImeSwitcherLongClick() {
+        mNavigationBar.init();
+        mNavigationBar.onViewAttached();
+        mNavigationBar.onImeSwitcherLongClick(mImeSwitchButtonView);
+
+        verify(mUiEventLogger, never()).log(NAVBAR_IME_SWITCHER_BUTTON_TAP);
+        if (Flags.imeSwitcherRevamp()) {
+            verify(mUiEventLogger).log(NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+            verify(mInputMethodManager).showInputMethodPickerFromSystem(
+                    true /* showAuxiliarySubtypes */, mNavigationBar.mDisplayId);
+        } else {
+            verify(mUiEventLogger, never()).log(NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+            verify(mInputMethodManager, never()).showInputMethodPickerFromSystem(
+                    anyBoolean() /* showAuxiliarySubtypes */, anyInt() /* displayId */);
+        }
+    }
+
+    @Test
     public void testRegisteredWithUserTracker() {
         mNavigationBar.init();
         mNavigationBar.onViewAttached();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
new file mode 100644
index 0000000..4c77fb8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.qs.tiles
+
+import android.graphics.drawable.TestStubDrawable
+import android.os.Handler
+import android.platform.test.annotations.EnableFlags
+import android.service.quicksettings.Tile
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.settingslib.notification.data.repository.FakeZenModeRepository
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileDataInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.res.R
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.SecureSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@EnableFlags(android.app.Flags.FLAG_MODES_UI)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper(setAsMainLooper = true)
+class ModesTileTest : SysuiTestCase() {
+
+    @Mock private lateinit var qsHost: QSHost
+
+    @Mock private lateinit var metricsLogger: MetricsLogger
+
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+
+    @Mock private lateinit var activityStarter: ActivityStarter
+
+    @Mock private lateinit var qsLogger: QSLogger
+
+    @Mock private lateinit var uiEventLogger: QsEventLogger
+
+    @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider
+
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+    private val zenModeRepository = FakeZenModeRepository()
+    private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository)
+    private val mapper =
+        ModesTileMapper(
+            context.orCreateTestableResources
+                .apply {
+                    addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable())
+                    addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable())
+                }
+                .resources,
+            context.theme,
+        )
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private lateinit var userActionInteractor: ModesTileUserActionInteractor
+    private lateinit var secureSettings: SecureSettings
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var underTest: ModesTile
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        testableLooper = TestableLooper.get(this)
+        secureSettings = FakeSettings()
+
+        // Allow the tile to load resources
+        whenever(qsHost.context).thenReturn(context)
+        whenever(qsHost.userContext).thenReturn(context)
+
+        whenever(qsTileConfigProvider.getConfig(any()))
+            .thenReturn(
+                QSTileConfigTestBuilder.build {
+                    uiConfig =
+                        QSTileUIConfig.Resource(
+                            iconRes = R.drawable.qs_dnd_icon_off,
+                            labelRes = R.string.quick_settings_modes_label,
+                        )
+                }
+            )
+
+        userActionInteractor = ModesTileUserActionInteractor(inputHandler)
+
+        underTest =
+            ModesTile(
+                qsHost,
+                uiEventLogger,
+                testableLooper.looper,
+                Handler(testableLooper.looper),
+                FalsingManagerFake(),
+                metricsLogger,
+                statusBarStateController,
+                activityStarter,
+                qsLogger,
+                qsTileConfigProvider,
+                tileDataInteractor,
+                mapper,
+                userActionInteractor,
+            )
+
+        underTest.initialize()
+        underTest.setListening(Object(), true)
+
+        testableLooper.processAllMessages()
+    }
+
+    @After
+    fun tearDown() {
+        underTest.destroy()
+        testableLooper.processAllMessages()
+    }
+
+    @Test
+    fun stateUpdatesOnChange() =
+        testScope.runTest {
+            assertThat(underTest.state.state).isEqualTo(Tile.STATE_INACTIVE)
+
+            zenModeRepository.addMode(id = "Test", active = true)
+            runCurrent()
+            testableLooper.processAllMessages()
+
+            assertThat(underTest.state.state).isEqualTo(Tile.STATE_ACTIVE)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
index 5273495..eea02ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
@@ -61,6 +61,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.settingslib.wifi.WifiUtils;
@@ -160,7 +161,7 @@
     @Mock
     InternetDialogController.InternetDialogCallback mInternetDialogCallback;
     @Mock
-    private WindowManager mWindowManager;
+    private ViewCaptureAwareWindowManager mWindowManager;
     @Mock
     private ToastFactory mToastFactory;
     @Mock
@@ -232,8 +233,9 @@
                 mSubscriptionManager, mTelephonyManager, mWifiManager,
                 mConnectivityManager, mHandler, mExecutor, mBroadcastDispatcher,
                 mock(KeyguardUpdateMonitor.class), mGlobalSettings, mKeyguardStateController,
-                mWindowManager, mToastFactory, mWorkerHandler, mCarrierConfigTracker,
-                mLocationController, mDialogTransitionAnimator, mWifiStateWorker, mFlags);
+                mWindowManager, mToastFactory, mWorkerHandler,
+                mCarrierConfigTracker, mLocationController, mDialogTransitionAnimator,
+                mWifiStateWorker, mFlags);
         mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor,
                 mInternetDialogController.mOnSubscriptionsChangedListener);
         mInternetDialogController.onStart(mInternetDialogCallback, true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index 2444af7..477c50b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -18,6 +18,8 @@
 
 import static android.os.Process.myUid;
 
+import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertFalse;
@@ -69,10 +71,6 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-/**
- * Tests for exception handling and  bitmap configuration in adding smart actions to Screenshot
- * Notification.
- */
 public class RecordingControllerTest extends SysuiTestCase {
 
     private static final int TEST_USER_ID = 12345;
@@ -146,6 +144,7 @@
                 mFeatureFlags,
                 () -> mDevicePolicyResolver,
                 mUserTracker,
+                new RecordingControllerLogger(logcatLogBuffer("RecordingControllerTest")),
                 mMediaProjectionMetricsLogger,
                 mScreenCaptureDisabledDialogDelegate,
                 mScreenRecordDialogFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
index 10d07a0..5052a00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
@@ -35,8 +35,8 @@
 import android.os.PowerManager;
 import android.provider.Settings;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
@@ -53,6 +53,7 @@
 import com.android.systemui.unfold.FoldAodAnimationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.util.settings.FakeSettings;
+import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -114,6 +115,7 @@
                 .thenReturn(mFoldAodAnimationController);
         when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
 
+        SecureSettings secureSettings = new FakeSettings();
         mDozeParameters = new DozeParameters(
             mContext,
             mHandler,
@@ -132,7 +134,7 @@
             mStatusBarStateController,
             mUserTracker,
             mDozeInteractor,
-            new FakeSettings()
+            secureSettings
         );
 
         verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java
index 59b20c8..627463b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java
@@ -1,6 +1,8 @@
 package com.android.systemui.statusbar.policy;
 
 
+import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
+
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
@@ -58,7 +60,8 @@
         mController = new CastControllerImpl(
                 mContext,
                 mock(PackageManager.class),
-                mock(DumpManager.class));
+                mock(DumpManager.class),
+                new CastControllerLogger(logcatLogBuffer("CastControllerImplTest")));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
index 03ad66c..16061df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
@@ -25,6 +25,7 @@
 import android.media.projection.MediaProjectionInfo
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.CastDevice.Companion.toCastDevice
 import com.google.common.truth.Truth.assertThat
@@ -40,6 +41,7 @@
 class CastDeviceTest : SysuiTestCase() {
     private val mockAppInfo =
         mock<ApplicationInfo>().apply { whenever(this.loadLabel(any())).thenReturn("") }
+    private val logger = CastControllerLogger(logcatLogBuffer("CastDeviceTest"))
 
     private val packageManager =
         mock<PackageManager>().apply {
@@ -322,7 +324,7 @@
                 whenever(this.packageName).thenReturn("fake.package")
             }
 
-        val device = projection.toCastDevice(context, packageManager)
+        val device = projection.toCastDevice(context, packageManager, logger)
 
         assertThat(device.id).isEqualTo("fake.package")
     }
@@ -334,7 +336,7 @@
                 whenever(this.packageName).thenReturn(HEADLESS_REMOTE_PACKAGE)
             }
 
-        val device = projection.toCastDevice(context, packageManager)
+        val device = projection.toCastDevice(context, packageManager, logger)
 
         assertThat(device.name).isEmpty()
     }
@@ -349,7 +351,7 @@
         whenever(packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>()))
             .thenThrow(PackageManager.NameNotFoundException())
 
-        val device = projection.toCastDevice(context, packageManager)
+        val device = projection.toCastDevice(context, packageManager, logger)
 
         assertThat(device.name).isEqualTo(NORMAL_PACKAGE)
     }
@@ -366,7 +368,7 @@
         whenever(packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>()))
             .thenReturn(appInfo)
 
-        val device = projection.toCastDevice(context, packageManager)
+        val device = projection.toCastDevice(context, packageManager, logger)
 
         assertThat(device.name).isEqualTo(NORMAL_PACKAGE)
     }
@@ -383,7 +385,7 @@
         whenever(packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>()))
             .thenReturn(appInfo)
 
-        val device = projection.toCastDevice(context, packageManager)
+        val device = projection.toCastDevice(context, packageManager, logger)
 
         assertThat(device.name).isEqualTo("Valid App Name")
     }
@@ -392,7 +394,7 @@
     fun projectionToCastDevice_descriptionIsCasting() {
         val projection = mockProjectionInfo()
 
-        val device = projection.toCastDevice(context, packageManager)
+        val device = projection.toCastDevice(context, packageManager, logger)
 
         assertThat(device.description).isEqualTo(context.getString(R.string.quick_settings_casting))
     }
@@ -401,7 +403,7 @@
     fun projectionToCastDevice_stateIsConnected() {
         val projection = mockProjectionInfo()
 
-        val device = projection.toCastDevice(context, packageManager)
+        val device = projection.toCastDevice(context, packageManager, logger)
 
         assertThat(device.state).isEqualTo(CastDevice.CastState.Connected)
     }
@@ -410,7 +412,7 @@
     fun projectionToCastDevice_tagIsProjection() {
         val projection = mockProjectionInfo()
 
-        val device = projection.toCastDevice(context, packageManager)
+        val device = projection.toCastDevice(context, packageManager, logger)
 
         assertThat(device.tag).isEqualTo(projection)
     }
@@ -419,7 +421,7 @@
     fun projectionToCastDevice_originIsMediaProjection() {
         val projection = mockProjectionInfo()
 
-        val device = projection.toCastDevice(context, packageManager)
+        val device = projection.toCastDevice(context, packageManager, logger)
 
         assertThat(device.origin).isEqualTo(CastDevice.CastOrigin.MediaProjection)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index ce6bc09..cf0db7b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -29,11 +29,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@Ignore
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class BackGestureMonitorTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
index e632e34..f40282f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
@@ -21,8 +21,11 @@
 import android.view.MotionEvent
 import android.view.MotionEvent.CLASSIFICATION_NONE
 import android.view.MotionEvent.TOOL_TYPE_FINGER
+import java.lang.reflect.Method
+import org.mockito.kotlin.doNothing
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
 
 fun motionEvent(
     action: Int,
@@ -37,12 +40,31 @@
     val event =
         MotionEvent.obtain(/* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0)
     event.source = source
-    return spy<MotionEvent>(event) {
-        on { getToolType(0) } doReturn toolType
-        on { getPointerCount() } doReturn pointerCount
-        axisValues.forEach { (key, value) -> on { getAxisValue(key) } doReturn value }
-        on { getClassification() } doReturn classification
-    }
+    val spy =
+        spy<MotionEvent>(event) {
+            on { getToolType(0) } doReturn toolType
+            on { getPointerCount() } doReturn pointerCount
+            axisValues.forEach { (key, value) -> on { getAxisValue(key) } doReturn value }
+            on { getClassification() } doReturn classification
+        }
+    ensureFinalizeIsNotCalledTwice(spy)
+    return spy
+}
+
+private fun ensureFinalizeIsNotCalledTwice(spy: MotionEvent) {
+    // Spy in mockito will create copy of the spied object, copying all its field etc. Here it means
+    // we create copy of MotionEvent and its mNativePtr, so we have two separate objects of type
+    // MotionEvents with the same mNativePtr. That breaks because MotionEvent has custom finalize()
+    // method which goes to native code and tries to delete the reference from mNativePtr. It works
+    // first time but second time reference is already deleted and it breaks. That's why we have to
+    // avoid calling finalize twice
+    doNothing().whenever(spy).finalizeUsingReflection()
+}
+
+private fun MotionEvent.finalizeUsingReflection() {
+    val finalizeMethod: Method = MotionEvent::class.java.getDeclaredMethod("finalize")
+    finalizeMethod.isAccessible = true
+    finalizeMethod.invoke(this)
 }
 
 fun touchpadEvent(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index f5ef8b0..769f264 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -34,11 +34,9 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
 import com.google.common.truth.Truth.assertThat
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@Ignore
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class TouchpadGestureHandlerTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt
index 2a05598..d5451ee 100644
--- a/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt
@@ -19,4 +19,6 @@
 import com.android.systemui.kosmos.Kosmos
 import org.mockito.Mockito.mock
 
-val Kosmos.windowManager by Kosmos.Fixture<WindowManager> { mock(WindowManager::class.java) }
+val Kosmos.mockWindowManager: WindowManager by Kosmos.Fixture { mock(WindowManager::class.java) }
+
+var Kosmos.windowManager: WindowManager by Kosmos.Fixture { mockWindowManager }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
index 4b64416..de5f0f3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.deviceentry.domain.interactor.biometricMessageInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
@@ -44,7 +44,7 @@
             clock = systemClock,
             biometricMessageInteractor = biometricMessageInteractor,
             faceAuthInteractor = deviceEntryFaceAuthInteractor,
-            deviceEntryInteractor = deviceEntryInteractor,
+            deviceUnlockedInteractor = deviceUnlockedInteractor,
             fingerprintInteractor = deviceEntryFingerprintAuthInteractor,
             flags = composeBouncerFlags,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
index 25e7729..045bd5d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
@@ -30,16 +30,10 @@
     private val _isBypassEnabled = MutableStateFlow(false)
     override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled
 
-    var userPresentCount = 0
-
     override suspend fun isLockscreenEnabled(): Boolean {
         return isLockscreenEnabled
     }
 
-    override suspend fun reportUserPresent() {
-        userPresentCount++
-    }
-
     fun setLockscreenEnabled(isLockscreenEnabled: Boolean) {
         this.isLockscreenEnabled = isLockscreenEnabled
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
index a8fc27a..b9be04d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
@@ -57,5 +57,6 @@
             powerInteractor = powerInteractor,
             biometricSettingsRepository = biometricSettingsRepository,
             trustManager = trustManager,
+            deviceEntryFaceAuthStatusInteractor = deviceEntryFaceAuthStatusInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorKosmos.kt
new file mode 100644
index 0000000..66d3709
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.deviceentry.domain.interactor
+
+import android.content.res.mainResources
+import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.deviceEntryFaceAuthStatusInteractor by
+    Kosmos.Fixture {
+        DeviceEntryFaceAuthStatusInteractor(
+            repository = deviceEntryFaceAuthRepository,
+            resources = mainResources,
+            applicationScope = applicationCoroutineScope,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt
index 1200866..caa6e99 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt
@@ -19,8 +19,6 @@
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
-import com.android.systemui.flags.fakeSystemPropertiesHelper
-import com.android.systemui.keyguard.domain.interactor.trustInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -34,12 +32,7 @@
             repository = deviceEntryRepository,
             authenticationInteractor = authenticationInteractor,
             sceneInteractor = sceneInteractor,
-            faceAuthInteractor = deviceEntryFaceAuthInteractor,
-            fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
-            biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor,
-            trustInteractor = trustInteractor,
             deviceUnlockedInteractor = deviceUnlockedInteractor,
-            systemPropertiesHelper = fakeSystemPropertiesHelper,
             alternateBouncerInteractor = alternateBouncerInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
index 14210bc..1ed10fbe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
@@ -18,6 +18,8 @@
 
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
+import com.android.systemui.flags.fakeSystemPropertiesHelper
+import com.android.systemui.flags.systemPropertiesHelper
 import com.android.systemui.keyguard.domain.interactor.trustInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -33,5 +35,7 @@
         faceAuthInteractor = deviceEntryFaceAuthInteractor,
         fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
         powerInteractor = powerInteractor,
+        biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor,
+        systemPropertiesHelper = fakeSystemPropertiesHelper,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
new file mode 100644
index 0000000..f73f43d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 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.education.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import java.time.Instant
+
+var Kosmos.contextualEducationRepository: ContextualEducationRepository by
+    Kosmos.Fixture { FakeContextualEducationRepository(FakeEduClock(Instant.MIN)) }
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
new file mode 100644
index 0000000..5410882
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 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.education.data.repository
+
+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
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeContextualEducationRepository(private val clock: Clock) : ContextualEducationRepository {
+
+    private val userGestureMap = mutableMapOf<Int, GestureEduModel>()
+    private val _gestureEduModels = MutableStateFlow(GestureEduModel())
+    private val gestureEduModelsFlow = _gestureEduModels.asStateFlow()
+
+    override fun setUser(userId: Int) {
+        if (!userGestureMap.contains(userId)) {
+            userGestureMap[userId] = GestureEduModel()
+        }
+        _gestureEduModels.value = userGestureMap[userId]!!
+    }
+
+    override fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> {
+        return gestureEduModelsFlow
+    }
+
+    override suspend fun incrementSignalCount(gestureType: GestureType) {
+        _gestureEduModels.value =
+            GestureEduModel(
+                signalCount = _gestureEduModels.value.signalCount + 1,
+            )
+    }
+
+    override suspend fun updateShortcutTriggerTime(gestureType: GestureType) {
+        _gestureEduModels.value = GestureEduModel(lastShortcutTriggeredTime = clock.instant())
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt
new file mode 100644
index 0000000..513c143
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 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.education.data.repository
+
+import java.time.Clock
+import java.time.Instant
+import java.time.ZoneId
+
+class FakeEduClock(private val base: Instant) : Clock() {
+    private val zone: ZoneId = ZoneId.of("UTC")
+
+    override fun instant(): Instant {
+        return base
+    }
+
+    override fun withZone(zoneId: ZoneId?): Clock {
+        return FakeEduClock(base)
+    }
+
+    override fun getZone(): ZoneId {
+        return zone
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 001b55b..c423b62 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -41,13 +41,12 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.sysUiState
 import com.android.systemui.settings.displayTracker
-import com.android.systemui.util.icons.fakeAppCategoryIconProvider
 
 var Kosmos.shortcutHelperAppCategoriesShortcutsSource: KeyboardShortcutGroupsSource by
     Kosmos.Fixture {
         AppCategoriesShortcutsSource(
-            fakeAppCategoryIconProvider,
-            mainResources,
+            windowManager,
+            testDispatcher,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..b5f0b89
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModelKosmos.kt
@@ -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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+var Kosmos.dreamingToAodTransitionViewModel by Fixture {
+    DreamingToAodTransitionViewModel(
+        deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 2567ffe..3c5baa5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -46,6 +46,7 @@
         dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel,
         dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
         dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
+        dreamingToAodTransitionViewModel = dreamingToAodTransitionViewModel,
         dreamingToGoneTransitionViewModel = dreamingToGoneTransitionViewModel,
         dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
         glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt
index 81ba77a..0412274 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.mediaprojection.taskswitcher.activityTaskManagerTasksRepository
 import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
 
@@ -37,5 +38,6 @@
             tasksRepository = activityTaskManagerTasksRepository,
             backgroundDispatcher = testDispatcher,
             mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
+            logger = logcatLogBuffer("TestMediaProjection"),
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
index 327e1b5..d391750 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
@@ -16,16 +16,40 @@
 
 package com.android.systemui.volume.data.repository
 
+import androidx.annotation.IntRange
 import com.android.settingslib.volume.data.repository.AudioSharingRepository
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX
+import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN
+import com.android.settingslib.volume.data.repository.GroupIdToVolumes
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 
 class FakeAudioSharingRepository : AudioSharingRepository {
     private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    private val mutableSecondaryGroupId: MutableStateFlow<Int> =
+        MutableStateFlow(TEST_GROUP_ID_INVALID)
+    private val mutableVolumeMap: MutableStateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap())
 
     override val inAudioSharing: Flow<Boolean> = mutableInAudioSharing
+    override val secondaryGroupId: StateFlow<Int> = mutableSecondaryGroupId
+    override val volumeMap: StateFlow<GroupIdToVolumes> = mutableVolumeMap
+
+    override suspend fun setSecondaryVolume(volume: Int) {}
 
     fun setInAudioSharing(state: Boolean) {
         mutableInAudioSharing.value = state
     }
+
+    fun setSecondaryGroupId(groupId: Int) {
+        mutableSecondaryGroupId.value = groupId
+    }
+
+    fun setVolumeMap(volumeMap: GroupIdToVolumes) {
+        mutableVolumeMap.value = volumeMap
+    }
+
+    private companion object {
+        const val TEST_GROUP_ID_INVALID = -1
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt
new file mode 100644
index 0000000..03981bb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.volume.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.data.repository.audioSharingRepository
+
+val Kosmos.audioSharingInteractor by
+    Kosmos.Fixture {
+        AudioSharingInteractorImpl(
+            applicationCoroutineScope,
+            audioSharingRepository,
+        )
+    }
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 e031eb2..8a1fe62 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -30,7 +30,6 @@
         return RavenwoodRuntimeNative.lseek(fd, offset, whence);
     }
 
-
     public static FileDescriptor[] pipe2(int flags) throws ErrnoException {
         return RavenwoodRuntimeNative.pipe2(flags);
     }
@@ -42,4 +41,16 @@
     public static int fcntlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException {
         return RavenwoodRuntimeNative.fcntlInt(fd, cmd, arg);
     }
+
+    public static StructStat fstat(FileDescriptor fd) throws ErrnoException {
+        return RavenwoodRuntimeNative.fstat(fd);
+    }
+
+    public static StructStat lstat(String path) throws ErrnoException {
+        return RavenwoodRuntimeNative.lstat(path);
+    }
+
+    public static StructStat stat(String path) throws ErrnoException {
+        return RavenwoodRuntimeNative.stat(path);
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java
new file mode 100644
index 0000000..a8b1fca
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructStat.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2011 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.system;
+
+import libcore.util.Objects;
+
+/**
+ * File information returned by {@link Os#fstat}, {@link Os#lstat}, and {@link Os#stat}.
+ * Corresponds to C's {@code struct stat} from {@code <stat.h>}.
+ */
+public final class StructStat {
+    /** Device ID of device containing file. */
+    public final long st_dev; /*dev_t*/
+
+    /** File serial number (inode). */
+    public final long st_ino; /*ino_t*/
+
+    /** Mode (permissions) of file. */
+    public final int st_mode; /*mode_t*/
+
+    /** Number of hard links to the file. */
+    public final long st_nlink; /*nlink_t*/
+
+    /** User ID of file. */
+    public final int st_uid; /*uid_t*/
+
+    /** Group ID of file. */
+    public final int st_gid; /*gid_t*/
+
+    /** Device ID (if file is character or block special). */
+    public final long st_rdev; /*dev_t*/
+
+    /**
+     * For regular files, the file size in bytes.
+     * For symbolic links, the length in bytes of the pathname contained in the symbolic link.
+     * For a shared memory object, the length in bytes.
+     * For a typed memory object, the length in bytes.
+     * For other file types, the use of this field is unspecified.
+     */
+    public final long st_size; /*off_t*/
+
+    /** Seconds part of time of last access. */
+    public final long st_atime; /*time_t*/
+
+    /** StructTimespec with time of last access. */
+    public final StructTimespec st_atim;
+
+    /** Seconds part of time of last data modification. */
+    public final long st_mtime; /*time_t*/
+
+    /** StructTimespec with time of last modification. */
+    public final StructTimespec st_mtim;
+
+    /** Seconds part of time of last status change */
+    public final long st_ctime; /*time_t*/
+
+    /** StructTimespec with time of last status change. */
+    public final StructTimespec st_ctim;
+
+    /**
+     * A file system-specific preferred I/O block size for this object.
+     * For some file system types, this may vary from file to file.
+     */
+    public final long st_blksize; /*blksize_t*/
+
+    /** Number of blocks allocated for this object. */
+    public final long st_blocks; /*blkcnt_t*/
+
+    /**
+     * Constructs an instance with the given field values.
+     */
+    public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid,
+            long st_rdev, long st_size, long st_atime, long st_mtime, long st_ctime,
+            long st_blksize, long st_blocks) {
+        this(st_dev, st_ino, st_mode, st_nlink, st_uid, st_gid,
+                st_rdev, st_size, new StructTimespec(st_atime, 0L), new StructTimespec(st_mtime, 0L),
+                new StructTimespec(st_ctime, 0L), st_blksize, st_blocks);
+    }
+
+    /**
+     * Constructs an instance with the given field values.
+     */
+    public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid,
+            long st_rdev, long st_size, StructTimespec st_atim, StructTimespec st_mtim,
+            StructTimespec st_ctim, long st_blksize, long st_blocks) {
+        this.st_dev = st_dev;
+        this.st_ino = st_ino;
+        this.st_mode = st_mode;
+        this.st_nlink = st_nlink;
+        this.st_uid = st_uid;
+        this.st_gid = st_gid;
+        this.st_rdev = st_rdev;
+        this.st_size = st_size;
+        this.st_atime = st_atim.tv_sec;
+        this.st_mtime = st_mtim.tv_sec;
+        this.st_ctime = st_ctim.tv_sec;
+        this.st_atim = st_atim;
+        this.st_mtim = st_mtim;
+        this.st_ctim = st_ctim;
+        this.st_blksize = st_blksize;
+        this.st_blocks = st_blocks;
+    }
+
+    @Override public String toString() {
+        return Objects.toString(this);
+    }
+}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java
new file mode 100644
index 0000000..c106780
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/StructTimespec.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system;
+
+import libcore.util.Objects;;
+
+/**
+ * Corresponds to C's {@code struct timespec} from {@code <time.h>}.
+ */
+public final class StructTimespec implements Comparable<StructTimespec> {
+    /** Seconds part of time of last data modification. */
+    public final long tv_sec; /*time_t*/
+
+    /** Nanoseconds (values are [0, 999999999]). */
+    public final long tv_nsec;
+
+    public StructTimespec(long tv_sec, long tv_nsec) {
+        this.tv_sec = tv_sec;
+        this.tv_nsec = tv_nsec;
+        if (tv_nsec < 0 || tv_nsec > 999_999_999) {
+            throw new IllegalArgumentException(
+                    "tv_nsec value " + tv_nsec + " is not in [0, 999999999]");
+        }
+    }
+
+    @Override
+    public int compareTo(StructTimespec other) {
+        if (tv_sec > other.tv_sec) {
+            return 1;
+        }
+        if (tv_sec < other.tv_sec) {
+            return -1;
+        }
+        if (tv_nsec > other.tv_nsec) {
+            return 1;
+        }
+        if (tv_nsec < other.tv_nsec) {
+            return -1;
+        }
+        return 0;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        StructTimespec that = (StructTimespec) o;
+
+        if (tv_sec != that.tv_sec) return false;
+        return tv_nsec == that.tv_nsec;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (int) (tv_sec ^ (tv_sec >>> 32));
+        result = 31 * result + (int) (tv_nsec ^ (tv_nsec >>> 32));
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toString(this);
+    }
+}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
similarity index 65%
rename from ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java
rename to ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
index 6540221..e9b305e 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
@@ -15,6 +15,9 @@
  */
 package com.android.ravenwood.common;
 
+import android.system.ErrnoException;
+import android.system.StructStat;
+
 import java.io.FileDescriptor;
 
 /**
@@ -31,19 +34,25 @@
 
     public static native void applyFreeFunction(long freeFunction, long nativePtr);
 
-    public static native long nLseek(int fd, long offset, int whence);
+    private static native long nLseek(int fd, long offset, int whence) throws ErrnoException;
 
-    public static native int[] nPipe2(int flags);
+    private static native int[] nPipe2(int flags) throws ErrnoException;
 
-    public static native int nDup(int oldfd);
+    private static native int nDup(int oldfd) throws ErrnoException;
 
-    public static native int nFcntlInt(int fd, int cmd, int arg);
+    private static native int nFcntlInt(int fd, int cmd, int arg) throws ErrnoException;
 
-    public static long lseek(FileDescriptor fd, long offset, int whence) {
+    private static native StructStat nFstat(int fd) throws ErrnoException;
+
+    public static native StructStat lstat(String path) throws ErrnoException;
+
+    public static native StructStat stat(String path) throws ErrnoException;
+
+    public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
         return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
     }
 
-    public static FileDescriptor[] pipe2(int flags) {
+    public static FileDescriptor[] pipe2(int flags) throws ErrnoException {
         var fds = nPipe2(flags);
         var ret = new FileDescriptor[] {
                 new FileDescriptor(),
@@ -55,7 +64,7 @@
         return ret;
     }
 
-    public static FileDescriptor dup(FileDescriptor fd) {
+    public static FileDescriptor dup(FileDescriptor fd) throws ErrnoException {
         var fdInt = nDup(JvmWorkaround.getInstance().getFdInt(fd));
 
         var retFd = new java.io.FileDescriptor();
@@ -63,9 +72,15 @@
         return retFd;
     }
 
-    public static int fcntlInt(FileDescriptor fd, int cmd, int arg) {
+    public static int fcntlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException {
         var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
 
         return nFcntlInt(fdInt, cmd, arg);
     }
+
+    public static StructStat fstat(FileDescriptor fd) throws ErrnoException {
+        var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
+
+        return nFstat(fdInt);
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java
new file mode 100644
index 0000000..3781fcf
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/Objects.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2010 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 libcore.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+
+public final class Objects {
+    private Objects() {}
+
+    /**
+     * Returns a string reporting the value of each declared field, via reflection.
+     * Static and transient fields are automatically skipped. Produces output like
+     * "SimpleClassName[integer=1234,string="hello",character='c',intArray=[1,2,3]]".
+     */
+    public static String toString(Object o) {
+        Class<?> c = o.getClass();
+        StringBuilder sb = new StringBuilder();
+        sb.append(c.getSimpleName()).append('[');
+        int i = 0;
+        for (Field f : c.getDeclaredFields()) {
+            if ((f.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) {
+                continue;
+            }
+            f.setAccessible(true);
+            try {
+                Object value = f.get(o);
+
+                if (i++ > 0) {
+                    sb.append(',');
+                }
+
+                sb.append(f.getName());
+                sb.append('=');
+
+                if (value.getClass().isArray()) {
+                    if (value.getClass() == boolean[].class) {
+                        sb.append(Arrays.toString((boolean[]) value));
+                    } else if (value.getClass() == byte[].class) {
+                        sb.append(Arrays.toString((byte[]) value));
+                    } else if (value.getClass() == char[].class) {
+                        sb.append(Arrays.toString((char[]) value));
+                    } else if (value.getClass() == double[].class) {
+                        sb.append(Arrays.toString((double[]) value));
+                    } else if (value.getClass() == float[].class) {
+                        sb.append(Arrays.toString((float[]) value));
+                    } else if (value.getClass() == int[].class) {
+                        sb.append(Arrays.toString((int[]) value));
+                    } else if (value.getClass() == long[].class) {
+                        sb.append(Arrays.toString((long[]) value));
+                    } else if (value.getClass() == short[].class) {
+                        sb.append(Arrays.toString((short[]) value));
+                    } else {
+                        sb.append(Arrays.toString((Object[]) value));
+                    }
+                } else if (value.getClass() == Character.class) {
+                    sb.append('\'').append(value).append('\'');
+                } else if (value.getClass() == String.class) {
+                    sb.append('"').append(value).append('"');
+                } else {
+                    sb.append(value);
+                }
+            } catch (IllegalAccessException unexpected) {
+                throw new AssertionError(unexpected);
+            }
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+}
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index 34cf9f9..e0a3e1c 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -19,6 +19,9 @@
 #include <string.h>
 #include <unistd.h>
 #include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <nativehelper/ScopedUtfChars.h>
+
 #include "jni.h"
 #include "utils/Log.h"
 #include "utils/misc.h"
@@ -41,6 +44,75 @@
     return rc;
 }
 
+// ---- Helper functions ---
+
+static jclass g_StructStat;
+static jclass g_StructTimespecClass;
+
+static jclass findClass(JNIEnv* env, const char* name) {
+    ScopedLocalRef<jclass> localClass(env, env->FindClass(name));
+    jclass result = reinterpret_cast<jclass>(env->NewGlobalRef(localClass.get()));
+    if (result == NULL) {
+        ALOGE("failed to find class '%s'", name);
+        abort();
+    }
+    return result;
+}
+
+static jobject makeStructTimespec(JNIEnv* env, const struct timespec& ts) {
+    static jmethodID ctor = env->GetMethodID(g_StructTimespecClass, "<init>",
+            "(JJ)V");
+    if (ctor == NULL) {
+        return NULL;
+    }
+    return env->NewObject(g_StructTimespecClass, ctor,
+            static_cast<jlong>(ts.tv_sec), static_cast<jlong>(ts.tv_nsec));
+}
+
+static jobject makeStructStat(JNIEnv* env, const struct stat64& sb) {
+    static jmethodID ctor = env->GetMethodID(g_StructStat, "<init>",
+            "(JJIJIIJJLandroid/system/StructTimespec;Landroid/system/StructTimespec;Landroid/system/StructTimespec;JJ)V");
+    if (ctor == NULL) {
+        return NULL;
+    }
+
+    jobject atim_timespec = makeStructTimespec(env, sb.st_atim);
+    if (atim_timespec == NULL) {
+        return NULL;
+    }
+    jobject mtim_timespec = makeStructTimespec(env, sb.st_mtim);
+    if (mtim_timespec == NULL) {
+        return NULL;
+    }
+    jobject ctim_timespec = makeStructTimespec(env, sb.st_ctim);
+    if (ctim_timespec == NULL) {
+        return NULL;
+    }
+
+    return env->NewObject(g_StructStat, ctor,
+            static_cast<jlong>(sb.st_dev), static_cast<jlong>(sb.st_ino),
+            static_cast<jint>(sb.st_mode), static_cast<jlong>(sb.st_nlink),
+            static_cast<jint>(sb.st_uid), static_cast<jint>(sb.st_gid),
+            static_cast<jlong>(sb.st_rdev), static_cast<jlong>(sb.st_size),
+            atim_timespec, mtim_timespec, ctim_timespec,
+            static_cast<jlong>(sb.st_blksize), static_cast<jlong>(sb.st_blocks));
+}
+
+static jobject doStat(JNIEnv* env, jstring javaPath, bool isLstat) {
+    ScopedUtfChars path(env, javaPath);
+    if (path.c_str() == NULL) {
+        return NULL;
+    }
+    struct stat64 sb;
+    int rc = isLstat ? TEMP_FAILURE_RETRY(lstat64(path.c_str(), &sb))
+                     : TEMP_FAILURE_RETRY(stat64(path.c_str(), &sb));
+    if (rc == -1) {
+        throwErrnoException(env, isLstat ? "lstat" : "stat");
+        return NULL;
+    }
+    return makeStructStat(env, sb);
+}
+
 // ---- JNI methods ----
 
 typedef void (*FreeFunction)(void*);
@@ -77,6 +149,24 @@
     return throwIfMinusOne(env, "fcntl", TEMP_FAILURE_RETRY(fcntl(fd, F_DUPFD_CLOEXEC, 0)));
 }
 
+static jobject nFstat(JNIEnv* env, jobject, jint fd) {
+    struct stat64 sb;
+    int rc = TEMP_FAILURE_RETRY(fstat64(fd, &sb));
+    if (rc == -1) {
+        throwErrnoException(env, "fstat");
+        return NULL;
+    }
+    return makeStructStat(env, sb);
+}
+
+static jobject Linux_lstat(JNIEnv* env, jobject, jstring javaPath) {
+    return doStat(env, javaPath, true);
+}
+
+static jobject Linux_stat(JNIEnv* env, jobject, jstring javaPath) {
+    return doStat(env, javaPath, false);
+}
+
 // ---- Registration ----
 
 static const JNINativeMethod sMethods[] =
@@ -86,6 +176,9 @@
     { "nLseek", "(IJI)J", (void*)nLseek },
     { "nPipe2", "(I)[I", (void*)nPipe2 },
     { "nDup", "(I)I", (void*)nDup },
+    { "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 },
 };
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
@@ -101,6 +194,9 @@
 
     ALOGI("%s: JNI_OnLoad", __FILE__);
 
+    g_StructStat = findClass(env, "android/system/StructStat");
+    g_StructTimespecClass = findClass(env, "android/system/StructTimespec");
+
     jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/common/RavenwoodRuntimeNative",
             sMethods, NELEM(sMethods));
     if (res < 0) {
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
index b5038e6..05275b2 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
@@ -15,15 +15,26 @@
  */
 package com.android.ravenwood.runtimetest;
 
+import static android.system.OsConstants.S_ISBLK;
+import static android.system.OsConstants.S_ISCHR;
+import static android.system.OsConstants.S_ISDIR;
+import static android.system.OsConstants.S_ISFIFO;
+import static android.system.OsConstants.S_ISLNK;
+import static android.system.OsConstants.S_ISREG;
+import static android.system.OsConstants.S_ISSOCK;
+
 import static org.junit.Assert.assertEquals;
 
+import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
+
 import android.system.Os;
 import android.system.OsConstants;
+import android.system.StructStat;
+import android.system.StructTimespec;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.ravenwood.common.JvmWorkaround;
-import com.android.ravenwood.common.RavenwoodRuntimeNative;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -32,8 +43,15 @@
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
-import java.io.IOException;
 import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 public class OsTest {
@@ -41,7 +59,7 @@
         void accept(T var1) throws Exception;
     }
 
-    private void withTestFile(ConsumerWithThrow<FileDescriptor> consumer) throws Exception {
+    private void withTestFileFD(ConsumerWithThrow<FileDescriptor> consumer) throws Exception {
         File file = File.createTempFile("osTest", "bin");
         try (var raf = new RandomAccessFile(file, "rw")) {
             var fd = raf.getFD();
@@ -57,9 +75,20 @@
         }
     }
 
+    private void withTestFile(ConsumerWithThrow<Path> consumer) throws Exception {
+        var path = Files.createTempFile("osTest", "bin");
+        try (var os = Files.newOutputStream(path)) {
+            os.write(1);
+            os.write(2);
+            os.write(3);
+            os.write(4);
+        }
+        consumer.accept(path);
+    }
+
     @Test
     public void testLseek() throws Exception {
-        withTestFile((fd) -> {
+        withTestFileFD((fd) -> {
             assertEquals(4, Os.lseek(fd, 4, OsConstants.SEEK_SET));
             assertEquals(4, Os.lseek(fd, 0, OsConstants.SEEK_CUR));
             assertEquals(6, Os.lseek(fd, 2, OsConstants.SEEK_CUR));
@@ -68,7 +97,7 @@
 
     @Test
     public void testDup() throws Exception {
-        withTestFile((fd) -> {
+        withTestFileFD((fd) -> {
             var dup = Os.dup(fd);
 
             checkAreDup(fd, dup);
@@ -85,7 +114,7 @@
 
     @Test
     public void testFcntlInt() throws Exception {
-        withTestFile((fd) -> {
+        withTestFileFD((fd) -> {
             var dupInt = Os.fcntlInt(fd, 0, 0);
 
             var dup = new FileDescriptor();
@@ -95,16 +124,90 @@
         });
     }
 
-    private static void write(FileDescriptor fd, int oneByte)  throws IOException {
+    @Test
+    public void testStat() throws Exception {
+        withTestFile(path -> {
+            var attr = Files.readAttributes(path, PosixFileAttributes.class);
+            var stat = Os.stat(path.toAbsolutePath().toString());
+            assertAttributesEqual(attr, stat);
+        });
+    }
+
+    @Test
+    public void testLstat() throws Exception {
+        withTestFile(path -> {
+            // Create a symbolic link
+            var lnk = Files.createTempFile("osTest", "lnk");
+            Files.delete(lnk);
+            Files.createSymbolicLink(lnk, path);
+
+            // Test lstat
+            var attr = Files.readAttributes(lnk, PosixFileAttributes.class, NOFOLLOW_LINKS);
+            var stat = Os.lstat(lnk.toAbsolutePath().toString());
+            assertAttributesEqual(attr, stat);
+
+            // Test stat
+            var followAttr = Files.readAttributes(lnk, PosixFileAttributes.class);
+            var followStat = Os.stat(lnk.toAbsolutePath().toString());
+            assertAttributesEqual(followAttr, followStat);
+        });
+    }
+
+    @Test
+    public void testFstat() throws Exception {
+        withTestFile(path -> {
+            var attr = Files.readAttributes(path, PosixFileAttributes.class);
+            try (var raf = new RandomAccessFile(path.toFile(), "r")) {
+                var fd = raf.getFD();
+                var stat = Os.fstat(fd);
+                assertAttributesEqual(attr, stat);
+            }
+        });
+    }
+
+    // Verify StructStat values from libcore against native JVM PosixFileAttributes
+    private static void assertAttributesEqual(PosixFileAttributes attr, StructStat stat) {
+        assertEquals(attr.lastModifiedTime(), convertTimespecToFileTime(stat.st_mtim));
+        assertEquals(attr.size(), stat.st_size);
+        assertEquals(attr.isDirectory(), S_ISDIR(stat.st_mode));
+        assertEquals(attr.isRegularFile(), S_ISREG(stat.st_mode));
+        assertEquals(attr.isSymbolicLink(), S_ISLNK(stat.st_mode));
+        assertEquals(attr.isOther(), S_ISCHR(stat.st_mode)
+                || S_ISBLK(stat.st_mode) || S_ISFIFO(stat.st_mode) || S_ISSOCK(stat.st_mode));
+        assertEquals(attr.permissions(), convertModeToPosixPerms(stat.st_mode));
+
+    }
+
+    private static FileTime convertTimespecToFileTime(StructTimespec ts) {
+        var nanos = TimeUnit.SECONDS.toNanos(ts.tv_sec);
+        nanos += ts.tv_nsec;
+        return FileTime.from(nanos, TimeUnit.NANOSECONDS);
+    }
+
+    private static Set<PosixFilePermission> convertModeToPosixPerms(int mode) {
+        var set = new HashSet<PosixFilePermission>();
+        if ((mode & OsConstants.S_IRUSR) != 0) set.add(PosixFilePermission.OWNER_READ);
+        if ((mode & OsConstants.S_IWUSR) != 0) set.add(PosixFilePermission.OWNER_WRITE);
+        if ((mode & OsConstants.S_IXUSR) != 0) set.add(PosixFilePermission.OWNER_EXECUTE);
+        if ((mode & OsConstants.S_IRGRP) != 0) set.add(PosixFilePermission.GROUP_READ);
+        if ((mode & OsConstants.S_IWGRP) != 0) set.add(PosixFilePermission.GROUP_WRITE);
+        if ((mode & OsConstants.S_IXGRP) != 0) set.add(PosixFilePermission.GROUP_EXECUTE);
+        if ((mode & OsConstants.S_IROTH) != 0) set.add(PosixFilePermission.OTHERS_READ);
+        if ((mode & OsConstants.S_IWOTH) != 0) set.add(PosixFilePermission.OTHERS_WRITE);
+        if ((mode & OsConstants.S_IXOTH) != 0) set.add(PosixFilePermission.OTHERS_EXECUTE);
+        return set;
+    }
+
+    private static void write(FileDescriptor fd, int oneByte) throws Exception {
         // Create a dup to avoid closing the FD.
-        try (var dup = new FileOutputStream(RavenwoodRuntimeNative.dup(fd))) {
+        try (var dup = new FileOutputStream(Os.dup(fd))) {
             dup.write(oneByte);
         }
     }
 
-    private static int read(FileDescriptor fd) throws IOException {
+    private static int read(FileDescriptor fd) throws Exception {
         // Create a dup to avoid closing the FD.
-        try (var dup = new FileInputStream(RavenwoodRuntimeNative.dup(fd))) {
+        try (var dup = new FileInputStream(Os.dup(fd))) {
             return dup.read();
         }
     }
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index 7a99b60..311addb 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -29,10 +29,12 @@
         "//frameworks/base/packages/SettingsLib/RestrictedLockUtils:SettingsLibRestrictedLockUtilsSrc",
     ],
     libs: [
+        "aatf",
         "services.core",
         "androidx.annotation_annotation",
     ],
     static_libs: [
+        "a11ychecker-protos-java-proto-lite",
         "com_android_server_accessibility_flags_lib",
         "//frameworks/base/packages/SystemUI/aconfig:com_android_systemui_flags_lib",
 
@@ -68,3 +70,14 @@
     name: "com_android_server_accessibility_flags_lib",
     aconfig_declarations: "com_android_server_accessibility_flags",
 }
+
+java_library_static {
+    name: "a11ychecker-protos-java-proto-lite",
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+    srcs: [
+        "java/**/a11ychecker/proto/*.proto",
+    ],
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
new file mode 100644
index 0000000..55af9a0
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 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.accessibility.a11ychecker;
+
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.util.Slog;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckClass;
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultType;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClassNameCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateClickableBoundsCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateSpeakableTextCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ImageContrastCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.LinkPurposeUnclearCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.RedundantDescriptionCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TextSizeCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TraversalOrderCheck;
+
+import java.util.AbstractMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Util class to process a11y checker results for logging.
+ *
+ * @hide
+ */
+public class AccessibilityCheckerUtils {
+
+    private static final String LOG_TAG = "AccessibilityCheckerUtils";
+    @VisibleForTesting
+    // LINT.IfChange
+    static final Map<Class<? extends AccessibilityHierarchyCheck>, AccessibilityCheckClass>
+            CHECK_CLASS_TO_ENUM_MAP =
+            Map.ofEntries(
+                    classMapEntry(ClassNameCheck.class, AccessibilityCheckClass.CLASS_NAME_CHECK),
+                    classMapEntry(ClickableSpanCheck.class,
+                            AccessibilityCheckClass.CLICKABLE_SPAN_CHECK),
+                    classMapEntry(DuplicateClickableBoundsCheck.class,
+                            AccessibilityCheckClass.DUPLICATE_CLICKABLE_BOUNDS_CHECK),
+                    classMapEntry(DuplicateSpeakableTextCheck.class,
+                            AccessibilityCheckClass.DUPLICATE_SPEAKABLE_TEXT_CHECK),
+                    classMapEntry(EditableContentDescCheck.class,
+                            AccessibilityCheckClass.EDITABLE_CONTENT_DESC_CHECK),
+                    classMapEntry(ImageContrastCheck.class,
+                            AccessibilityCheckClass.IMAGE_CONTRAST_CHECK),
+                    classMapEntry(LinkPurposeUnclearCheck.class,
+                            AccessibilityCheckClass.LINK_PURPOSE_UNCLEAR_CHECK),
+                    classMapEntry(RedundantDescriptionCheck.class,
+                            AccessibilityCheckClass.REDUNDANT_DESCRIPTION_CHECK),
+                    classMapEntry(SpeakableTextPresentCheck.class,
+                            AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK),
+                    classMapEntry(TextContrastCheck.class,
+                            AccessibilityCheckClass.TEXT_CONTRAST_CHECK),
+                    classMapEntry(TextSizeCheck.class, AccessibilityCheckClass.TEXT_SIZE_CHECK),
+                    classMapEntry(TouchTargetSizeCheck.class,
+                            AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK),
+                    classMapEntry(TraversalOrderCheck.class,
+                            AccessibilityCheckClass.TRAVERSAL_ORDER_CHECK));
+    // LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto)
+
+    static Set<AccessibilityCheckResultReported> processResults(
+            Context context,
+            AccessibilityNodeInfo nodeInfo,
+            List<AccessibilityHierarchyCheckResult> checkResults,
+            @Nullable AccessibilityEvent accessibilityEvent,
+            ComponentName a11yServiceComponentName) {
+        return processResults(nodeInfo, checkResults, accessibilityEvent,
+                context.getPackageManager(), a11yServiceComponentName);
+    }
+
+    @VisibleForTesting
+    static Set<AccessibilityCheckResultReported> processResults(
+            AccessibilityNodeInfo nodeInfo,
+            List<AccessibilityHierarchyCheckResult> checkResults,
+            @Nullable AccessibilityEvent accessibilityEvent,
+            PackageManager packageManager,
+            ComponentName a11yServiceComponentName) {
+        String appPackageName = nodeInfo.getPackageName().toString();
+        AccessibilityCheckResultReported.Builder builder;
+        try {
+            builder = AccessibilityCheckResultReported.newBuilder()
+                    .setPackageName(appPackageName)
+                    .setAppVersionCode(getAppVersionCode(packageManager, appPackageName))
+                    .setUiElementPath(AccessibilityNodePathBuilder.createNodePath(nodeInfo))
+                    .setActivityName(getActivityName(packageManager, accessibilityEvent))
+                    .setWindowTitle(getWindowTitle(nodeInfo))
+                    .setSourceComponentName(a11yServiceComponentName.flattenToString())
+                    .setSourceVersionCode(
+                            getAppVersionCode(packageManager,
+                                    a11yServiceComponentName.getPackageName()));
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.e(LOG_TAG, "Unknown package name", e);
+            return Set.of();
+        }
+
+        return checkResults.stream()
+                .filter(checkResult -> checkResult.getType()
+                        == AccessibilityCheckResult.AccessibilityCheckResultType.ERROR
+                        || checkResult.getType()
+                        == AccessibilityCheckResult.AccessibilityCheckResultType.WARNING)
+                .map(checkResult -> builder.setResultCheckClass(
+                        getCheckClass(checkResult)).setResultType(
+                        getCheckResultType(checkResult)).setResultId(
+                        checkResult.getResultId()).build())
+                .collect(Collectors.toUnmodifiableSet());
+    }
+
+    private static long getAppVersionCode(PackageManager packageManager, String packageName) throws
+            PackageManager.NameNotFoundException {
+        PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
+        return packageInfo.getLongVersionCode();
+    }
+
+    /**
+     * Returns the simple class name of the Activity providing the cache update, if available,
+     * or an empty String if not.
+     */
+    @VisibleForTesting
+    static String getActivityName(
+            PackageManager packageManager, @Nullable AccessibilityEvent accessibilityEvent) {
+        if (accessibilityEvent == null) {
+            return "";
+        }
+        CharSequence activityName = accessibilityEvent.getClassName();
+        if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+                && accessibilityEvent.getPackageName() != null
+                && activityName != null) {
+            try {
+                // Check class is for a valid Activity.
+                packageManager
+                        .getActivityInfo(
+                                new ComponentName(accessibilityEvent.getPackageName().toString(),
+                                        activityName.toString()), 0);
+                int qualifierEnd = activityName.toString().lastIndexOf('.');
+                return activityName.toString().substring(qualifierEnd + 1);
+            } catch (PackageManager.NameNotFoundException e) {
+                // No need to spam the logs. This is very frequent when the class doesn't match
+                // an activity.
+            }
+        }
+        return "";
+    }
+
+    /**
+     * Returns the title of the window containing the a11y node.
+     */
+    private static String getWindowTitle(AccessibilityNodeInfo nodeInfo) {
+        if (nodeInfo.getWindow() == null) {
+            return "";
+        }
+        CharSequence windowTitle = nodeInfo.getWindow().getTitle();
+        return windowTitle == null ? "" : windowTitle.toString();
+    }
+
+    /**
+     * Maps the {@link AccessibilityHierarchyCheck} class that produced the given result, with the
+     * corresponding {@link AccessibilityCheckClass} enum. This enumeration is to avoid relying on
+     * String class names in the logging, which can be proguarded. It also reduces the logging size.
+     */
+    private static AccessibilityCheckClass getCheckClass(
+            AccessibilityHierarchyCheckResult checkResult) {
+        if (CHECK_CLASS_TO_ENUM_MAP.containsKey(checkResult.getSourceCheckClass())) {
+            return CHECK_CLASS_TO_ENUM_MAP.get(checkResult.getSourceCheckClass());
+        }
+        return AccessibilityCheckClass.UNKNOWN_CHECK;
+    }
+
+    private static AccessibilityCheckResultType getCheckResultType(
+            AccessibilityHierarchyCheckResult checkResult) {
+        return switch (checkResult.getType()) {
+            case ERROR -> AccessibilityCheckResultType.ERROR;
+            case WARNING -> AccessibilityCheckResultType.WARNING;
+            default -> AccessibilityCheckResultType.UNKNOWN_RESULT_TYPE;
+        };
+    }
+
+    private static Map.Entry<Class<? extends AccessibilityHierarchyCheck>,
+            AccessibilityCheckClass> classMapEntry(
+            Class<? extends AccessibilityHierarchyCheck> checkClass,
+            AccessibilityCheckClass checkClassEnum) {
+        return new AbstractMap.SimpleImmutableEntry<>(checkClass, checkClassEnum);
+    }
+}
diff --git a/core/java/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilder.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
similarity index 98%
rename from core/java/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
rename to services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
index 2996dde..bbfb217 100644
--- a/core/java/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.view.accessibility.a11ychecker;
+package com.android.server.accessibility.a11ychecker;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
diff --git a/core/java/android/view/accessibility/a11ychecker/OWNERS b/services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS
similarity index 100%
rename from core/java/android/view/accessibility/a11ychecker/OWNERS
rename to services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
new file mode 100644
index 0000000..8beed4a
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+syntax = "proto2";
+package android.accessibility;
+
+option java_package = "com.android.server.accessibility.a11ychecker";
+option java_outer_classname = "A11yCheckerProto";
+
+// TODO(b/326385939): remove and replace usage with the atom extension proto, when submitted.
+/** Logs the result of an AccessibilityCheck. */
+message AccessibilityCheckResultReported {
+  // Package name of the app containing the checked View.
+  optional string package_name = 1;
+  // Version code of the app containing the checked View.
+  optional int64 app_version_code = 2;
+  // The path of the View starting from the root element in the window. Each element is
+  // represented by the View's resource id, when available, or the View's class name.
+  optional string ui_element_path = 3;
+  // Class name of the activity containing the checked View.
+  optional string activity_name = 4;
+  // Title of the window containing the checked View.
+  optional string window_title = 5;
+  // The flattened component name of the app running the AccessibilityService which provided the a11y node.
+  optional string source_component_name = 6;
+  // Version code of the app running the AccessibilityService that provided the a11y node.
+  optional int64 source_version_code = 7;
+  // Class Name of the AccessibilityCheck that produced the result.
+  optional AccessibilityCheckClass result_check_class = 8;
+  // Result type of the AccessibilityCheckResult.
+  optional AccessibilityCheckResultType result_type = 9;
+  // Result ID of the AccessibilityCheckResult.
+  optional int32 result_id = 10;
+}
+
+/** The AccessibilityCheck class. */
+// LINT.IfChange
+enum AccessibilityCheckClass {
+  UNKNOWN_CHECK = 0;
+  CLASS_NAME_CHECK = 1;
+  CLICKABLE_SPAN_CHECK = 2;
+  DUPLICATE_CLICKABLE_BOUNDS_CHECK = 3;
+  DUPLICATE_SPEAKABLE_TEXT_CHECK = 4;
+  EDITABLE_CONTENT_DESC_CHECK = 5;
+  IMAGE_CONTRAST_CHECK = 6;
+  LINK_PURPOSE_UNCLEAR_CHECK = 7;
+  REDUNDANT_DESCRIPTION_CHECK = 8;
+  SPEAKABLE_TEXT_PRESENT_CHECK = 9;
+  TEXT_CONTRAST_CHECK = 10;
+  TEXT_SIZE_CHECK = 11;
+  TOUCH_TARGET_SIZE_CHECK = 12;
+  TRAVERSAL_ORDER_CHECK = 13;
+}
+// LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java)
+
+/** The type of AccessibilityCheckResult */
+enum AccessibilityCheckResultType {
+  UNKNOWN_RESULT_TYPE = 0;
+  ERROR = 1;
+  WARNING = 2;
+}
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index d7da2f0..026c69c 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -846,7 +846,6 @@
                 mCallingAppUid,
                 event.mIsCredentialRequest,
                 event.mWebviewRequestedCredential,
-                event.mFilteredFillabaleViewCount,
                 event.mViewFillableTotalCount,
                 event.mViewFillFailureCount,
                 event.mFocusedId,
@@ -859,7 +858,8 @@
                 event.mSuggestionPresentedLastTimestampMs,
                 event.mFocusedVirtualAutofillId,
                 event.mFieldFirstLength,
-                event.mFieldLastLength);
+                event.mFieldLastLength,
+                event.mFilteredFillabaleViewCount);
         mEventInternal = Optional.empty();
     }
 
diff --git a/services/core/java/com/android/server/ExplicitHealthCheckController.java b/services/core/java/com/android/server/ExplicitHealthCheckController.java
index 3d610d3..6a6aea4 100644
--- a/services/core/java/com/android/server/ExplicitHealthCheckController.java
+++ b/services/core/java/com/android/server/ExplicitHealthCheckController.java
@@ -15,6 +15,7 @@
  */
 
 package com.android.server;
+import static android.crashrecovery.flags.Flags.refactorCrashrecovery;
 import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE;
 import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES;
 import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES;
@@ -41,7 +42,6 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.Preconditions;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -363,22 +363,34 @@
     @GuardedBy("mLock")
     @Nullable
     private ServiceInfo getServiceInfoLocked() {
-        final String packageName =
-                mContext.getPackageManager().getServicesSystemSharedLibraryPackageName();
-        if (packageName == null) {
-            Slog.w(TAG, "no external services package!");
-            return null;
-        }
+        if (refactorCrashrecovery()) {
+            final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE);
+            final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
+                    PackageManager.GET_SERVICES | PackageManager.GET_META_DATA
+                            |  PackageManager.MATCH_SYSTEM_ONLY);
+            if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+                Slog.w(TAG, "No valid components found.");
+                return null;
+            }
+            return resolveInfo.serviceInfo;
+        } else {
+            final String packageName =
+                    mContext.getPackageManager().getServicesSystemSharedLibraryPackageName();
+            if (packageName == null) {
+                Slog.w(TAG, "no external services package!");
+                return null;
+            }
 
-        final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE);
-        intent.setPackage(packageName);
-        final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
-                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
-        if (resolveInfo == null || resolveInfo.serviceInfo == null) {
-            Slog.w(TAG, "No valid components found.");
-            return null;
+            final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE);
+            intent.setPackage(packageName);
+            final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
+                    PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+            if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+                Slog.w(TAG, "No valid components found.");
+                return null;
+            }
+            return resolveInfo.serviceInfo;
         }
-        return resolveInfo.serviceInfo;
     }
 
     @GuardedBy("mLock")
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 458749d..2d91331 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -185,6 +185,12 @@
 
     final Context mContext;
 
+    private static final int[] INTERESTING_APP_OPS = new int[] {
+        AppOpsManager.OP_GET_ACCOUNTS,
+        AppOpsManager.OP_READ_CONTACTS,
+        AppOpsManager.OP_WRITE_CONTACTS,
+    };
+
     private final PackageManager mPackageManager;
     private final AppOpsManager mAppOpsManager;
     private UserManager mUserManager;
@@ -388,74 +394,47 @@
         }.register(mContext, mHandler.getLooper(), UserHandle.ALL, true);
 
         // Cancel account request notification if an app op was preventing the account access
-        mAppOpsManager.startWatchingMode(AppOpsManager.OP_GET_ACCOUNTS, null,
-                new AppOpsManager.OnOpChangedInternalListener() {
-            @Override
-            public void onOpChanged(int op, String packageName) {
-                try {
-                    final int userId = ActivityManager.getCurrentUser();
-                    final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
-                    final int mode = mAppOpsManager.checkOpNoThrow(
-                            AppOpsManager.OP_GET_ACCOUNTS, uid, packageName);
-                    if (mode == AppOpsManager.MODE_ALLOWED) {
-                        final long identity = Binder.clearCallingIdentity();
-                        try {
-                            UserAccounts accounts = getUserAccounts(userId);
-                            cancelAccountAccessRequestNotificationIfNeeded(
-                                    packageName, uid, true, accounts);
-                        } finally {
-                            Binder.restoreCallingIdentity(identity);
-                        }
-                    }
-                } catch (NameNotFoundException e) {
-                    /* ignore */
-                } catch (SQLiteCantOpenDatabaseException e) {
-                    Log.w(TAG, "Can't read accounts database", e);
-                    return;
-                }
-            }
-        });
+        for (int i = 0; i < INTERESTING_APP_OPS.length; ++i) {
+            mAppOpsManager.startWatchingMode(INTERESTING_APP_OPS[i], null,
+                    new OnInterestingAppOpChangedListener());
+        }
 
-        // Cancel account request notification if a permission was preventing the account access
-        mPackageManager.addOnPermissionsChangeListener(
-                (int uid) -> {
-            // Permission changes cause requires updating accounts cache.
+        // Clear the accounts cache on permission changes.
+        // The specific permissions we care about are backed by AppOps, so just
+        // let the change events on those handle clearing any notifications.
+        mPackageManager.addOnPermissionsChangeListener((int uid) -> {
             AccountManager.invalidateLocalAccountsDataCaches();
-
-            Account[] accounts = null;
-            String[] packageNames = mPackageManager.getPackagesForUid(uid);
-            if (packageNames != null) {
-                final int userId = UserHandle.getUserId(uid);
-                final long identity = Binder.clearCallingIdentity();
-                try {
-                    for (String packageName : packageNames) {
-                                // if app asked for permission we need to cancel notification even
-                                // for O+ applications.
-                                if (mPackageManager.checkPermission(
-                                        Manifest.permission.GET_ACCOUNTS,
-                                        packageName) != PackageManager.PERMISSION_GRANTED) {
-                                    continue;
-                                }
-
-                        if (accounts == null) {
-                            accounts = getAccountsOrEmptyArray(null, userId, "android");
-                            if (ArrayUtils.isEmpty(accounts)) {
-                                return;
-                            }
-                        }
-                        UserAccounts userAccounts = getUserAccounts(UserHandle.getUserId(uid));
-                        for (Account account : accounts) {
-                            cancelAccountAccessRequestNotificationIfNeeded(
-                                    account, uid, packageName, true, userAccounts);
-                        }
-                    }
-                } finally {
-                    Binder.restoreCallingIdentity(identity);
-                }
-            }
         });
     }
 
+    private class OnInterestingAppOpChangedListener implements AppOpsManager.OnOpChangedListener {
+        @Override
+        public void onOpChanged(String op, String packageName) {
+            final int userId = ActivityManager.getCurrentUser();
+            final int packageUid;
+            try {
+                packageUid = mPackageManager.getPackageUidAsUser(packageName, userId);
+            } catch (NameNotFoundException e) {
+                /* ignore */
+                return;
+            }
+
+            final int mode = mAppOpsManager.checkOpNoThrow(op, packageUid, packageName);
+            if (mode != AppOpsManager.MODE_ALLOWED) {
+                return;
+            }
+
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                cancelAccountAccessRequestNotificationIfNeeded(
+                        packageName, packageUid, true, getUserAccounts(userId));
+            } catch (SQLiteCantOpenDatabaseException e) {
+                Log.w(TAG, "Can't read accounts database", e);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
 
     boolean getBindInstantServiceAllowed(int userId) {
         return  mAuthenticatorCache.getBindInstantServiceAllowed(userId);
diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java
index 12ec248..222c5a8 100644
--- a/services/core/java/com/android/server/display/DisplayBrightnessState.java
+++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java
@@ -16,7 +16,6 @@
 
 package com.android.server.display;
 
-import android.hardware.display.BrightnessInfo;
 import android.text.TextUtils;
 
 import com.android.server.display.brightness.BrightnessEvent;
@@ -51,8 +50,6 @@
 
     private final boolean mIsUserInitiatedChange;
 
-    private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason;
-
     private DisplayBrightnessState(Builder builder) {
         mBrightness = builder.getBrightness();
         mHdrBrightness = builder.getHdrBrightness();
@@ -67,7 +64,6 @@
         mBrightnessEvent = builder.getBrightnessEvent();
         mBrightnessAdjustmentFlag = builder.getBrightnessAdjustmentFlag();
         mIsUserInitiatedChange = builder.isUserInitiatedChange();
-        mBrightnessMaxReason = builder.getBrightnessMaxReason();
     }
 
     /**
@@ -163,13 +159,6 @@
         return mIsUserInitiatedChange;
     }
 
-    /**
-     * Gets reason for max brightness restriction
-     */
-    public @BrightnessInfo.BrightnessMaxReason int getBrightnessMaxReason() {
-        return mBrightnessMaxReason;
-    }
-
     @Override
     public String toString() {
         StringBuilder stringBuilder = new StringBuilder("DisplayBrightnessState:");
@@ -191,8 +180,6 @@
                 .append(Objects.toString(mBrightnessEvent, "null"));
         stringBuilder.append("\n    mBrightnessAdjustmentFlag:").append(mBrightnessAdjustmentFlag);
         stringBuilder.append("\n    mIsUserInitiatedChange:").append(mIsUserInitiatedChange);
-        stringBuilder.append("\n    mBrightnessMaxReason:")
-                .append(BrightnessInfo.briMaxReasonToString(mBrightnessMaxReason));
         return stringBuilder.toString();
     }
 
@@ -225,8 +212,7 @@
                     == otherState.shouldUpdateScreenBrightnessSetting()
                 && Objects.equals(mBrightnessEvent, otherState.getBrightnessEvent())
                 && mBrightnessAdjustmentFlag == otherState.getBrightnessAdjustmentFlag()
-                && mIsUserInitiatedChange == otherState.isUserInitiatedChange()
-                && mBrightnessMaxReason == otherState.getBrightnessMaxReason();
+                && mIsUserInitiatedChange == otherState.isUserInitiatedChange();
     }
 
     @Override
@@ -235,7 +221,7 @@
                 mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mMinBrightness,
                 mCustomAnimationRate,
                 mShouldUpdateScreenBrightnessSetting, mBrightnessEvent, mBrightnessAdjustmentFlag,
-                mIsUserInitiatedChange, mBrightnessMaxReason);
+                mIsUserInitiatedChange);
     }
 
     /**
@@ -259,11 +245,12 @@
         private float mMinBrightness;
         private float mCustomAnimationRate = CUSTOM_ANIMATION_RATE_NOT_SET;
         private boolean mShouldUpdateScreenBrightnessSetting;
+
         private BrightnessEvent mBrightnessEvent;
-        private int mBrightnessAdjustmentFlag = 0;
+
+        public int mBrightnessAdjustmentFlag = 0;
+
         private boolean mIsUserInitiatedChange;
-        private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason =
-                BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
 
         /**
          * Create a builder starting with the values from the specified {@link
@@ -287,7 +274,6 @@
             builder.setBrightnessEvent(state.getBrightnessEvent());
             builder.setBrightnessAdjustmentFlag(state.getBrightnessAdjustmentFlag());
             builder.setIsUserInitiatedChange(state.isUserInitiatedChange());
-            builder.setBrightnessMaxReason(state.getBrightnessMaxReason());
             return builder;
         }
 
@@ -510,21 +496,5 @@
             mIsUserInitiatedChange = isUserInitiatedChange;
             return this;
         }
-
-        /**
-         * Gets reason for max brightness restriction
-         */
-        public @BrightnessInfo.BrightnessMaxReason int getBrightnessMaxReason() {
-            return mBrightnessMaxReason;
-        }
-
-        /**
-         * Sets reason for max brightness restriction
-         */
-        public Builder setBrightnessMaxReason(
-                @BrightnessInfo.BrightnessMaxReason int brightnessMaxReason) {
-            mBrightnessMaxReason = brightnessMaxReason;
-            return this;
-        }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index f5231ae..7a055d1 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -619,6 +619,15 @@
  *     </idleScreenRefreshRateTimeout>
  *     <supportsVrr>true</supportsVrr>
  *
+ *     <dozeBrightnessSensorValueToBrightness>
+ *         <item>-1</item> <!-- 0: OFF -->
+ *         <item>0.003937008</item> <!-- 1: NIGHT -->
+ *         <item>0.015748031</item> <!-- 2: LOW -->
+ *         <item>0.102362205</item> <!-- 3: HIGH -->
+ *         <item>0.106299213</item> <!-- 4: SUN -->
+ *     </dozeBrightnessSensorValueToBrightness>
+ *     <defaultDozeBrightness>0.235</defaultDozeBrightness>
+ *
  *    </displayConfiguration>
  *  }
  *  </pre>
@@ -638,6 +647,10 @@
 
     public static final int DEFAULT_LOW_REFRESH_RATE = 60;
 
+    // Float.NaN (used as invalid for brightness) cannot be stored in config.xml
+    // so -2 is used instead
+    public static final float INVALID_BRIGHTNESS_IN_CONFIG = -2f;
+
     @VisibleForTesting
     static final float BRIGHTNESS_DEFAULT = 0.5f;
     private static final String ETC_DIR = "etc";
@@ -656,10 +669,6 @@
     private static final int INTERPOLATION_DEFAULT = 0;
     private static final int INTERPOLATION_LINEAR = 1;
 
-    // Float.NaN (used as invalid for brightness) cannot be stored in config.xml
-    // so -2 is used instead
-    private static final float INVALID_BRIGHTNESS_IN_CONFIG = -2f;
-
     // Length of the ambient light horizon used to calculate the long term estimate of ambient
     // light.
     private static final int AMBIENT_LIGHT_LONG_HORIZON_MILLIS = 10000;
@@ -670,6 +679,11 @@
     // Invalid value of AutoBrightness brightening and darkening light debounce
     private static final int INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE = -1;
 
+    @VisibleForTesting
+    static final float HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT = 0.5f;
+
+    private static final int KEEP_CURRENT_BRIGHTNESS = -1;
+
     private final Context mContext;
 
     // The details of the ambient light sensor associated with this display.
@@ -877,6 +891,10 @@
 
     private boolean mVrrSupportEnabled;
 
+    @Nullable
+    private float[] mDozeBrightnessSensorValueToBrightness;
+    private float mDefaultDozeBrightness;
+
     private final DisplayManagerFlags mFlags;
 
     @VisibleForTesting
@@ -1592,6 +1610,24 @@
         return mVrrSupportEnabled;
     }
 
+    /**
+     * While the device is dozing, a designated light sensor is used to determine the brightness.
+     * @return The mapping between doze brightness sensor values and brightness values. The value
+     * -1 means that the current brightness should be kept.
+     */
+    @Nullable
+    public float[] getDozeBrightnessSensorValueToBrightness() {
+        return mDozeBrightnessSensorValueToBrightness;
+    }
+
+    /**
+     * @return The default doze brightness to use while no other doze brightness is available. Can
+     * be {@link PowerManager#BRIGHTNESS_INVALID_FLOAT} if undefined.
+     */
+    public float getDefaultDozeBrightness() {
+        return mDefaultDozeBrightness;
+    }
+
     @Override
     public String toString() {
         return "DisplayDeviceConfig{"
@@ -1689,6 +1725,9 @@
                 ? mEvenDimmerBrightnessData.toString() : "null")
                 + "\n"
                 + "mVrrSupported= " + mVrrSupportEnabled + "\n"
+                + "mDozeBrightnessSensorValueToBrightness= "
+                + Arrays.toString(mDozeBrightnessSensorValueToBrightness) + "\n"
+                + "mDefaultDozeBrightness= " + mDefaultDozeBrightness + "\n"
                 + "}";
     }
 
@@ -1783,6 +1822,7 @@
                 loadBrightnessCapForWearBedtimeMode(config);
                 loadIdleScreenRefreshRateTimeoutConfigs(config);
                 mVrrSupportEnabled = config.getSupportsVrr();
+                loadDozeBrightness(config);
             } else {
                 Slog.w(TAG, "DisplayDeviceConfig file is null");
             }
@@ -1811,6 +1851,7 @@
         loadRefreshRateSetting(null);
         loadBrightnessCapForWearBedtimeModeFromConfigXml();
         loadIdleScreenRefreshRateTimeoutConfigs(null);
+        loadDozeBrightness(null);
         mLoadedFrom = "<config.xml>";
     }
 
@@ -2745,6 +2786,37 @@
         }
     }
 
+    private void loadDozeBrightness(DisplayConfiguration config) {
+        if (mFlags.isDozeBrightnessFloatEnabled() && config != null
+                && config.getDozeBrightnessSensorValueToBrightness() != null) {
+            List<BigDecimal> values = config.getDozeBrightnessSensorValueToBrightness().getItem();
+            mDozeBrightnessSensorValueToBrightness = new float[values.size()];
+            for (int i = 0; i < values.size(); i++) {
+                float backlight = values.get(i).floatValue();
+                if (backlight != KEEP_CURRENT_BRIGHTNESS) {
+                    mDozeBrightnessSensorValueToBrightness[i] =
+                            getBrightnessFromBacklight(backlight);
+                } else {
+                    mDozeBrightnessSensorValueToBrightness[i] = KEEP_CURRENT_BRIGHTNESS;
+                }
+            }
+        }
+
+        if (mFlags.isDozeBrightnessFloatEnabled() && config != null
+                && config.getDefaultDozeBrightness() != null) {
+            float backlight = config.getDefaultDozeBrightness().floatValue();
+            mDefaultDozeBrightness = getBrightnessFromBacklight(backlight);
+        } else {
+            mDefaultDozeBrightness = mContext.getResources().getFloat(
+                    com.android.internal.R.dimen.config_screenBrightnessDozeFloat);
+            if (mDefaultDozeBrightness == INVALID_BRIGHTNESS_IN_CONFIG) {
+                mDefaultDozeBrightness = BrightnessSynchronizer.brightnessIntToFloat(
+                        mContext.getResources().getInteger(
+                                com.android.internal.R.integer.config_screenBrightnessDoze));
+            }
+        }
+    }
+
     private void validateIdleScreenRefreshRateTimeoutConfig(
             IdleScreenRefreshRateTimeout idleScreenRefreshRateTimeoutConfig) {
         IdleScreenRefreshRateTimeoutLuxThresholds idleScreenRefreshRateTimeoutLuxThresholds =
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 2f3584c..b3a6c1c 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -4726,6 +4726,32 @@
             DisplayManagerService.this.mDisplayModeDirector.requestDisplayModes(
                     token, displayId, modeIds);
         }
+
+        @EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+        @Override // Binder call
+        public float[] getDozeBrightnessSensorValueToBrightness(int displayId) {
+            getDozeBrightnessSensorValueToBrightness_enforcePermission();
+            DisplayDeviceConfig ddc =
+                    mDisplayDeviceConfigProvider.getDisplayDeviceConfig(displayId);
+            if (ddc == null) {
+                throw new IllegalArgumentException(
+                        "Display ID does not have a config: " + displayId);
+            }
+            return ddc.getDozeBrightnessSensorValueToBrightness();
+        }
+
+        @EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS)
+        @Override // Binder call
+        public float getDefaultDozeBrightness(int displayId) {
+            getDefaultDozeBrightness_enforcePermission();
+            DisplayDeviceConfig ddc =
+                    mDisplayDeviceConfigProvider.getDisplayDeviceConfig(displayId);
+            if (ddc == null) {
+                throw new IllegalArgumentException(
+                        "Display ID does not have a config for doze-default: " + displayId);
+            }
+            return ddc.getDefaultDozeBrightness();
+        }
     }
 
     private static boolean isValidBrightness(float brightness) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 01604b8..5c1e783 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -272,7 +272,7 @@
     private final SettingsObserver mSettingsObserver;
 
     // The doze screen brightness.
-    private final float mScreenBrightnessDozeConfig;
+    private float mScreenBrightnessDozeConfig;
 
     // True if auto-brightness should be used.
     private boolean mUseSoftwareAutoBrightnessConfig;
@@ -550,7 +550,7 @@
 
         // DOZE AND DIM SETTINGS
         mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness(
-                pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE));
+                mDisplayDeviceConfig.getDefaultDozeBrightness());
         loadBrightnessRampRates();
         mSkipScreenOnBrightnessRamp = resources.getBoolean(
                 R.bool.config_skipScreenOnBrightnessRamp);
@@ -932,6 +932,8 @@
             HighBrightnessModeMetadata hbmMetadata) {
         // All properties that depend on the associated DisplayDevice and the DDC must be
         // updated here.
+        mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness(
+                mDisplayDeviceConfig.getDefaultDozeBrightness());
         loadBrightnessRampRates();
         loadNitsRange(mContext.getResources());
         setUpAutoBrightness(mContext, mHandler);
@@ -1580,7 +1582,7 @@
         // brightness sources (such as an app override) are not saved to the setting, but should be
         // reflected in HBM calculations.
         mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,
-                clampedState.getBrightnessMaxReason());
+                mBrightnessClamperController.getBrightnessMaxReason());
 
         // Animate the screen brightness when the screen is on or dozing.
         // Skip the animation when the screen is off or suspended.
@@ -1783,7 +1785,7 @@
 
             if (userSetBrightnessChanged
                     || newEvent.getReason().getReason() != BrightnessReason.REASON_TEMPORARY) {
-                logBrightnessEvent(newEvent, unthrottledBrightnessState, clampedState);
+                logBrightnessEvent(newEvent, unthrottledBrightnessState);
             }
             if (mBrightnessEventRingBuffer != null) {
                 mBrightnessEventRingBuffer.append(newEvent);
@@ -1976,9 +1978,6 @@
         synchronized (mCachedBrightnessInfo) {
             float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX;
             float stateMin = state != null ? state.getMinBrightness() : PowerManager.BRIGHTNESS_MAX;
-            @BrightnessInfo.BrightnessMaxReason int maxReason =
-                    state != null ? state.getBrightnessMaxReason()
-                            : BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
             final float minBrightness = Math.max(stateMin, Math.min(
                     mBrightnessRangeController.getCurrentBrightnessMin(), stateMax));
             final float maxBrightness = Math.min(
@@ -2005,7 +2004,7 @@
                             mBrightnessRangeController.getTransitionPoint());
             changed |=
                     mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,
-                            maxReason);
+                            mBrightnessClamperController.getBrightnessMaxReason());
             return changed;
         }
     }
@@ -2905,8 +2904,7 @@
         return FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_UNKNOWN;
     }
 
-    private void logBrightnessEvent(BrightnessEvent event, float unmodifiedBrightness,
-            DisplayBrightnessState brightnessState) {
+    private void logBrightnessEvent(BrightnessEvent event, float unmodifiedBrightness) {
         int modifier = event.getReason().getModifier();
         int flags = event.getFlags();
         // It's easier to check if the brightness is at maximum level using the brightness
@@ -2943,7 +2941,7 @@
                     event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
                     event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR,
                     (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0,
-                    brightnessState.getBrightnessMaxReason(),
+                    mBrightnessClamperController.getBrightnessMaxReason(),
                     // TODO: (flc) add brightnessMinReason here too.
                     (modifier & BrightnessReason.MODIFIER_DIMMED) > 0,
                     event.isRbcEnabled(),
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index d3b41b8..e9ecfc6 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -25,6 +25,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.devicestate.DeviceState;
+import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.devicestate.feature.flags.FeatureFlags;
 import android.hardware.devicestate.feature.flags.FeatureFlagsImpl;
 import android.os.Handler;
@@ -614,7 +615,11 @@
                     && isBootCompleted
                     && !mFoldSettingProvider.shouldStayAwakeOnFold();
         } else {
-            return mDeviceStatesOnWhichToSelectiveSleep.get(pendingState.getIdentifier())
+            return currentState.getIdentifier()
+                    != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER
+                    && pendingState.getIdentifier()
+                    != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER
+                    && mDeviceStatesOnWhichToSelectiveSleep.get(pendingState.getIdentifier())
                     && !mDeviceStatesOnWhichToSelectiveSleep.get(currentState.getIdentifier())
                     && isInteractive
                     && isBootCompleted
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 d1fb009..88d2c00 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
@@ -139,7 +139,6 @@
         builder.setBrightness(cappedBrightness);
         builder.setMaxBrightness(mBrightnessCap);
         builder.setCustomAnimationRate(mCustomAnimationRate);
-        builder.setBrightnessMaxReason(getBrightnessMaxReason());
 
         if (mClamperType != null) {
             builder.getBrightnessReason().addModifier(BrightnessReason.MODIFIER_THROTTLED);
@@ -164,8 +163,19 @@
         return builder.build();
     }
 
+    /**
+     * See BrightnessThrottler.getBrightnessMaxReason:
+     * used in:
+     * 1) DPC2.CachedBrightnessInfo to determine changes
+     * 2) DPC2.logBrightnessEvent
+     * 3) HBMController - for logging
+     * Method is called in mHandler thread (DisplayControllerHandler), in the same thread
+     * recalculateBrightnessCap and DPC2.updatePowerStateInternal are called.
+     * Should be moved to DisplayBrightnessState OR derived from DisplayBrightnessState
+     * TODO: b/263362199
+     */
     @BrightnessInfo.BrightnessMaxReason
-    private int getBrightnessMaxReason() {
+    public int getBrightnessMaxReason() {
         if (mClamperType == null) {
             return BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
         } else if (mClamperType == Type.THERMAL) {
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index 3ce7d2a..e1934b0 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -154,6 +154,10 @@
             Flags::useFusionProxSensor
     );
 
+    private final FlagState mDozeBrightnessFloat = new FlagState(
+            Flags.FLAG_DOZE_BRIGHTNESS_FLOAT,
+            Flags::dozeBrightnessFloat);
+
     private final FlagState mOffloadControlsDozeAutoBrightness = new FlagState(
             Flags.FLAG_OFFLOAD_CONTROLS_DOZE_AUTO_BRIGHTNESS,
             Flags::offloadControlsDozeAutoBrightness
@@ -347,6 +351,10 @@
         return mUseFusionProxSensor.getName();
     }
 
+    public boolean isDozeBrightnessFloatEnabled() {
+        return mDozeBrightnessFloat.isEnabled();
+    }
+
     /**
      * @return Whether DisplayOffload should control auto-brightness in doze
      */
@@ -415,6 +423,7 @@
         pw.println(" " + mRefactorDisplayPowerController);
         pw.println(" " + mResolutionBackupRestore);
         pw.println(" " + mUseFusionProxSensor);
+        pw.println(" " + mDozeBrightnessFloat);
         pw.println(" " + mOffloadControlsDozeAutoBrightness);
         pw.println(" " + mPeakRefreshRatePhysicalLimit);
         pw.println(" " + mIgnoreAppPreferredRefreshRate);
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index fd3af23..ac5f97f 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -246,6 +246,14 @@
 }
 
 flag {
+    name: "doze_brightness_float"
+    namespace: "display_manager"
+    description: "Define doze brightness in the float scale [0, 1]."
+    bug: "343796384"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "offload_controls_doze_auto_brightness"
     namespace: "display_manager"
     description: "Allows the registered DisplayOffloader to control if auto-brightness is used in doze"
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index 5ff421a..7c93c8b 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -89,8 +89,8 @@
     void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
             @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason, @UserIdInt int userId) {
-        final var bindingController = mService.getInputMethodBindingController(userId);
         final var userData = mService.getUserData(userId);
+        final var bindingController = userData.mBindingController;
         final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         if (curMethod != null) {
             if (DEBUG) {
@@ -128,9 +128,9 @@
     void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason,
             @UserIdInt int userId) {
-        final var bindingController = mService.getInputMethodBindingController(userId);
-        final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         final var userData = mService.getUserData(userId);
+        final var bindingController = userData.mBindingController;
+        final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         if (curMethod != null) {
             // The IME will report its visible state again after the following message finally
             // delivered to the IME process as an IPC.  Hence the inconsistency between
@@ -171,8 +171,8 @@
     void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
             @ImeVisibilityStateComputer.VisibilityState int state,
             @SoftInputShowHideReason int reason, @UserIdInt int userId) {
-        final var bindingController = mService.getInputMethodBindingController(userId);
         final var userData = mService.getUserData(userId);
+        final var bindingController = userData.mBindingController;
         final int displayIdToShowIme = bindingController.getDisplayIdToShowIme();
         switch (state) {
             case STATE_SHOW_IME:
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
index 3f28c47..a7280e6 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
@@ -138,6 +138,9 @@
         @PermissionVerified(Manifest.permission.TEST_INPUT_METHOD)
         boolean isInputMethodPickerShownForTest();
 
+        @PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+        void onImeSwitchButtonClickFromSystem(int displayId);
+
         InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId);
 
         void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes,
@@ -344,6 +347,14 @@
         return mCallback.isInputMethodPickerShownForTest();
     }
 
+    @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @Override
+    public void onImeSwitchButtonClickFromSystem(int displayId) {
+        super.onImeSwitchButtonClickFromSystem_enforcePermission();
+
+        mCallback.onImeSwitchButtonClickFromSystem(displayId);
+    }
+
     @Override
     public InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) {
         return mCallback.getCurrentInputMethodSubtype(userId);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index f5faeef..8fd2033 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -53,6 +53,7 @@
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
 import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO;
 import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -475,7 +476,7 @@
 
     @AnyThread
     @NonNull
-    UserDataRepository.UserData getUserData(@UserIdInt int userId) {
+    UserData getUserData(@UserIdInt int userId) {
         return mUserDataRepository.getOrCreate(userId);
     }
 
@@ -1421,15 +1422,15 @@
      * Returns true iff the caller is identified to be the current input method with the token.
      *
      * @param token the window token given to the input method when it was started
-     * @param userId userId of the calling IME process
+     * @param userData {@link UserData} of the calling IME process
      * @return true if and only if non-null valid token is specified
      */
     @GuardedBy("ImfLock.class")
-    private boolean calledWithValidTokenLocked(@NonNull IBinder token, @UserIdInt int userId) {
+    private boolean calledWithValidTokenLocked(@NonNull IBinder token, @NonNull UserData userData) {
         if (token == null) {
             throw new InvalidParameterException("token must not be null.");
         }
-        final var bindingController = getInputMethodBindingController(userId);
+        final var bindingController = userData.mBindingController;
         if (token != bindingController.getCurToken()) {
             Slog.e(TAG, "Ignoring " + Debug.getCaller() + " due to an invalid token."
                     + " uid:" + Binder.getCallingUid() + " token:" + token);
@@ -1708,7 +1709,7 @@
         clearClientSessionLocked(client);
         clearClientSessionForAccessibilityLocked(client);
         // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
-        @SuppressWarnings("GuardedBy") Consumer<UserDataRepository.UserData> clientRemovedForUser =
+        @SuppressWarnings("GuardedBy") Consumer<UserData> clientRemovedForUser =
                 userData -> onClientRemovedInternalLocked(client, userData);
         mUserDataRepository.forAllUserData(clientRemovedForUser);
     }
@@ -1718,15 +1719,14 @@
      */
     // TODO(b/325515685): Move this method to InputMethodBindingController
     @GuardedBy("ImfLock.class")
-    private void onClientRemovedInternalLocked(ClientState client,
-            @NonNull UserDataRepository.UserData userData) {
+    private void onClientRemovedInternalLocked(ClientState client, @NonNull UserData userData) {
         final int userId = userData.mUserId;
         if (userData.mCurClient == client) {
             hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
                     SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
             if (userData.mBoundToMethod) {
                 userData.mBoundToMethod = false;
-                final var userBindingController = getInputMethodBindingController(userId);
+                final var userBindingController = userData.mBindingController;
                 IInputMethodInvoker curMethod = userBindingController.getCurMethod();
                 if (curMethod != null) {
                     // When we unbind input, we are unbinding the client, so we always
@@ -1758,7 +1758,7 @@
                 Slog.v(TAG, "unbindCurrentInputLocked: client="
                         + userData.mCurClient.mClient.asBinder());
             }
-            final var bindingController = getInputMethodBindingController(userId);
+            final var bindingController = userData.mBindingController;
             if (userData.mBoundToMethod) {
                 userData.mBoundToMethod = false;
                 IInputMethodInvoker curMethod = bindingController.getCurMethod();
@@ -1844,8 +1844,8 @@
     @NonNull
     InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial,
             @UserIdInt int userId) {
-        final var bindingController = getInputMethodBindingController(userId);
         final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         if (!userData.mBoundToMethod) {
             bindingController.getCurMethod().bindInput(userData.mCurClient.mBinding);
             userData.mBoundToMethod = true;
@@ -2274,9 +2274,10 @@
                     + bindingController.getCurTokenDisplayId());
         }
         final int userId = bindingController.getUserId();
+        final var userData = getUserData(userId);
         inputMethod.initializeInternal(token,
-                new InputMethodPrivilegedOperationsImpl(this, token, userId),
-                getInputMethodNavButtonFlagsLocked(getUserData(userId)));
+                new InputMethodPrivilegedOperationsImpl(this, token, userData),
+                getInputMethodNavButtonFlagsLocked(userData));
     }
 
     @AnyThread
@@ -2316,8 +2317,8 @@
                     channel.dispose();
                     return;
                 }
-                final var bindingController = getInputMethodBindingController(userId);
                 final var userData = getUserData(userId);
+                final var bindingController = userData.mBindingController;
                 IInputMethodInvoker curMethod = bindingController.getCurMethod();
                 if (curMethod != null && method != null
                         && curMethod.asBinder() == method.asBinder()) {
@@ -2530,9 +2531,10 @@
 
     @BinderThread
     private void updateStatusIcon(@NonNull IBinder token, String packageName,
-            @DrawableRes int iconId, @UserIdInt int userId) {
+            @DrawableRes int iconId, @NonNull UserData userData) {
+        final int userId = userData.mUserId;
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token, userId)) {
+            if (!calledWithValidTokenLocked(token, userData)) {
                 return;
             }
             final long ident = Binder.clearCallingIdentity();
@@ -2575,8 +2577,7 @@
 
     @GuardedBy("ImfLock.class")
     @InputMethodNavButtonFlags
-    private int getInputMethodNavButtonFlagsLocked(
-            @NonNull UserDataRepository.UserData userData) {
+    private int getInputMethodNavButtonFlagsLocked(@NonNull UserData userData) {
         final int userId = userData.mUserId;
         final var bindingController = userData.mBindingController;
         // Whether the current display has a navigation bar. When this is false (e.g. emulator),
@@ -2671,14 +2672,15 @@
     @BinderThread
     @SuppressWarnings("deprecation")
     private void setImeWindowStatus(@NonNull IBinder token, int vis, int backDisposition,
-            @UserIdInt int userId) {
+            @NonNull UserData userData) {
         final int topFocusedDisplayId = mWindowManagerInternal.getTopFocusedDisplayId();
 
+        final int userId = userData.mUserId;
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token, userId)) {
+            if (!calledWithValidTokenLocked(token, userData)) {
                 return;
             }
-            final var bindingController = getInputMethodBindingController(userId);
+            final var bindingController = userData.mBindingController;
             // Skip update IME status when current token display is not same as focused display.
             // Note that we still need to update IME status when focusing external display
             // that does not support system decoration and fallback to show IME on default
@@ -2710,9 +2712,9 @@
 
     @BinderThread
     private void reportStartInput(@NonNull IBinder token, IBinder startInputToken,
-            @UserIdInt int userId) {
+            @NonNull UserData userData) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token, userId)) {
+            if (!calledWithValidTokenLocked(token, userData)) {
                 return;
             }
             final IBinder targetWindow = mImeTargetWindowMap.get(startInputToken);
@@ -2746,8 +2748,8 @@
 
     @GuardedBy("ImfLock.class")
     private void updateSystemUiLocked(int vis, int backDisposition, @UserIdInt int userId) {
-        final var bindingController = getInputMethodBindingController(userId);
         final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         final var curToken = bindingController.getCurToken();
         if (curToken == null) {
             return;
@@ -2845,11 +2847,11 @@
                 settings.putSelectedInputMethod(id);
             }
         }
-        final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         bindingController.setSelectedMethodId(id);
 
         // Also re-initialize controllers.
-        final var userData = getUserData(userId);
         userData.mSwitchingController.resetCircularListLocked(mContext, settings);
         userData.mHardwareKeyboardShortcutController.update(settings);
     }
@@ -2886,7 +2888,8 @@
             }
         }
 
-        final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         if (bindingController.getDeviceIdToShowIme() == DEVICE_ID_DEFAULT) {
             String ime = SecureSettingsWrapper.getString(
                     Settings.Secure.DEFAULT_INPUT_METHOD, null, userId);
@@ -2926,7 +2929,6 @@
             resetCurrentMethodAndClientLocked(UnbindReason.NO_IME, userId);
         }
 
-        final var userData = getUserData(userId);
         userData.mSwitchingController.resetCircularListLocked(mContext, settings);
         userData.mHardwareKeyboardShortcutController.update(settings);
         sendOnNavButtonFlagsChangedLocked(userData);
@@ -3408,8 +3410,8 @@
         mVisibilityStateComputer.requestImeVisibility(windowToken, true);
 
         // Ensure binding the connection when IME is going to show.
-        final var bindingController = getInputMethodBindingController(userId);
         final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         bindingController.setCurrentMethodVisible();
         final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         ImeTracker.forLogging().onCancelled(userData.mCurStatsToken,
@@ -3542,7 +3544,8 @@
     boolean hideCurrentInputLocked(IBinder windowToken, @NonNull ImeTracker.Token statsToken,
             @InputMethodManager.HideFlags int flags, @Nullable ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason, @UserIdInt int userId) {
-        final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         if (!mVisibilityStateComputer.canHideIme(statsToken, flags)) {
             return false;
         }
@@ -3555,7 +3558,6 @@
         // since Android Eclair.  That's why we need to accept IMM#hideSoftInput() even when only
         // IMMS#InputShown indicates that the software keyboard is shown.
         // TODO(b/246309664): Clean up IMMS#mImeWindowVis
-        final var userData = getUserData(userId);
         IInputMethodInvoker curMethod = bindingController.getCurMethod();
         final boolean shouldHideSoftInput = curMethod != null
                 && (isInputShownLocked()
@@ -3635,6 +3637,7 @@
             Slog.w(TAG, "User #" + userId + " is not running.");
             return InputBindResult.INVALID_USER;
         }
+        final var userData = getUserData(userId);
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
                     "IMMS.startInputOrWindowGainedFocus");
@@ -3642,7 +3645,7 @@
                     "InputMethodManagerService#startInputOrWindowGainedFocus", mDumper);
             final InputBindResult result;
             synchronized (ImfLock.class) {
-                final var bindingController = getInputMethodBindingController(userId);
+                final var bindingController = userData.mBindingController;
                 // If the system is not yet ready, we shouldn't be running third party code.
                 if (!mSystemReady) {
                     return new InputBindResult(
@@ -3707,7 +3710,6 @@
                     final boolean shouldClearFlag =
                             mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid);
                     final boolean showForced = mVisibilityStateComputer.mShowForced;
-                    final var userData = getUserData(userId);
                     if (userData.mImeBindingState.mFocusedWindow != windowToken
                             && showForced && shouldClearFlag) {
                         mVisibilityStateComputer.mShowForced = false;
@@ -3967,6 +3969,25 @@
         }
     }
 
+    @BinderThread
+    private void onImeSwitchButtonClickFromClient(@NonNull IBinder token, int displayId,
+            @NonNull UserData userData) {
+        synchronized (ImfLock.class) {
+            if (!calledWithValidTokenLocked(token, userData)) {
+                return;
+            }
+            showInputMethodPickerFromSystem(
+                    InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId);
+        }
+    }
+
+    @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @Override
+    public void onImeSwitchButtonClickFromSystem(int displayId) {
+        showInputMethodPickerFromSystem(
+                InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId);
+    }
+
     @NonNull
     private static IllegalArgumentException getExceptionForUnknownImeId(
             @Nullable String imeId) {
@@ -3974,10 +3995,11 @@
     }
 
     @BinderThread
-    private void setInputMethod(@NonNull IBinder token, String id, @UserIdInt int userId) {
+    private void setInputMethod(@NonNull IBinder token, String id, @NonNull UserData userData) {
         final int callingUid = Binder.getCallingUid();
+        final int userId = userData.mUserId;
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token, userId)) {
+            if (!calledWithValidTokenLocked(token, userData)) {
                 return;
             }
             final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
@@ -3992,10 +4014,11 @@
 
     @BinderThread
     private void setInputMethodAndSubtype(@NonNull IBinder token, String id,
-            InputMethodSubtype subtype, @UserIdInt int userId) {
+            InputMethodSubtype subtype, @NonNull UserData userData) {
         final int callingUid = Binder.getCallingUid();
+        final int userId = userData.mUserId;
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token, userId)) {
+            if (!calledWithValidTokenLocked(token, userData)) {
                 return;
             }
             final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
@@ -4008,18 +4031,20 @@
                 setInputMethodWithSubtypeIdLocked(token, id,
                         SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()), userId);
             } else {
-                setInputMethod(token, id, userId);
+                setInputMethod(token, id, userData);
             }
         }
     }
 
     @BinderThread
-    private boolean switchToPreviousInputMethod(@NonNull IBinder token, @UserIdInt int userId) {
+    private boolean switchToPreviousInputMethod(@NonNull IBinder token,
+            @NonNull UserData userData) {
+        final int userId = userData.mUserId;
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token, userId)) {
+            if (!calledWithValidTokenLocked(token, userData)) {
                 return false;
             }
-            final var bindingController = getInputMethodBindingController(userId);
+            final var bindingController = userData.mBindingController;
             final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
             final Pair<String, String> lastIme = settings.getLastInputMethodAndSubtype();
             final InputMethodInfo lastImi;
@@ -4096,43 +4121,45 @@
 
     @BinderThread
     private boolean switchToNextInputMethod(@NonNull IBinder token, boolean onlyCurrentIme,
-            @UserIdInt int userId) {
+            @NonNull UserData userData) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token, userId)) {
+            if (!calledWithValidTokenLocked(token, userData)) {
                 return false;
             }
-            return switchToNextInputMethodLocked(token, onlyCurrentIme, userId);
+            return switchToNextInputMethodLocked(token, onlyCurrentIme, userData);
         }
     }
 
     @GuardedBy("ImfLock.class")
     private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme,
-            @UserIdInt int userId) {
-        final var bindingController = getInputMethodBindingController(userId);
+            @NonNull UserData userData) {
+        final var bindingController = userData.mBindingController;
         final var currentImi = bindingController.getSelectedMethod();
-        final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController
+        final ImeSubtypeListItem nextSubtype = userData.mSwitchingController
                 .getNextInputMethodLocked(onlyCurrentIme, currentImi,
-                        bindingController.getCurrentSubtype());
+                        bindingController.getCurrentSubtype(),
+                        MODE_AUTO, true /* forward */);
         if (nextSubtype == null) {
             return false;
         }
         setInputMethodWithSubtypeIdLocked(token, nextSubtype.mImi.getId(),
-                nextSubtype.mSubtypeId, userId);
+                nextSubtype.mSubtypeId, userData.mUserId);
         return true;
     }
 
     @BinderThread
     private boolean shouldOfferSwitchingToNextInputMethod(@NonNull IBinder token,
-            @UserIdInt int userId) {
+            @NonNull UserData userData) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token, userId)) {
+            if (!calledWithValidTokenLocked(token, userData)) {
                 return false;
             }
-            final var bindingController = getInputMethodBindingController(userId);
+            final var bindingController = userData.mBindingController;
             final var currentImi = bindingController.getSelectedMethod();
-            final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController
+            final ImeSubtypeListItem nextSubtype = userData.mSwitchingController
                     .getNextInputMethodLocked(false /* onlyCurrentIme */, currentImi,
-                            bindingController.getCurrentSubtype());
+                            bindingController.getCurrentSubtype(),
+                            MODE_AUTO, true /* forward */);
             return nextSubtype != null;
         }
     }
@@ -4530,8 +4557,8 @@
     private void dumpDebug(ProtoOutputStream proto, long fieldId) {
         synchronized (ImfLock.class) {
             final int userId = mCurrentUserId;
-            final var bindingController = getInputMethodBindingController(userId);
             final var userData = getUserData(userId);
+            final var bindingController = userData.mBindingController;
             final long token = proto.start(fieldId);
             proto.write(CUR_METHOD_ID, bindingController.getSelectedMethodId());
             proto.write(CUR_SEQ, bindingController.getSequenceNumber());
@@ -4562,12 +4589,12 @@
     }
 
     @BinderThread
-    private void notifyUserAction(@NonNull IBinder token, @UserIdInt int userId) {
+    private void notifyUserAction(@NonNull IBinder token, @NonNull UserData userData) {
         if (DEBUG) {
             Slog.d(TAG, "Got the notification of a user action.");
         }
         synchronized (ImfLock.class) {
-            final var bindingController = getInputMethodBindingController(userId);
+            final var bindingController = userData.mBindingController;
             if (bindingController.getCurToken() != token) {
                 if (DEBUG) {
                     Slog.d(TAG, "Ignoring the user action notification from IMEs that are no longer"
@@ -4577,7 +4604,7 @@
             }
             final InputMethodInfo imi = bindingController.getSelectedMethod();
             if (imi != null) {
-                getUserData(userId).mSwitchingController.onUserActionLocked(imi,
+                userData.mSwitchingController.onUserActionLocked(imi,
                         bindingController.getCurrentSubtype());
             }
         }
@@ -4585,11 +4612,12 @@
 
     @BinderThread
     private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible,
-            @NonNull ImeTracker.Token statsToken, @UserIdInt int userId) {
+            @NonNull ImeTracker.Token statsToken, @NonNull UserData userData) {
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility");
+            final int userId = userData.mUserId;
             synchronized (ImfLock.class) {
-                if (!calledWithValidTokenLocked(token, userId)) {
+                if (!calledWithValidTokenLocked(token, userData)) {
                     ImeTracker.forLogging().onFailed(statsToken,
                             ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                     return;
@@ -4664,8 +4692,8 @@
             @UserIdInt int userId) {
         final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(requestImeToken,
                 userId);
-        final var bindingController = getInputMethodBindingController(userId);
         final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         final WindowManagerInternal.ImeTargetInfo info =
                 mWindowManagerInternal.onToggleImeRequested(
                         show, userData.mImeBindingState.mFocusedWindow, requestToken,
@@ -4685,16 +4713,16 @@
     @BinderThread
     private void hideMySoftInput(@NonNull IBinder token, @NonNull ImeTracker.Token statsToken,
             @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason,
-            @UserIdInt int userId) {
+            @NonNull UserData userData) {
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput");
+            final int userId = userData.mUserId;
             synchronized (ImfLock.class) {
-                if (!calledWithValidTokenLocked(token, userId)) {
+                if (!calledWithValidTokenLocked(token, userData)) {
                     ImeTracker.forLogging().onFailed(statsToken,
                             ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                     return;
                 }
-                final var userData = getUserData(userId);
                 ImeTracker.forLogging().onProgress(statsToken,
                         ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                 final long ident = Binder.clearCallingIdentity();
@@ -4724,16 +4752,16 @@
     @BinderThread
     private void showMySoftInput(@NonNull IBinder token, @NonNull ImeTracker.Token statsToken,
             @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason,
-            @UserIdInt int userId) {
+            @NonNull UserData userData) {
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showMySoftInput");
+            final int userId = userData.mUserId;
             synchronized (ImfLock.class) {
-                if (!calledWithValidTokenLocked(token, userId)) {
+                if (!calledWithValidTokenLocked(token, userData)) {
                     ImeTracker.forLogging().onFailed(statsToken,
                             ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                     return;
                 }
-                final var userData = getUserData(userId);
                 ImeTracker.forLogging().onProgress(statsToken,
                         ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                 final long ident = Binder.clearCallingIdentity();
@@ -4778,8 +4806,7 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void setEnabledSessionLocked(SessionState session,
-            @NonNull UserDataRepository.UserData userData) {
+    void setEnabledSessionLocked(SessionState session, @NonNull UserData userData) {
         if (userData.mEnabledSession != session) {
             if (userData.mEnabledSession != null && userData.mEnabledSession.mSession != null) {
                 if (DEBUG) Slog.v(TAG, "Disabling: " + userData.mEnabledSession);
@@ -4798,7 +4825,7 @@
     @GuardedBy("ImfLock.class")
     void setEnabledSessionForAccessibilityLocked(
             SparseArray<AccessibilitySessionState> accessibilitySessions,
-            @NonNull UserDataRepository.UserData userData) {
+            @NonNull UserData userData) {
         // mEnabledAccessibilitySessions could the same object as accessibilitySessions.
         SparseArray<IAccessibilityInputMethodSession> disabledSessions = new SparseArray<>();
         for (int i = 0; i < userData.mEnabledAccessibilitySessions.size(); i++) {
@@ -5007,9 +5034,8 @@
             case MSG_START_HANDWRITING:
                 final var handwritingRequest = (HandwritingRequest) msg.obj;
                 synchronized (ImfLock.class) {
-                    final int userId = handwritingRequest.userId;
-                    final var bindingController = getInputMethodBindingController(userId);
-                    final var userData = getUserData(userId);
+                    final var userData = handwritingRequest.userData;
+                    final var bindingController = userData.mBindingController;
                     IInputMethodInvoker curMethod = bindingController.getCurMethod();
                     if (curMethod == null || userData.mImeBindingState.mFocusedWindow == null) {
                         return true;
@@ -5054,24 +5080,24 @@
         return false;
     }
 
-    private record HandwritingRequest(int requestId, int pid, @UserIdInt int userId) { }
+    private record HandwritingRequest(int requestId, int pid, @NonNull UserData userData) { }
 
     @BinderThread
-    private void onStylusHandwritingReady(int requestId, int pid, @UserIdInt int userId) {
+    private void onStylusHandwritingReady(int requestId, int pid, @NonNull UserData userData) {
         mHandler.obtainMessage(MSG_START_HANDWRITING,
-                new HandwritingRequest(requestId, pid, userId)).sendToTarget();
+                new HandwritingRequest(requestId, pid, userData)).sendToTarget();
     }
 
     private void handleSetInteractive(final boolean interactive) {
         synchronized (ImfLock.class) {
             // TODO(b/305849394): Support multiple IMEs.
             final int userId = mCurrentUserId;
-            final var bindingController = getInputMethodBindingController(userId);
+            final var userData = getUserData(userId);
+            final var bindingController = userData.mBindingController;
             mIsInteractive = interactive;
             updateSystemUiLocked(
                     interactive ? bindingController.getImeWindowVis() : 0,
                     bindingController.getBackDisposition(), userId);
-            final var userData = getUserData(userId);
             // Inform the current client of the change in active status
             if (userData.mCurClient == null || userData.mCurClient.mClient == null) {
                 return;
@@ -5303,7 +5329,7 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void sendOnNavButtonFlagsChangedLocked(@NonNull UserDataRepository.UserData userData) {
+    void sendOnNavButtonFlagsChangedLocked(@NonNull UserData userData) {
         final var bindingController = userData.mBindingController;
         final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         if (curMethod == null) {
@@ -5319,7 +5345,7 @@
         final boolean value =
                 InputMethodDrawsNavBarResourceMonitor.evaluate(mContext, profileParentId);
         final var profileUserIds = mUserManagerInternal.getProfileIds(profileParentId, false);
-        final ArrayList<UserDataRepository.UserData> updatedUsers = new ArrayList<>();
+        final ArrayList<UserData> updatedUsers = new ArrayList<>();
         for (int profileUserId : profileUserIds) {
             final var userData = getUserData(profileUserId);
             userData.mImeDrawsNavBar.set(value);
@@ -5457,6 +5483,10 @@
             // Set InputMethod here
             settings.putSelectedInputMethod(imi != null ? imi.getId() : "");
         }
+
+        if (Flags.imeSwitcherRevamp()) {
+            getUserData(userId).mSwitchingController.onInputMethodSubtypeChanged();
+        }
     }
 
     @GuardedBy("ImfLock.class")
@@ -5568,20 +5598,38 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private void switchKeyboardLayoutLocked(int direction, @UserIdInt int userId) {
+    private void switchKeyboardLayoutLocked(int direction, @NonNull UserData userData) {
+        final int userId = userData.mUserId;
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
 
-        final var bindingController = getInputMethodBindingController(userId);
+        final var bindingController = userData.mBindingController;
         final InputMethodInfo currentImi = settings.getMethodMap().get(
                 bindingController.getSelectedMethodId());
         if (currentImi == null) {
             return;
         }
-        final InputMethodSubtypeHandle currentSubtypeHandle =
-                InputMethodSubtypeHandle.of(currentImi, bindingController.getCurrentSubtype());
-        final InputMethodSubtypeHandle nextSubtypeHandle =
-                getUserData(userId).mHardwareKeyboardShortcutController.onSubtypeSwitch(
+        final var currentSubtype = bindingController.getCurrentSubtype();
+        final InputMethodSubtypeHandle nextSubtypeHandle;
+        if (Flags.imeSwitcherRevamp()) {
+            final var nextItem = userData.mSwitchingController
+                    .getNextInputMethodForHardware(
+                            false /* onlyCurrentIme */, currentImi, currentSubtype, MODE_AUTO,
+                            direction > 0 /* forward */);
+            if (nextItem == null) {
+                Slog.i(TAG, "Hardware keyboard switching shortcut,"
+                        + " next input method and subtype not found");
+                return;
+            }
+
+            final var nextSubtype = nextItem.mSubtypeId > NOT_A_SUBTYPE_ID
+                    ? nextItem.mImi.getSubtypeAt(nextItem.mSubtypeId) : null;
+            nextSubtypeHandle = InputMethodSubtypeHandle.of(nextItem.mImi, nextSubtype);
+        } else {
+            final InputMethodSubtypeHandle currentSubtypeHandle =
+                    InputMethodSubtypeHandle.of(currentImi, currentSubtype);
+            nextSubtypeHandle = userData.mHardwareKeyboardShortcutController.onSubtypeSwitch(
                         currentSubtypeHandle, direction > 0);
+        }
         if (nextSubtypeHandle == null) {
             return;
         }
@@ -5727,7 +5775,7 @@
                 if (displayId != bindingController.getCurTokenDisplayId()) {
                     return false;
                 }
-                curHostInputToken = getInputMethodBindingController(userId).getCurHostInputToken();
+                curHostInputToken = bindingController.getCurHostInputToken();
                 if (curHostInputToken == null) {
                     return false;
                 }
@@ -5783,8 +5831,8 @@
         public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
                 IAccessibilityInputMethodSession session, @UserIdInt int userId) {
             synchronized (ImfLock.class) {
-                final var bindingController = getInputMethodBindingController(userId);
                 final var userData = getUserData(userId);
+                final var bindingController = userData.mBindingController;
                 // TODO(b/305829876): Implement user ID verification
                 if (userData.mCurClient != null) {
                     clearClientSessionForAccessibilityLocked(userData.mCurClient,
@@ -5821,8 +5869,8 @@
         public void unbindAccessibilityFromCurrentClient(int accessibilityConnectionId,
                 @UserIdInt int userId) {
             synchronized (ImfLock.class) {
-                final var bindingController = getInputMethodBindingController(userId);
                 final var userData = getUserData(userId);
+                final var bindingController = userData.mBindingController;
                 // TODO(b/305829876): Implement user ID verification
                 if (userData.mCurClient != null) {
                     if (DEBUG) {
@@ -5866,14 +5914,14 @@
                 IBinder targetWindowToken) {
             synchronized (ImfLock.class) {
                 // TODO(b/305849394): Infer userId from displayId
-                switchKeyboardLayoutLocked(direction, mCurrentUserId);
+                switchKeyboardLayoutLocked(direction, getUserData(mCurrentUserId));
             }
         }
     }
 
     @BinderThread
     private IInputContentUriToken createInputContentUriToken(@Nullable IBinder token,
-            @Nullable Uri contentUri, @Nullable String packageName, @UserIdInt int imeUserId) {
+            @Nullable Uri contentUri, @Nullable String packageName, @NonNull UserData userData) {
         if (token == null) {
             throw new NullPointerException("token");
         }
@@ -5890,7 +5938,7 @@
 
         synchronized (ImfLock.class) {
             final int uid = Binder.getCallingUid();
-            final var bindingController = getInputMethodBindingController(imeUserId);
+            final var bindingController = userData.mBindingController;
             if (bindingController.getSelectedMethodId() == null) {
                 return null;
             }
@@ -5902,7 +5950,6 @@
             // We cannot simply distinguish a bad IME that reports an arbitrary package name from
             // an unfortunate IME whose internal state is already obsolete due to the asynchronous
             // nature of our system.  Let's compare it with our internal record.
-            final var userData = getUserData(imeUserId);
             final var curPackageName = userData.mCurEditorInfo != null
                     ? userData.mCurEditorInfo.packageName : null;
             if (!TextUtils.equals(curPackageName, packageName)) {
@@ -5914,7 +5961,7 @@
             final int appUserId = UserHandle.getUserId(userData.mCurClient.mUid);
             // This user ID may be invalid if "contentUri" embedded an invalid user ID.
             final int contentUriOwnerUserId = ContentProvider.getUserIdFromUri(contentUri,
-                    imeUserId);
+                    userData.mUserId);
             final Uri contentUriWithoutUserId = ContentProvider.getUriWithoutUserId(contentUri);
             // Note: InputContentUriTokenHandler.take() checks whether the IME (specified by "uid")
             // actually has the right to grant a read permission for "contentUriWithoutUserId" that
@@ -5929,12 +5976,11 @@
 
     @BinderThread
     private void reportFullscreenMode(@NonNull IBinder token, boolean fullscreen,
-            @UserIdInt int userId) {
+            @NonNull UserData userData) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token, userId)) {
+            if (!calledWithValidTokenLocked(token, userData)) {
                 return;
             }
-            final var userData = getUserData(userId);
             if (userData.mCurClient != null && userData.mCurClient.mClient != null) {
                 userData.mInFullscreenMode = fullscreen;
                 userData.mCurClient.mClient.reportFullscreenMode(fullscreen);
@@ -6061,8 +6107,8 @@
                 p.println("    pid=" + c.mPid);
             };
             mClientController.forAllClients(clientControllerDump);
-            final var bindingController = getInputMethodBindingController(mCurrentUserId);
-            p.println("  mCurrentUserId=" + mCurrentUserId);
+            final var bindingController = userData.mBindingController;
+            p.println("  mCurrentUserId=" + userData.mUserId);
             p.println("  mCurMethodId=" + bindingController.getSelectedMethodId());
             client = userData.mCurClient;
             p.println("  mCurClient=" + client + " mCurSeq="
@@ -6077,7 +6123,7 @@
 
             p.println("  mUserDataRepository=");
             // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
-            @SuppressWarnings("GuardedBy") Consumer<UserDataRepository.UserData> userDataDump =
+            @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump =
                     u -> {
                         p.println("    mUserId=" + u.mUserId);
                         p.println("      hasMainConnection="
@@ -6614,7 +6660,7 @@
                                     0 /* flags */,
                                     SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND, userId);
                         }
-                        final var bindingController = getInputMethodBindingController(userId);
+                        final var bindingController = userData.mBindingController;
                         bindingController.unbindCurrentMethod();
 
                         // Enable default IMEs, disable others
@@ -6762,26 +6808,26 @@
         private final InputMethodManagerService mImms;
         @NonNull
         private final IBinder mToken;
-        @UserIdInt
-        private final int mUserId;
+        @NonNull
+        private final UserData mUserData;
 
         InputMethodPrivilegedOperationsImpl(InputMethodManagerService imms,
-                @NonNull IBinder token, @UserIdInt int userId) {
+                @NonNull IBinder token, @NonNull UserData userData) {
             mImms = imms;
             mToken = token;
-            mUserId = userId;
+            mUserData = userData;
         }
 
         @BinderThread
         @Override
         public void setImeWindowStatusAsync(int vis, int backDisposition) {
-            mImms.setImeWindowStatus(mToken, vis, backDisposition, mUserId);
+            mImms.setImeWindowStatus(mToken, vis, backDisposition, mUserData);
         }
 
         @BinderThread
         @Override
         public void reportStartInputAsync(IBinder startInputToken) {
-            mImms.reportStartInput(mToken, startInputToken, mUserId);
+            mImms.reportStartInput(mToken, startInputToken, mUserData);
         }
 
         @BinderThread
@@ -6797,7 +6843,7 @@
             @SuppressWarnings("unchecked") final AndroidFuture<IBinder> typedFuture = future;
             try {
                 typedFuture.complete(mImms.createInputContentUriToken(
-                        mToken, contentUri, packageName, mUserId).asBinder());
+                        mToken, contentUri, packageName, mUserData).asBinder());
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
             }
@@ -6806,7 +6852,7 @@
         @BinderThread
         @Override
         public void reportFullscreenModeAsync(boolean fullscreen) {
-            mImms.reportFullscreenMode(mToken, fullscreen, mUserId);
+            mImms.reportFullscreenMode(mToken, fullscreen, mUserData);
         }
 
         @BinderThread
@@ -6814,7 +6860,7 @@
         public void setInputMethod(String id, AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
             try {
-                mImms.setInputMethod(mToken, id, mUserId);
+                mImms.setInputMethod(mToken, id, mUserData);
                 typedFuture.complete(null);
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
@@ -6827,7 +6873,7 @@
                 AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
             try {
-                mImms.setInputMethodAndSubtype(mToken, id, subtype, mUserId);
+                mImms.setInputMethodAndSubtype(mToken, id, subtype, mUserData);
                 typedFuture.complete(null);
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
@@ -6841,7 +6887,7 @@
                 AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
             try {
-                mImms.hideMySoftInput(mToken, statsToken, flags, reason, mUserId);
+                mImms.hideMySoftInput(mToken, statsToken, flags, reason, mUserData);
                 typedFuture.complete(null);
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
@@ -6855,7 +6901,7 @@
                 AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
             try {
-                mImms.showMySoftInput(mToken, statsToken, flags, reason, mUserId);
+                mImms.showMySoftInput(mToken, statsToken, flags, reason, mUserData);
                 typedFuture.complete(null);
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
@@ -6865,7 +6911,7 @@
         @BinderThread
         @Override
         public void updateStatusIconAsync(String packageName, @DrawableRes int iconId) {
-            mImms.updateStatusIcon(mToken, packageName, iconId, mUserId);
+            mImms.updateStatusIcon(mToken, packageName, iconId, mUserData);
         }
 
         @BinderThread
@@ -6873,7 +6919,7 @@
         public void switchToPreviousInputMethod(AndroidFuture future /* T=Boolean */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future;
             try {
-                typedFuture.complete(mImms.switchToPreviousInputMethod(mToken, mUserId));
+                typedFuture.complete(mImms.switchToPreviousInputMethod(mToken, mUserData));
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
             }
@@ -6886,7 +6932,7 @@
             @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future;
             try {
                 typedFuture.complete(mImms.switchToNextInputMethod(mToken, onlyCurrentIme,
-                        mUserId));
+                        mUserData));
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
             }
@@ -6897,7 +6943,8 @@
         public void shouldOfferSwitchingToNextInputMethod(AndroidFuture future /* T=Boolean */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future;
             try {
-                typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethod(mToken, mUserId));
+                typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethod(mToken,
+                        mUserData));
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
             }
@@ -6905,21 +6952,27 @@
 
         @BinderThread
         @Override
+        public void onImeSwitchButtonClickFromClient(int displayId) {
+            mImms.onImeSwitchButtonClickFromClient(mToken, displayId, mUserData);
+        }
+
+        @BinderThread
+        @Override
         public void notifyUserActionAsync() {
-            mImms.notifyUserAction(mToken, mUserId);
+            mImms.notifyUserAction(mToken, mUserData);
         }
 
         @BinderThread
         @Override
         public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible,
                 @NonNull ImeTracker.Token statsToken) {
-            mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken, mUserId);
+            mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken, mUserData);
         }
 
         @BinderThread
         @Override
         public void onStylusHandwritingReady(int requestId, int pid) {
-            mImms.onStylusHandwritingReady(requestId, pid, mUserId);
+            mImms.onStylusHandwritingReady(requestId, pid, mUserData);
         }
 
         @BinderThread
@@ -6932,12 +6985,12 @@
         @Override
         public void switchKeyboardLayoutAsync(int direction) {
             synchronized (ImfLock.class) {
-                if (!mImms.calledWithValidTokenLocked(mToken, mUserId)) {
+                if (!mImms.calledWithValidTokenLocked(mToken, mUserData)) {
                     return;
                 }
                 final long ident = Binder.clearCallingIdentity();
                 try {
-                    mImms.switchKeyboardLayoutLocked(direction, mUserId);
+                    mImms.switchKeyboardLayoutLocked(direction, mUserData);
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index bb1b9df..05cc598 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -16,6 +16,8 @@
 
 package com.android.server.inputmethod;
 
+import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -24,11 +26,14 @@
 import android.util.ArraySet;
 import android.util.Printer;
 import android.util.Slog;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -45,6 +50,34 @@
     private static final boolean DEBUG = false;
     private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
 
+    @IntDef(prefix = {"MODE_"}, value = {
+            MODE_STATIC,
+            MODE_RECENT,
+            MODE_AUTO
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SwitchMode {
+    }
+
+    /**
+     * Switch using the static order (the order of the given list of input methods and subtypes).
+     * This order is only set when given a new list, and never updated.
+     */
+    public static final int MODE_STATIC = 0;
+
+    /**
+     * Switch using the recency based order, going from most recent to least recent,
+     * updated on {@link #onUserActionLocked user action}.
+     */
+    public static final int MODE_RECENT = 1;
+
+    /**
+     * If there was a {@link #onUserActionLocked user action} since the last
+     * {@link #onInputMethodSubtypeChanged() switch}, and direction is forward,
+     * use {@link #MODE_RECENT}, otherwise use {@link #MODE_STATIC}.
+     */
+    public static final int MODE_AUTO = 2;
+
     public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> {
 
         @NonNull
@@ -117,20 +150,25 @@
             if (result != 0) {
                 return result;
             }
-            // Subtype that has the same locale of the system's has higher priority.
-            result = (mIsSystemLocale ? -1 : 0) - (other.mIsSystemLocale ? -1 : 0);
-            if (result != 0) {
-                return result;
+            if (!Flags.imeSwitcherRevamp()) {
+                // Subtype that has the same locale of the system's has higher priority.
+                result = (mIsSystemLocale ? -1 : 0) - (other.mIsSystemLocale ? -1 : 0);
+                if (result != 0) {
+                    return result;
+                }
+                // Subtype that has the same language of the system's has higher priority.
+                result = (mIsSystemLanguage ? -1 : 0) - (other.mIsSystemLanguage ? -1 : 0);
+                if (result != 0) {
+                    return result;
+                }
+                result = compareNullableCharSequences(mSubtypeName, other.mSubtypeName);
+                if (result != 0) {
+                    return result;
+                }
             }
-            // Subtype that has the same language of the system's has higher priority.
-            result = (mIsSystemLanguage ? -1 : 0) - (other.mIsSystemLanguage ? -1 : 0);
-            if (result != 0) {
-                return result;
-            }
-            result = compareNullableCharSequences(mSubtypeName, other.mSubtypeName);
-            if (result != 0) {
-                return result;
-            }
+            // This will no longer compare by subtype name, however as {@link Collections.sort} is
+            // guaranteed to be a stable sorting, this allows sorting by the IME name (and ID),
+            // while maintaining the order of subtypes (given by each IME) at the IME level.
             return mImi.getId().compareTo(other.mImi.getId());
         }
 
@@ -226,6 +264,59 @@
         return imList;
     }
 
+    @NonNull
+    private static List<ImeSubtypeListItem> getInputMethodAndSubtypeListForHardwareKeyboard(
+            @NonNull Context context, @NonNull InputMethodSettings settings) {
+        if (!Flags.imeSwitcherRevamp()) {
+            return new ArrayList<>();
+        }
+        final int userId = settings.getUserId();
+        final Context userAwareContext = context.getUserId() == userId
+                ? context
+                : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
+        final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag();
+
+        final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList();
+        if (imis.isEmpty()) {
+            Slog.w(TAG, "Enabled input method list is empty.");
+            return new ArrayList<>();
+        }
+
+        final ArrayList<ImeSubtypeListItem> imList = new ArrayList<>();
+        final int numImes = imis.size();
+        for (int i = 0; i < numImes; ++i) {
+            final InputMethodInfo imi = imis.get(i);
+            if (!imi.shouldShowInInputMethodPicker()) {
+                continue;
+            }
+            final var subtypes = settings.getEnabledInputMethodSubtypeList(imi, true);
+            final ArraySet<InputMethodSubtype> enabledSubtypeSet = new ArraySet<>(subtypes);
+            final CharSequence imeLabel = imi.loadLabel(userAwareContext.getPackageManager());
+            if (!subtypes.isEmpty()) {
+                final int subtypeCount = imi.getSubtypeCount();
+                if (DEBUG) {
+                    Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId());
+                }
+                for (int j = 0; j < subtypeCount; j++) {
+                    final InputMethodSubtype subtype = imi.getSubtypeAt(j);
+                    if (enabledSubtypeSet.contains(subtype)
+                            && subtype.isSuitableForPhysicalKeyboardLayoutMapping()) {
+                        final CharSequence subtypeLabel =
+                                subtype.overridesImplicitlyEnabledSubtype() ? null : subtype
+                                        .getDisplayName(userAwareContext, imi.getPackageName(),
+                                                imi.getServiceInfo().applicationInfo);
+                        imList.add(new ImeSubtypeListItem(imeLabel,
+                                subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr));
+                    }
+                }
+            } else {
+                imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null,
+                        mSystemLocaleStr));
+            }
+        }
+        return imList;
+    }
+
     private static int calculateSubtypeId(@NonNull InputMethodInfo imi,
             @Nullable InputMethodSubtype subtype) {
         return subtype != null ? SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode())
@@ -385,6 +476,132 @@
         }
     }
 
+    /**
+     * List container that allows getting the next item in either forwards or backwards direction,
+     * in either static or recency order, and either in the same IME or not.
+     */
+    private static class RotationList {
+
+        /**
+         * List of items in a static order.
+         */
+        @NonNull
+        private final List<ImeSubtypeListItem> mItems;
+
+        /**
+         * Mapping of recency index to static index (in {@link #mItems}), with lower indices being
+         * more recent.
+         */
+        @NonNull
+        private final int[] mRecencyMap;
+
+        RotationList(@NonNull List<ImeSubtypeListItem> items) {
+            mItems = items;
+            mRecencyMap = new int[items.size()];
+            for (int i = 0; i < mItems.size(); i++) {
+                mRecencyMap[i] = i;
+            }
+        }
+
+        /**
+         * Gets the next input method and subtype from the given ones.
+         *
+         * @param imi            the input method to find the next value from.
+         * @param subtype        the input method subtype to find the next value from, if any.
+         * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+         * @param useRecency     whether to use the recency order, or the static order.
+         * @param forward        whether to search forwards to backwards in the list.
+         * @return the next input method and subtype if found, otherwise {@code null}.
+         */
+        @Nullable
+        public ImeSubtypeListItem next(@NonNull InputMethodInfo imi,
+                @Nullable InputMethodSubtype subtype, boolean onlyCurrentIme,
+                boolean useRecency, boolean forward) {
+            final int size = mItems.size();
+            if (size <= 1) {
+                return null;
+            }
+            final int index = getIndex(imi, subtype, useRecency);
+            if (index < 0) {
+                return null;
+            }
+
+            final int incrementSign = (forward ? 1 : -1);
+
+            for (int i = 1; i < size; i++) {
+                final int nextIndex = (index + i * incrementSign + size) % size;
+                final int mappedIndex = useRecency ? mRecencyMap[nextIndex] : nextIndex;
+                final var nextItem = mItems.get(mappedIndex);
+                if (!onlyCurrentIme || nextItem.mImi.equals(imi)) {
+                    return nextItem;
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Sets the given input method and subtype as the most recent one.
+         *
+         * @param imi     the input method to set as the most recent.
+         * @param subtype the input method subtype to set as the most recent, if any.
+         * @return {@code true} if the recency was updated, otherwise {@code false}.
+         */
+        public boolean setMostRecent(@NonNull InputMethodInfo imi,
+                @Nullable InputMethodSubtype subtype) {
+            if (mItems.size() <= 1) {
+                return false;
+            }
+
+            final int recencyIndex = getIndex(imi, subtype, true /* useRecency */);
+            if (recencyIndex <= 0) {
+                // Already most recent or not found.
+                return false;
+            }
+            final int staticIndex = mRecencyMap[recencyIndex];
+            System.arraycopy(mRecencyMap, 0, mRecencyMap, 1, recencyIndex);
+            mRecencyMap[0] = staticIndex;
+            return true;
+        }
+
+        /**
+         * Gets the index of the given input method and subtype, in either recency or static order.
+         *
+         * @param imi        the input method to get the index of.
+         * @param subtype    the input method subtype to get the index of, if any.
+         * @param useRecency whether to get the index in the recency or static order.
+         * @return an index in either {@link #mItems} or {@link #mRecencyMap}, or {@code -1}
+         * if not found.
+         */
+        @IntRange(from = -1)
+        private int getIndex(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+                boolean useRecency) {
+            final int subtypeIndex = calculateSubtypeId(imi, subtype);
+            for (int i = 0; i < mItems.size(); i++) {
+                final int mappedIndex = useRecency ? mRecencyMap[i] : i;
+                final var item = mItems.get(mappedIndex);
+                if (item.mImi.equals(imi) && item.mSubtypeId == subtypeIndex) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        /** Dumps the state of the list into the given printer. */
+        private void dump(@NonNull Printer pw, @NonNull String prefix) {
+            pw.println(prefix + "Static order:");
+            for (int i = 0; i < mItems.size(); ++i) {
+                final var item = mItems.get(i);
+                pw.println(prefix + "i=" + i + " item=" + item);
+            }
+            pw.println(prefix + "Recency order:");
+            for (int i = 0; i < mRecencyMap.length; ++i) {
+                final int index = mRecencyMap[i];
+                final var item = mItems.get(index);
+                pw.println(prefix + "i=" + i + " item=" + item);
+            }
+        }
+    }
+
     @VisibleForTesting
     public static class ControllerImpl {
 
@@ -392,10 +609,23 @@
         private final DynamicRotationList mSwitchingAwareRotationList;
         @NonNull
         private final StaticRotationList mSwitchingUnawareRotationList;
+        /** List of input methods and subtypes. */
+        @Nullable
+        private final RotationList mRotationList;
+        /** List of input methods and subtypes suitable for hardware keyboards. */
+        @Nullable
+        private final RotationList mHardwareRotationList;
+
+        /**
+         * Whether there was a user action since the last input method and subtype switch.
+         * Used to determine the switching behaviour for {@link #MODE_AUTO}.
+         */
+        private boolean mUserActionSinceSwitch;
 
         @NonNull
         public static ControllerImpl createFrom(@Nullable ControllerImpl currentInstance,
-                @NonNull List<ImeSubtypeListItem> sortedEnabledItems) {
+                @NonNull List<ImeSubtypeListItem> sortedEnabledItems,
+                @NonNull List<ImeSubtypeListItem> hardwareKeyboardItems) {
             final var switchingAwareImeSubtypes = filterImeSubtypeList(sortedEnabledItems,
                     true /* supportsSwitchingToNextInputMethod */);
             final var switchingUnawareImeSubtypes = filterImeSubtypeList(sortedEnabledItems,
@@ -421,22 +651,55 @@
                 switchingUnawareRotationList = new StaticRotationList(switchingUnawareImeSubtypes);
             }
 
-            return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList);
+            final RotationList rotationList;
+            if (!Flags.imeSwitcherRevamp()) {
+                rotationList = null;
+            } else if (currentInstance != null && currentInstance.mRotationList != null
+                    && Objects.equals(
+                            currentInstance.mRotationList.mItems, sortedEnabledItems)) {
+                // Can reuse the current instance.
+                rotationList = currentInstance.mRotationList;
+            } else {
+                rotationList = new RotationList(sortedEnabledItems);
+            }
+
+            final RotationList hardwareRotationList;
+            if (!Flags.imeSwitcherRevamp()) {
+                hardwareRotationList = null;
+            } else if (currentInstance != null && currentInstance.mHardwareRotationList != null
+                    && Objects.equals(
+                            currentInstance.mHardwareRotationList.mItems, hardwareKeyboardItems)) {
+                // Can reuse the current instance.
+                hardwareRotationList = currentInstance.mHardwareRotationList;
+            } else {
+                hardwareRotationList = new RotationList(hardwareKeyboardItems);
+            }
+
+            return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList,
+                    rotationList, hardwareRotationList);
         }
 
         private ControllerImpl(@NonNull DynamicRotationList switchingAwareRotationList,
-                @NonNull StaticRotationList switchingUnawareRotationList) {
+                @NonNull StaticRotationList switchingUnawareRotationList,
+                @Nullable RotationList rotationList,
+                @Nullable RotationList hardwareRotationList) {
             mSwitchingAwareRotationList = switchingAwareRotationList;
             mSwitchingUnawareRotationList = switchingUnawareRotationList;
+            mRotationList = rotationList;
+            mHardwareRotationList = hardwareRotationList;
         }
 
         @Nullable
         public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme,
-                @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
+                @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+                @SwitchMode int mode, boolean forward) {
             if (imi == null) {
                 return null;
             }
-            if (imi.supportsSwitchingToNextInputMethod()) {
+            if (Flags.imeSwitcherRevamp() && mRotationList != null) {
+                return mRotationList.next(imi, subtype, onlyCurrentIme,
+                        isRecency(mode, forward), forward);
+            } else if (imi.supportsSwitchingToNextInputMethod()) {
                 return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
                         subtype);
             } else {
@@ -445,11 +708,66 @@
             }
         }
 
-        public void onUserActionLocked(@NonNull InputMethodInfo imi,
+        @Nullable
+        public ImeSubtypeListItem getNextInputMethodForHardware(boolean onlyCurrentIme,
+                @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+                @SwitchMode int mode, boolean forward) {
+            if (Flags.imeSwitcherRevamp() && mHardwareRotationList != null) {
+                return mHardwareRotationList.next(imi, subtype, onlyCurrentIme,
+                        isRecency(mode, forward), forward);
+            }
+            return null;
+        }
+
+        /**
+         * Called when the user took an action that should update the recency of the current
+         * input method and subtype in the switching list.
+         *
+         * @param imi     the currently selected input method.
+         * @param subtype the currently selected input method subtype, if any.
+         * @return {@code true} if the recency was updated, otherwise {@code false}.
+         * @see android.inputmethodservice.InputMethodServiceInternal#notifyUserActionIfNecessary()
+         */
+        public boolean onUserActionLocked(@NonNull InputMethodInfo imi,
                 @Nullable InputMethodSubtype subtype) {
-            if (imi.supportsSwitchingToNextInputMethod()) {
+            boolean recencyUpdated = false;
+            if (Flags.imeSwitcherRevamp()) {
+                if (mRotationList != null) {
+                    recencyUpdated |= mRotationList.setMostRecent(imi, subtype);
+                }
+                if (mHardwareRotationList != null) {
+                    recencyUpdated |= mHardwareRotationList.setMostRecent(imi, subtype);
+                }
+                if (recencyUpdated) {
+                    mUserActionSinceSwitch = true;
+                }
+            } else if (imi.supportsSwitchingToNextInputMethod()) {
                 mSwitchingAwareRotationList.onUserAction(imi, subtype);
             }
+            return recencyUpdated;
+        }
+
+        /** Called when the input method and subtype was changed. */
+        public void onInputMethodSubtypeChanged() {
+            mUserActionSinceSwitch = false;
+        }
+
+        /**
+         * Whether the given mode and direction result in recency or static order.
+         *
+         * <p>{@link #MODE_AUTO} resolves to the recency order for the first forwards switch
+         * after an {@link #onUserActionLocked user action}, and otherwise to the static order.</p>
+         *
+         * @param mode    the switching mode.
+         * @param forward the switching direction.
+         * @return {@code true} for the recency order, otherwise {@code false}.
+         */
+        private boolean isRecency(@SwitchMode int mode, boolean forward) {
+            if (mode == MODE_AUTO && mUserActionSinceSwitch && forward) {
+                return true;
+            } else {
+                return mode == MODE_RECENT;
+            }
         }
 
         @NonNull
@@ -473,6 +791,17 @@
             mSwitchingAwareRotationList.dump(pw, prefix + "  ");
             pw.println(prefix + "mSwitchingUnawareRotationList:");
             mSwitchingUnawareRotationList.dump(pw, prefix + "  ");
+            if (Flags.imeSwitcherRevamp()) {
+                if (mRotationList != null) {
+                    pw.println(prefix + "mRotationList:");
+                    mRotationList.dump(pw, prefix + "  ");
+                }
+                if (mHardwareRotationList != null) {
+                    pw.println(prefix + "mHardwareRotationList:");
+                    mHardwareRotationList.dump(pw, prefix + "  ");
+                }
+                pw.println("User action since last switch: " + mUserActionSinceSwitch);
+            }
         }
     }
 
@@ -480,26 +809,71 @@
     private ControllerImpl mController;
 
     InputMethodSubtypeSwitchingController() {
-        mController = ControllerImpl.createFrom(null, Collections.emptyList());
+        mController = ControllerImpl.createFrom(null, Collections.emptyList(),
+                Collections.emptyList());
     }
 
+    /**
+     * Called when the user took an action that should update the recency of the current
+     * input method and subtype in the switching list.
+     *
+     * @param imi     the currently selected input method.
+     * @param subtype the currently selected input method subtype, if any.
+     * @see android.inputmethodservice.InputMethodServiceInternal#notifyUserActionIfNecessary()
+     */
     public void onUserActionLocked(@NonNull InputMethodInfo imi,
             @Nullable InputMethodSubtype subtype) {
         mController.onUserActionLocked(imi, subtype);
     }
 
+    /** Called when the input method and subtype was changed. */
+    public void onInputMethodSubtypeChanged() {
+        mController.onInputMethodSubtypeChanged();
+    }
+
     public void resetCircularListLocked(@NonNull Context context,
             @NonNull InputMethodSettings settings) {
         mController = ControllerImpl.createFrom(mController,
                 getSortedInputMethodAndSubtypeList(
                         false /* includeAuxiliarySubtypes */, false /* isScreenLocked */,
-                        false /* forImeMenu */, context, settings));
+                        false /* forImeMenu */, context, settings),
+                getInputMethodAndSubtypeListForHardwareKeyboard(context, settings));
     }
 
+    /**
+     * Gets the next input method and subtype, starting from the given ones, in the given direction.
+     *
+     * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+     * @param imi            the input method to find the next value from.
+     * @param subtype        the input method subtype to find the next value from, if any.
+     * @param mode           the switching mode.
+     * @param forward        whether to search search forwards or backwards in the list.
+     * @return the next input method and subtype if found, otherwise {@code null}.
+     */
     @Nullable
     public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
-            @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
-        return mController.getNextInputMethod(onlyCurrentIme, imi, subtype);
+            @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+            @SwitchMode int mode, boolean forward) {
+        return mController.getNextInputMethod(onlyCurrentIme, imi, subtype, mode, forward);
+    }
+
+    /**
+     * Gets the next input method and subtype suitable for hardware keyboards, starting from the
+     * given ones, in the given direction.
+     *
+     * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+     * @param imi            the input method to find the next value from.
+     * @param subtype        the input method subtype to find the next value from, if any.
+     * @param mode           the switching mode
+     * @param forward        whether to search search forwards or backwards in the list.
+     * @return the next input method and subtype if found, otherwise {@code null}.
+     */
+    @Nullable
+    public ImeSubtypeListItem getNextInputMethodForHardware(boolean onlyCurrentIme,
+            @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+            @SwitchMode int mode, boolean forward) {
+        return mController.getNextInputMethodForHardware(onlyCurrentIme, imi, subtype, mode,
+                forward);
     }
 
     public void dump(@NonNull Printer pw, @NonNull String prefix) {
diff --git a/services/core/java/com/android/server/inputmethod/UserData.java b/services/core/java/com/android/server/inputmethod/UserData.java
new file mode 100644
index 0000000..ec5c9e6
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/UserData.java
@@ -0,0 +1,147 @@
+/*
+ * 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.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.util.SparseArray;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
+import android.window.ImeOnBackInvokedDispatcher;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Placeholder for all IMMS user specific fields */
+final class UserData {
+    @UserIdInt
+    final int mUserId;
+
+    @NonNull
+    final InputMethodBindingController mBindingController;
+
+    @NonNull
+    final InputMethodSubtypeSwitchingController mSwitchingController =
+            new InputMethodSubtypeSwitchingController();
+
+    @NonNull
+    final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController =
+            new HardwareKeyboardShortcutController();
+
+    /**
+     * Have we called mCurMethod.bindInput()?
+     */
+    @GuardedBy("ImfLock.class")
+    boolean mBoundToMethod = false;
+
+    /**
+     * Have we called bindInput() for accessibility services?
+     */
+    @GuardedBy("ImfLock.class")
+    boolean mBoundToAccessibility;
+
+    @GuardedBy("ImfLock.class")
+    @NonNull
+    ImeBindingState mImeBindingState = ImeBindingState.newEmptyState();
+
+    @GuardedBy("ImfLock.class")
+    @Nullable
+    ClientState mCurClient = null;
+
+    @GuardedBy("ImfLock.class")
+    boolean mInFullscreenMode;
+
+    /**
+     * The {@link IRemoteInputConnection} last provided by the current client.
+     */
+    @GuardedBy("ImfLock.class")
+    @Nullable
+    IRemoteInputConnection mCurInputConnection;
+
+    /**
+     * The {@link ImeOnBackInvokedDispatcher} last provided by the current client to
+     * receive {@link android.window.OnBackInvokedCallback}s forwarded from IME.
+     */
+    @GuardedBy("ImfLock.class")
+    @Nullable
+    ImeOnBackInvokedDispatcher mCurImeDispatcher;
+
+    /**
+     * The {@link IRemoteAccessibilityInputConnection} last provided by the current client.
+     */
+    @GuardedBy("ImfLock.class")
+    @Nullable
+    IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection;
+
+    /**
+     * The {@link EditorInfo} last provided by the current client.
+     */
+    @GuardedBy("ImfLock.class")
+    @Nullable
+    EditorInfo mCurEditorInfo;
+
+    /**
+     * The token tracking the current IME show request that is waiting for a connection to an
+     * IME, otherwise {@code null}.
+     */
+    @GuardedBy("ImfLock.class")
+    @Nullable
+    ImeTracker.Token mCurStatsToken;
+
+    /**
+     * Currently enabled session.
+     */
+    @GuardedBy("ImfLock.class")
+    @Nullable
+    InputMethodManagerService.SessionState mEnabledSession;
+
+    @GuardedBy("ImfLock.class")
+    @Nullable
+    SparseArray<InputMethodManagerService.AccessibilitySessionState>
+            mEnabledAccessibilitySessions = new SparseArray<>();
+
+    /**
+     * A per-user cache of {@link InputMethodSettings#getEnabledInputMethodsStr()}.
+     */
+    @GuardedBy("ImfLock.class")
+    @NonNull
+    String mLastEnabledInputMethodsStr = "";
+
+    /**
+     * {@code true} when the IME is responsible for drawing the navigation bar and its buttons.
+     */
+    @NonNull
+    final AtomicBoolean mImeDrawsNavBar = new AtomicBoolean();
+
+    /**
+     * Intended to be instantiated only from this file.
+     */
+    UserData(@UserIdInt int userId,
+            @NonNull InputMethodBindingController bindingController) {
+        mUserId = userId;
+        mBindingController = bindingController;
+    }
+
+    @Override
+    public String toString() {
+        return "UserData{" + "mUserId=" + mUserId + '}';
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
index 7c68d54..6f831cc 100644
--- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java
+++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
@@ -18,18 +18,11 @@
 
 import android.annotation.AnyThread;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.util.SparseArray;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.ImeTracker;
-import android.window.ImeOnBackInvokedDispatcher;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
-import com.android.internal.inputmethod.IRemoteInputConnection;
 
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.function.Consumer;
 import java.util.function.IntFunction;
@@ -87,120 +80,4 @@
             mUserDataLock.writeLock().unlock();
         }
     }
-
-    /** Placeholder for all IMMS user specific fields */
-    static final class UserData {
-        @UserIdInt
-        final int mUserId;
-
-        @NonNull
-        final InputMethodBindingController mBindingController;
-
-        @NonNull
-        final InputMethodSubtypeSwitchingController mSwitchingController;
-
-        @NonNull
-        final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
-
-        /**
-         * Have we called mCurMethod.bindInput()?
-         */
-        @GuardedBy("ImfLock.class")
-        boolean mBoundToMethod = false;
-
-        /**
-         * Have we called bindInput() for accessibility services?
-         */
-        @GuardedBy("ImfLock.class")
-        boolean mBoundToAccessibility;
-
-        @GuardedBy("ImfLock.class")
-        @NonNull
-        ImeBindingState mImeBindingState = ImeBindingState.newEmptyState();
-
-        @GuardedBy("ImfLock.class")
-        @Nullable
-        ClientState mCurClient = null;
-
-        @GuardedBy("ImfLock.class")
-        boolean mInFullscreenMode;
-
-        /**
-         * The {@link IRemoteInputConnection} last provided by the current client.
-         */
-        @GuardedBy("ImfLock.class")
-        @Nullable
-        IRemoteInputConnection mCurInputConnection;
-
-        /**
-         * The {@link ImeOnBackInvokedDispatcher} last provided by the current client to
-         * receive {@link android.window.OnBackInvokedCallback}s forwarded from IME.
-         */
-        @GuardedBy("ImfLock.class")
-        @Nullable
-        ImeOnBackInvokedDispatcher mCurImeDispatcher;
-
-        /**
-         * The {@link IRemoteAccessibilityInputConnection} last provided by the current client.
-         */
-        @GuardedBy("ImfLock.class")
-        @Nullable
-        IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection;
-
-        /**
-         * The {@link EditorInfo} last provided by the current client.
-         */
-        @GuardedBy("ImfLock.class")
-        @Nullable
-        EditorInfo mCurEditorInfo;
-
-        /**
-         * The token tracking the current IME show request that is waiting for a connection to an
-         * IME, otherwise {@code null}.
-         */
-        @GuardedBy("ImfLock.class")
-        @Nullable
-        ImeTracker.Token mCurStatsToken;
-
-        /**
-         * Currently enabled session.
-         */
-        @GuardedBy("ImfLock.class")
-        @Nullable
-        InputMethodManagerService.SessionState mEnabledSession;
-
-        @GuardedBy("ImfLock.class")
-        @Nullable
-        SparseArray<InputMethodManagerService.AccessibilitySessionState>
-                mEnabledAccessibilitySessions = new SparseArray<>();
-
-        /**
-         * A per-user cache of {@link InputMethodSettings#getEnabledInputMethodsStr()}.
-         */
-        @GuardedBy("ImfLock.class")
-        @NonNull
-        String mLastEnabledInputMethodsStr = "";
-
-        /**
-         * {@code true} when the IME is responsible for drawing the navigation bar and its buttons.
-         */
-        @NonNull
-        final AtomicBoolean mImeDrawsNavBar = new AtomicBoolean();
-
-        /**
-         * Intended to be instantiated only from this file.
-         */
-        private UserData(@UserIdInt int userId,
-                @NonNull InputMethodBindingController bindingController) {
-            mUserId = userId;
-            mBindingController = bindingController;
-            mSwitchingController = new InputMethodSubtypeSwitchingController();
-            mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController();
-        }
-
-        @Override
-        public String toString() {
-            return "UserData{" + "mUserId=" + mUserId + '}';
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 41aac32..770e12d 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -296,6 +296,12 @@
         return mInner.isInputMethodPickerShownForTest();
     }
 
+    @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @Override
+    public void onImeSwitchButtonClickFromSystem(int displayId) {
+        mInner.onImeSwitchButtonClickFromSystem(displayId);
+    }
+
     @Override
     public InputMethodSubtype getCurrentInputMethodSubtype(int userId) {
         return mInner.getCurrentInputMethodSubtype(userId);
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 4851a81..3d0b079 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -48,6 +48,7 @@
 import android.util.Slog;
 import android.util.Xml;
 
+import com.android.internal.annotations.KeepForWeakReference;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.FrameworkStatsLog;
@@ -100,6 +101,7 @@
 
     private LocaleManagerBackupHelper mBackupHelper;
 
+    @KeepForWeakReference
     private final PackageMonitor mPackageMonitor;
 
     private final Object mWriteLock = new Object();
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 9e53cc3..016abff 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -110,8 +110,8 @@
 import static android.service.notification.Adjustment.TYPE_SOCIAL_MEDIA;
 import static android.service.notification.Flags.callstyleCallbackApi;
 import static android.service.notification.Flags.notificationForceGrouping;
-import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners;
 import static android.service.notification.Flags.redactSensitiveNotificationsBigTextStyle;
+import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
@@ -248,7 +248,6 @@
 import android.content.pm.VersionedPackage;
 import android.content.res.Resources;
 import android.database.ContentObserver;
-import android.graphics.drawable.Icon;
 import android.metrics.LogMaker;
 import android.net.Uri;
 import android.os.Binder;
@@ -373,7 +372,6 @@
 import com.android.server.wm.BackgroundActivityStartCallback;
 import com.android.server.wm.WindowManagerInternal;
 
-import java.util.function.BiPredicate;
 import libcore.io.IoUtils;
 
 import org.json.JSONException;
@@ -4634,29 +4632,28 @@
 
         @Override
         public List<String> getPackagesBypassingDnd(int userId,
-                                                    boolean includeConversationChannels) {
+                boolean includeConversationChannels) {
             checkCallerIsSystem();
 
             final ArraySet<String> packageNames = new ArraySet<>();
 
-            for (int user : mUm.getProfileIds(userId, false)) {
-                List<PackageInfo> pkgs = mPackageManagerClient.getInstalledPackagesAsUser(0, user);
-                for (PackageInfo pi : pkgs) {
-                    String pkg = pi.packageName;
-                    // If any NotificationChannel for this package is bypassing, the
-                    // package is considered bypassing.
-                    for (NotificationChannel channel : getNotificationChannelsBypassingDnd(pkg,
-                            pi.applicationInfo.uid).getList()) {
-                        // Skips non-demoted conversation channels.
-                        if (!includeConversationChannels
-                                && !TextUtils.isEmpty(channel.getConversationId())
-                                && !channel.isDemoted()) {
-                            continue;
-                        }
-                        packageNames.add(pkg);
+            List<PackageInfo> pkgs = mPackageManagerClient.getInstalledPackagesAsUser(0, userId);
+            for (PackageInfo pi : pkgs) {
+                String pkg = pi.packageName;
+                // If any NotificationChannel for this package is bypassing, the
+                // package is considered bypassing.
+                for (NotificationChannel channel : getNotificationChannelsBypassingDnd(pkg,
+                        pi.applicationInfo.uid).getList()) {
+                    // Skips non-demoted conversation channels.
+                    if (!includeConversationChannels
+                            && !TextUtils.isEmpty(channel.getConversationId())
+                            && !channel.isDemoted()) {
+                        continue;
                     }
+                    packageNames.add(pkg);
                 }
             }
+
             return new ArrayList<String>(packageNames);
         }
 
diff --git a/services/core/java/com/android/server/notification/TimeToLiveHelper.java b/services/core/java/com/android/server/notification/TimeToLiveHelper.java
index a4460b2..cabe766 100644
--- a/services/core/java/com/android/server/notification/TimeToLiveHelper.java
+++ b/services/core/java/com/android/server/notification/TimeToLiveHelper.java
@@ -177,13 +177,16 @@
             if (ACTION.equals(action)) {
                 String timeoutKey = null;
                 synchronized (mLock) {
-                    Pair<Long, String> earliest = mKeys.first();
-                    String key = intent.getStringExtra(EXTRA_KEY);
-                    if (!earliest.second.equals(key)) {
-                        Slog.wtf(TAG, "Alarm triggered but wasn't the earliest we were tracking");
+                    if (!mKeys.isEmpty()) {
+                        Pair<Long, String> earliest = mKeys.first();
+                        String key = intent.getStringExtra(EXTRA_KEY);
+                        if (!earliest.second.equals(key)) {
+                            Slog.wtf(TAG,
+                                    "Alarm triggered but wasn't the earliest we were tracking");
+                        }
+                        removeMatchingEntry(key);
+                        timeoutKey = earliest.second;
                     }
-                    removeMatchingEntry(key);
-                    timeoutKey = earliest.second;
                 }
                 mNm.timeoutNotification(timeoutKey);
             }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 303371b..8d3f07e 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2261,6 +2261,12 @@
                         installRequest.getNewUsers());
                 mPm.updateSequenceNumberLP(ps, installRequest.getNewUsers());
                 mPm.updateInstantAppInstallerLocked(packageName);
+
+                // The installation is success, remove the split info copy stored in package
+                // setting for the downgrade version check of DELETE_KEEP_DATA and archived app
+                // cases.
+                ps.setSplitNames(null);
+                ps.setSplitRevisionCodes(null);
             }
             installRequest.onCommitFinished();
         }
diff --git a/services/core/java/com/android/server/pm/OWNERS b/services/core/java/com/android/server/pm/OWNERS
index c10dfcb..e8fc577 100644
--- a/services/core/java/com/android/server/pm/OWNERS
+++ b/services/core/java/com/android/server/pm/OWNERS
@@ -27,6 +27,7 @@
 per-file RestrictionsSet.java = file:MULTIUSER_AND_ENTERPRISE_OWNERS
 per-file UserRestriction* = file:MULTIUSER_AND_ENTERPRISE_OWNERS
 per-file User* = file:/MULTIUSER_OWNERS
+per-file BackgroundUser* = file:/MULTIUSER_OWNERS
 
 # security
 per-file KeySetHandle.java = cbrubaker@google.com, nnk@google.com
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index c3cac20..a1dffc6 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -1423,11 +1423,8 @@
      */
     public static void checkDowngrade(@NonNull PackageSetting before,
             @NonNull PackageInfoLite after) throws PackageManagerException {
-        if (after.getLongVersionCode() < before.getVersionCode()) {
-            throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
-                    "Update version code " + after.versionCode + " is older than current "
-                            + before.getVersionCode());
-        }
+        checkDowngrade(before.getVersionCode(), before.getBaseRevisionCode(),
+                before.getSplitNames(), before.getSplitRevisionCodes(), after);
     }
 
     /**
@@ -1436,28 +1433,35 @@
      */
     public static void checkDowngrade(@NonNull AndroidPackage before,
             @NonNull PackageInfoLite after) throws PackageManagerException {
-        if (after.getLongVersionCode() < before.getLongVersionCode()) {
+        checkDowngrade(before.getLongVersionCode(), before.getBaseRevisionCode(),
+                before.getSplitNames(), before.getSplitRevisionCodes(), after);
+    }
+
+    private static void checkDowngrade(long beforeVersionCode, int beforeBaseRevisionCode,
+            @NonNull String[] beforeSplitNames, @NonNull int[] beforeSplitRevisionCodes,
+            @NonNull PackageInfoLite after) throws PackageManagerException {
+        if (after.getLongVersionCode() < beforeVersionCode) {
             throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
                     "Update version code " + after.versionCode + " is older than current "
-                            + before.getLongVersionCode());
-        } else if (after.getLongVersionCode() == before.getLongVersionCode()) {
-            if (after.baseRevisionCode < before.getBaseRevisionCode()) {
+                            + beforeVersionCode);
+        } else if (after.getLongVersionCode() == beforeVersionCode) {
+            if (after.baseRevisionCode < beforeBaseRevisionCode) {
                 throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
                         "Update base revision code " + after.baseRevisionCode
-                                + " is older than current " + before.getBaseRevisionCode());
+                                + " is older than current " + beforeBaseRevisionCode);
             }
 
             if (!ArrayUtils.isEmpty(after.splitNames)) {
                 for (int i = 0; i < after.splitNames.length; i++) {
                     final String splitName = after.splitNames[i];
-                    final int j = ArrayUtils.indexOf(before.getSplitNames(), splitName);
+                    final int j = ArrayUtils.indexOf(beforeSplitNames, splitName);
                     if (j != -1) {
-                        if (after.splitRevisionCodes[i] < before.getSplitRevisionCodes()[j]) {
+                        if (after.splitRevisionCodes[i] < beforeSplitRevisionCodes[j]) {
                             throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
                                     "Update split " + splitName + " revision code "
                                             + after.splitRevisionCodes[i]
                                             + " is older than current "
-                                            + before.getSplitRevisionCodes()[j]);
+                                            + beforeSplitRevisionCodes[j]);
                         }
                     }
                 }
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 82df527..9f10e01 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -234,6 +234,22 @@
     @Nullable
     private byte[] mRestrictUpdateHash;
 
+    // This is the copy of the same data stored in AndroidPackage. It is not null if the
+    // AndroidPackage is deleted in cases of DELETE_KEEP_DATA. When AndroidPackage is not null,
+    // the field will be null, and the getter method will return the data from AndroidPackage
+    // instead.
+    @Nullable
+    private String[] mSplitNames;
+
+    // This is the copy of the same data stored in AndroidPackage. It is not null if the
+    // AndroidPackage is deleted in cases of DELETE_KEEP_DATA. When AndroidPackage is not null,
+    // the field will be null, and the getter method will return the data from AndroidPackage
+    // instead.
+    @Nullable
+    private int[] mSplitRevisionCodes;
+
+    private int mBaseRevisionCode;
+
     /**
      * Snapshot support.
      */
@@ -578,6 +594,62 @@
         return getBoolean(Booleans.DEBUGGABLE);
     }
 
+    /**
+     * @see AndroidPackage#getBaseRevisionCode
+     */
+    public PackageSetting setBaseRevisionCode(int value) {
+        mBaseRevisionCode = value;
+        onChanged();
+        return this;
+    }
+
+    /**
+     * @see AndroidPackage#getBaseRevisionCode
+     */
+    public int getBaseRevisionCode() {
+        return mBaseRevisionCode;
+    }
+
+    /**
+     * @see AndroidPackage#getSplitNames
+     */
+    public PackageSetting setSplitNames(String[] value) {
+        mSplitNames = value;
+        onChanged();
+        return this;
+    }
+
+    /**
+     * @see AndroidPackage#getSplitNames
+     */
+    @NonNull
+    public String[] getSplitNames() {
+        if (pkg != null) {
+            return pkg.getSplitNames();
+        }
+        return mSplitNames == null ? EmptyArray.STRING : mSplitNames;
+    }
+
+    /**
+     * @see AndroidPackage#getSplitRevisionCodes
+     */
+    public PackageSetting setSplitRevisionCodes(int[] value) {
+        mSplitRevisionCodes = value;
+        onChanged();
+        return this;
+    }
+
+    /**
+     * @see AndroidPackage#getSplitRevisionCodes
+     */
+    @NonNull
+    public int[] getSplitRevisionCodes() {
+        if (pkg != null) {
+            return pkg.getSplitRevisionCodes();
+        }
+        return mSplitRevisionCodes == null ? EmptyArray.INT : mSplitRevisionCodes;
+    }
+
     @Override
     public String toString() {
         return "PackageSetting{"
@@ -739,6 +811,11 @@
         mTargetSdkVersion = other.mTargetSdkVersion;
         mRestrictUpdateHash = other.mRestrictUpdateHash == null
                 ? null : other.mRestrictUpdateHash.clone();
+        mBaseRevisionCode = other.mBaseRevisionCode;
+        mSplitNames = other.mSplitNames != null
+                ? Arrays.copyOf(other.mSplitNames, other.mSplitNames.length) : null;
+        mSplitRevisionCodes = other.mSplitRevisionCodes != null
+                ? Arrays.copyOf(other.mSplitRevisionCodes, other.mSplitRevisionCodes.length) : null;
 
         usesSdkLibraries = other.usesSdkLibraries != null
                 ? Arrays.copyOf(other.usesSdkLibraries,
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 2f2c451..7afc358 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -432,6 +432,13 @@
                 }
                 deletedPs.setInstalled(/* installed= */ false, userId);
             }
+
+            // Preserve split apk information for downgrade check with DELETE_KEEP_DATA and archived
+            // app cases
+            if (deletedPkg.getSplitNames() != null) {
+                deletedPs.setSplitNames(deletedPkg.getSplitNames());
+                deletedPs.setSplitRevisionCodes(deletedPkg.getSplitRevisionCodes());
+            }
         }
 
         // make sure to preserve per-user installed state if this removal was just
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index d8ce38e..95561f5f 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -437,8 +437,9 @@
             pkgSetting.setIsOrphaned(true);
         }
 
-        // update debuggable to packageSetting
+        // update debuggable and BaseRevisionCode to packageSetting
         pkgSetting.setDebuggable(parsedPackage.isDebuggable());
+        pkgSetting.setBaseRevisionCode(parsedPackage.getBaseRevisionCode());
 
         // Take care of first install / last update times.
         final long scanFileTime = getLastModifiedTime(parsedPackage);
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 0d16b00..9177e2b 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -325,6 +325,7 @@
     private static final String TAG_MIME_TYPE = "mime-type";
     private static final String TAG_ARCHIVE_STATE = "archive-state";
     private static final String TAG_ARCHIVE_ACTIVITY_INFO = "archive-activity-info";
+    private static final String TAG_SPLIT_VERSION = "split-version";
 
     public static final String ATTR_NAME = "name";
     public static final String ATTR_PACKAGE = "package";
@@ -3261,6 +3262,9 @@
         if (pkg.isLoading()) {
             serializer.attributeBoolean(null, "isLoading", true);
         }
+        if (pkg.getBaseRevisionCode() != 0) {
+            serializer.attributeInt(null, "baseRevisionCode", pkg.getBaseRevisionCode());
+        }
         serializer.attributeFloat(null, "loadingProgress", pkg.getLoadingProgress());
         serializer.attributeLongHex(null, "loadingCompletedTime", pkg.getLoadingCompletedTime());
 
@@ -3289,7 +3293,12 @@
         writeUpgradeKeySetsLPr(serializer, pkg.getKeySetData());
         writeKeySetAliasesLPr(serializer, pkg.getKeySetData());
         writeMimeGroupLPr(serializer, pkg.getMimeGroups());
-
+        // If getPkg is not NULL, these values are from the getPkg. And these values are preserved
+        // for the downgrade check for DELETE_KEEP_DATA and archived app cases. If the getPkg is
+        // not NULL, we don't need to preserve it.
+        if (pkg.getPkg() == null) {
+            writeSplitVersionsLPr(serializer, pkg.getSplitNames(), pkg.getSplitRevisionCodes());
+        }
         serializer.endTag(null, "package");
     }
 
@@ -4071,6 +4080,7 @@
         byte[] restrictUpdateHash = null;
         boolean isScannedAsStoppedSystemApp = false;
         boolean isSdkLibrary = false;
+        int baseRevisionCode = 0;
         try {
             name = parser.getAttributeValue(null, ATTR_NAME);
             realName = parser.getAttributeValue(null, "realName");
@@ -4116,6 +4126,7 @@
             appMetadataFilePath = parser.getAttributeValue(null, "appMetadataFilePath");
             appMetadataSource = parser.getAttributeInt(null, "appMetadataSource",
                     PackageManager.APP_METADATA_SOURCE_UNKNOWN);
+            baseRevisionCode = parser.getAttributeInt(null, "baseRevisionCode", 0);
 
             isScannedAsStoppedSystemApp = parser.getAttributeBoolean(null,
                 "scannedAsStoppedSystemApp", false);
@@ -4269,6 +4280,7 @@
                     .setAppMetadataFilePath(appMetadataFilePath)
                     .setAppMetadataSource(appMetadataSource)
                     .setTargetSdkVersion(targetSdkVersion)
+                    .setBaseRevisionCode(baseRevisionCode)
                     .setRestrictUpdateHash(restrictUpdateHash)
                     .setScannedAsStoppedSystemApp(isScannedAsStoppedSystemApp);
             // Handle legacy string here for single-user mode
@@ -4374,6 +4386,8 @@
                     readUsesStaticLibLPw(parser, packageSetting);
                 } else if (tagName.equals(TAG_USES_SDK_LIB)) {
                     readUsesSdkLibLPw(parser, packageSetting);
+                } else if (tagName.equals(TAG_SPLIT_VERSION)) {
+                    readSplitVersionsLPw(parser, packageSetting);
                 } else {
                     PackageManagerService.reportSettingsProblem(Log.WARN,
                             "Unknown element under <package>: " + parser.getName());
@@ -4470,6 +4484,37 @@
         }
     }
 
+    private void readSplitVersionsLPw(TypedXmlPullParser parser, PackageSetting outPs)
+            throws IOException, XmlPullParserException {
+        String splitName = parser.getAttributeValue(null, ATTR_NAME);
+        int splitRevision = parser.getAttributeInt(null, ATTR_VERSION, -1);
+        if (splitName != null && splitRevision >= 0) {
+            outPs.setSplitNames(ArrayUtils.appendElement(String.class,
+                    outPs.getSplitNames(), splitName));
+            outPs.setSplitRevisionCodes(ArrayUtils.appendInt(
+                    outPs.getSplitRevisionCodes(), splitRevision));
+        }
+
+        XmlUtils.skipCurrentTag(parser);
+    }
+
+    private void writeSplitVersionsLPr(TypedXmlSerializer serializer, String[] splitNames,
+            int[] splitRevisionCodes) throws IOException {
+        if (ArrayUtils.isEmpty(splitNames) || ArrayUtils.isEmpty(splitRevisionCodes)
+                || splitNames.length != splitRevisionCodes.length) {
+            return;
+        }
+        final int libLength = splitNames.length;
+        for (int i = 0; i < libLength; i++) {
+            final String splitName = splitNames[i];
+            final int splitRevision = splitRevisionCodes[i];
+            serializer.startTag(null, TAG_SPLIT_VERSION);
+            serializer.attribute(null, ATTR_NAME, splitName);
+            serializer.attributeInt(null, ATTR_VERSION, splitRevision);
+            serializer.endTag(null, TAG_SPLIT_VERSION);
+        }
+    }
+
     private void readDisabledComponentsLPw(PackageSetting packageSetting, TypedXmlPullParser parser,
             int userId) throws IOException, XmlPullParserException {
         int outerDepth = parser.getDepth();
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 8dc9756..8419a60 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -933,8 +933,7 @@
         public void onWakeUp() {
             synchronized (mLock) {
                 if (shouldEnableWakeGestureLp()) {
-                    performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, false,
-                            "Wake Up");
+                    performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, "Wake Up");
                     mWindowWakeUpPolicy.wakeUpFromWakeGesture();
                 }
             }
@@ -1403,7 +1402,7 @@
                 break;
             case LONG_PRESS_POWER_GLOBAL_ACTIONS:
                 mPowerKeyHandled = true;
-                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false,
+                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON,
                         "Power - Long Press - Global Actions");
                 showGlobalActions();
                 break;
@@ -1415,14 +1414,14 @@
                 if (ActivityManager.isUserAMonkey()) {
                     break;
                 }
-                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false,
+                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON,
                         "Power - Long Press - Shut Off");
                 sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS);
                 mWindowManagerFuncs.shutdown(behavior == LONG_PRESS_POWER_SHUT_OFF);
                 break;
             case LONG_PRESS_POWER_GO_TO_VOICE_ASSIST:
                 mPowerKeyHandled = true;
-                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false,
+                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON,
                         "Power - Long Press - Go To Voice Assist");
                 // Some devices allow the voice assistant intent during setup (and use that intent
                 // to launch something else, like Settings). So we explicitly allow that via the
@@ -1431,7 +1430,7 @@
                 break;
             case LONG_PRESS_POWER_ASSISTANT:
                 mPowerKeyHandled = true;
-                performHapticFeedback(HapticFeedbackConstants.ASSISTANT_BUTTON, false,
+                performHapticFeedback(HapticFeedbackConstants.ASSISTANT_BUTTON,
                         "Power - Long Press - Go To Assistant");
                 final int powerKeyDeviceId = INVALID_INPUT_DEVICE_ID;
                 launchAssistAction(null, powerKeyDeviceId, eventTime,
@@ -1446,7 +1445,7 @@
                 break;
             case VERY_LONG_PRESS_POWER_GLOBAL_ACTIONS:
                 mPowerKeyHandled = true;
-                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false,
+                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON,
                         "Power - Very Long Press - Show Global Actions");
                 showGlobalActions();
                 break;
@@ -1599,8 +1598,7 @@
             case TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY:
                 mTalkbackShortcutController.toggleTalkback(mCurrentUserId);
                 if (mTalkbackShortcutController.isTalkBackShortcutGestureEnabled()) {
-                    performHapticFeedback(HapticFeedbackConstants.CONFIRM, /* always = */
-                            false, /* reason = */
+                    performHapticFeedback(HapticFeedbackConstants.CONFIRM,
                             "Stem primary - Triple Press - Toggle Accessibility");
                 }
                 break;
@@ -1771,7 +1769,7 @@
         @Override
         public void run() {
             mEndCallKeyHandled = true;
-            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, false,
+            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                     "End Call - Long Press - Show Global Actions");
             showGlobalActionsInternal();
         }
@@ -2087,8 +2085,7 @@
                 return;
             }
             mHomeConsumed = true;
-            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, false,
-                    "Home - Long Press");
+            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, "Home - Long Press");
             switch (mLongPressOnHomeBehavior) {
                 case LONG_PRESS_HOME_ALL_APPS:
                     if (mHasFeatureLeanback) {
@@ -2530,7 +2527,7 @@
                                 break;
                             case POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS:
                                 performHapticFeedback(
-                                        HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON, false,
+                                        HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON,
                                         "Power + Volume Up - Global Actions");
                                 showGlobalActions();
                                 mPowerKeyHandled = true;
@@ -5078,8 +5075,7 @@
         }
 
         if (useHapticFeedback) {
-            performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, false,
-                    "Virtual Key - Press");
+            performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, "Virtual Key - Press");
         }
 
         if (isWakeKey) {
@@ -5971,8 +5967,10 @@
     public void setSafeMode(boolean safeMode) {
         mSafeMode = safeMode;
         if (safeMode) {
-            performHapticFeedback(HapticFeedbackConstants.SAFE_MODE_ENABLED, true,
-                    "Safe Mode Enabled");
+            performHapticFeedback(Process.myUid(), mContext.getOpPackageName(),
+                    HapticFeedbackConstants.SAFE_MODE_ENABLED,
+                    "Safe Mode Enabled", HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
+                    0 /* privFlags */);
         }
     }
 
@@ -6441,9 +6439,9 @@
                 Settings.Global.THEATER_MODE_ON, 0) == 1;
     }
 
-    private boolean performHapticFeedback(int effectId, boolean always, String reason) {
+    private boolean performHapticFeedback(int effectId, String reason) {
         return performHapticFeedback(Process.myUid(), mContext.getOpPackageName(),
-            effectId, always, reason, false /* fromIme */);
+            effectId, reason, 0 /* flags */, 0 /* privFlags */);
     }
 
     @Override
@@ -6452,8 +6450,8 @@
     }
 
     @Override
-    public boolean performHapticFeedback(int uid, String packageName, int effectId,
-            boolean always, String reason, boolean fromIme) {
+    public boolean performHapticFeedback(int uid, String packageName, int effectId, String reason,
+            int flags, int privFlags) {
         if (!mVibrator.hasVibrator()) {
             return false;
         }
@@ -6464,7 +6462,7 @@
         }
         VibrationAttributes attrs =
                 mHapticFeedbackVibrationProvider.getVibrationAttributesForHapticFeedback(
-                        effectId, /* bypassVibrationIntensitySetting= */ always, fromIme);
+                        effectId, flags, privFlags);
         VibratorFrameworkStatsLogger.logPerformHapticsFeedbackIfKeyboard(uid, effectId);
         mVibrator.vibrate(uid, packageName, effect, reason, attrs);
         return true;
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 6c05d70..1b394f6 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -80,6 +80,7 @@
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
+import android.view.HapticFeedbackConstants;
 import android.view.IDisplayFoldListener;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
@@ -1079,7 +1080,8 @@
      * Call from application to perform haptic feedback on its window.
      */
     public boolean performHapticFeedback(int uid, String packageName, int effectId,
-            boolean always, String reason, boolean fromIme);
+            String reason, @HapticFeedbackConstants.Flags int flags,
+            @HapticFeedbackConstants.PrivateFlags int privFlags);
 
     /**
      * Called when we have started keeping the screen on because a window
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index ce0120c..6fe1ccd 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -33,6 +33,7 @@
 
 import static com.android.internal.util.LatencyTracker.ACTION_TURN_ON_SCREEN;
 import static com.android.server.deviceidle.Flags.disableWakelocksInLightIdle;
+import static com.android.server.display.DisplayDeviceConfig.INVALID_BRIGHTNESS_IN_CONFIG;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -239,9 +240,6 @@
     // This should perhaps be a setting.
     private static final int SCREEN_BRIGHTNESS_BOOST_TIMEOUT = 5 * 1000;
 
-    // Float.NaN cannot be stored in config.xml so -2 is used instead
-    private static final float INVALID_BRIGHTNESS_IN_CONFIG = -2f;
-
     // How long a partial wake lock must be held until we consider it a long wake lock.
     static final long MIN_LONG_WAKE_CHECK_INTERVAL = 60*1000;
 
@@ -619,7 +617,6 @@
     public final float mScreenBrightnessMinimum;
     public final float mScreenBrightnessMaximum;
     public final float mScreenBrightnessDefault;
-    public final float mScreenBrightnessDoze;
     public final float mScreenBrightnessDim;
 
     // Value we store for tracking face down behavior.
@@ -1219,8 +1216,6 @@
                 .config_screenBrightnessSettingMaximumFloat);
         final float def = mContext.getResources().getFloat(com.android.internal.R.dimen
                 .config_screenBrightnessSettingDefaultFloat);
-        final float doze = mContext.getResources().getFloat(com.android.internal.R.dimen
-                .config_screenBrightnessDozeFloat);
         final float dim = mContext.getResources().getFloat(com.android.internal.R.dimen
                 .config_screenBrightnessDimFloat);
 
@@ -1240,13 +1235,6 @@
             mScreenBrightnessMaximum = max;
             mScreenBrightnessDefault = def;
         }
-        if (doze == INVALID_BRIGHTNESS_IN_CONFIG) {
-            mScreenBrightnessDoze = BrightnessSynchronizer.brightnessIntToFloat(
-                    mContext.getResources().getInteger(com.android.internal.R.integer
-                            .config_screenBrightnessDoze));
-        } else {
-            mScreenBrightnessDoze = doze;
-        }
         if (dim == INVALID_BRIGHTNESS_IN_CONFIG) {
             mScreenBrightnessDim = BrightnessSynchronizer.brightnessIntToFloat(
                     mContext.getResources().getInteger(com.android.internal.R.integer
@@ -6090,8 +6078,6 @@
                     return mScreenBrightnessDefault;
                 case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM:
                     return mScreenBrightnessDim;
-                case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE:
-                    return mScreenBrightnessDoze;
                 default:
                     return PowerManager.BRIGHTNESS_INVALID_FLOAT;
             }
diff --git a/services/core/java/com/android/server/stats/pull/RawSettingsTelemetryUtil.java b/services/core/java/com/android/server/stats/pull/RawSettingsTelemetryUtil.java
new file mode 100644
index 0000000..905f38a
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/RawSettingsTelemetryUtil.java
@@ -0,0 +1,83 @@
+/*
+ * 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.stats.pull;
+
+import static android.provider.Settings.System.PEAK_REFRESH_RATE;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.internal.display.RefreshRateSettingsUtils;
+
+/**
+ * Utility methods for processing raw settings values to the format that
+ * is acceptable for telemetry system.
+ * For instance, raw setting values could be hard to visualize on dashboards, etc.
+ */
+public final class RawSettingsTelemetryUtil {
+
+    private static final String TAG = "SettingsTelemetryUtil";
+
+    /**
+     * Get string that should be written as a value of settingKey and should be sent to telemetry
+     * system.
+     *
+     * @param context The context
+     * @param key The setting key
+     * @param value The setting raw value that was parsed from Settings.
+     * @return The setting string value that should be sent to telemetry system.
+     */
+    @Nullable
+    public static String getTelemetrySettingFromRawVal(Context context, String key, String value) {
+        if (key == null) {
+            return null;
+        }
+
+        if (key.equals(PEAK_REFRESH_RATE)) {
+            return getPeakRefreshRateSetting(context, value);
+        }
+
+        return value;
+    }
+
+    /**
+     * Get string that should be written as a value of "peak_refresh_setting" setting
+     * and should be sent to telemetry.
+     * system.
+     *
+     * @param context The context
+     * @param settingRawValue The setting raw value that was parsed from Settings.
+     * @return The "peak_refresh_setting" string value that should be sent to telemetry system.
+     */
+    @Nullable
+    private static String getPeakRefreshRateSetting(Context context, String settingRawValue) {
+        if (settingRawValue == null) {
+            Log.e(TAG, "PEAK_REFRESH_RATE value is null");
+            return null;
+        }
+
+        String floatInfinityStr = Float.toString(Float.POSITIVE_INFINITY);
+        if (settingRawValue.equals(floatInfinityStr)) {
+            float max_refresh_rate =
+                    RefreshRateSettingsUtils.findHighestRefreshRateAmongAllBuiltInDisplays(context);
+            return Float.toString(max_refresh_rate);
+        }
+
+        return settingRawValue;
+    }
+}
diff --git a/services/core/java/com/android/server/stats/pull/SettingsStatsUtil.java b/services/core/java/com/android/server/stats/pull/SettingsStatsUtil.java
index 7cdb84b..2a4a39c 100644
--- a/services/core/java/com/android/server/stats/pull/SettingsStatsUtil.java
+++ b/services/core/java/com/android/server/stats/pull/SettingsStatsUtil.java
@@ -107,7 +107,9 @@
             }
             for (String key : proto.element) {
                 final String value = Settings.System.getStringForUser(resolver, key, userId);
-                output.add(createStatsEvent(atomTag, key, value, userId,
+                final String telemetryValue =
+                        RawSettingsTelemetryUtil.getTelemetrySettingFromRawVal(context, key, value);
+                output.add(createStatsEvent(atomTag, key, telemetryValue, userId,
                         flagsData.mDataType));
             }
         }
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 dfccd1a..bca81f52 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -1283,7 +1283,7 @@
             }
             case FrameworkStatsLog.PROXY_BYTES_TRANSFER_BY_FG_BG: {
                 final NetworkStats stats = getUidNetworkStatsSnapshotForTemplate(
-                        new NetworkTemplate.Builder(MATCH_PROXY).build(),  /*includeTags=*/true);
+                        new NetworkTemplate.Builder(MATCH_PROXY).build(),  /*includeTags=*/false);
                 if (stats != null) {
                     ret.add(new NetworkStatsExt(sliceNetworkStatsByUidTagAndMetered(stats),
                             new int[]{TRANSPORT_BLUETOOTH},
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
index 8138168..98a2ba0d 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
@@ -186,13 +186,13 @@
      *
      * @param effectId the haptic feedback effect ID whose respective vibration attributes we want
      *      to get.
-     * @param bypassVibrationIntensitySetting {@code true} if the returned attribute should bypass
-     *      vibration intensity settings. {@code false} otherwise.
-     * @param fromIme the haptic feedback is performed from an IME.
+     * @param flags Additional flags as per {@link HapticFeedbackConstants}.
+     * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}.
      * @return the {@link VibrationAttributes} that should be used for the provided haptic feedback.
      */
-    public VibrationAttributes getVibrationAttributesForHapticFeedback(
-            int effectId, boolean bypassVibrationIntensitySetting, boolean fromIme) {
+    public VibrationAttributes getVibrationAttributesForHapticFeedback(int effectId,
+            @HapticFeedbackConstants.Flags int flags,
+            @HapticFeedbackConstants.PrivateFlags int privFlags) {
         VibrationAttributes attrs;
         switch (effectId) {
             case HapticFeedbackConstants.EDGE_SQUEEZE:
@@ -208,7 +208,7 @@
                 break;
             case HapticFeedbackConstants.KEYBOARD_TAP:
             case HapticFeedbackConstants.KEYBOARD_RELEASE:
-                attrs = createKeyboardVibrationAttributes(fromIme);
+                attrs = createKeyboardVibrationAttributes(privFlags);
                 break;
             case HapticFeedbackConstants.BIOMETRIC_CONFIRM:
             case HapticFeedbackConstants.BIOMETRIC_REJECT:
@@ -218,18 +218,23 @@
                 attrs = TOUCH_VIBRATION_ATTRIBUTES;
         }
 
-        int flags = 0;
+        int vibFlags = 0;
+        boolean fromIme =
+                (privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) != 0;
+        boolean bypassVibrationIntensitySetting =
+                (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0;
         if (bypassVibrationIntensitySetting) {
-            flags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
+            vibFlags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
         }
         if (shouldBypassInterruptionPolicy(effectId)) {
-            flags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY;
+            vibFlags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY;
         }
         if (shouldBypassIntensityScale(effectId, fromIme)) {
-            flags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+            vibFlags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
         }
 
-        return flags == 0 ? attrs : new VibrationAttributes.Builder(attrs).setFlags(flags).build();
+        return vibFlags == 0 ? attrs : new VibrationAttributes.Builder(attrs)
+                .setFlags(vibFlags).build();
     }
 
     /**
@@ -373,12 +378,16 @@
         return false;
     }
 
-    private VibrationAttributes createKeyboardVibrationAttributes(boolean fromIme) {
-        // Use touch attribute when the keyboard category is disable or it's not from an IME.
-        if (!Flags.keyboardCategoryEnabled() || !fromIme) {
+    private VibrationAttributes createKeyboardVibrationAttributes(
+            @HapticFeedbackConstants.PrivateFlags int privFlags) {
+        // Use touch attribute when the keyboard category is disable.
+        if (!Flags.keyboardCategoryEnabled()) {
             return TOUCH_VIBRATION_ATTRIBUTES;
         }
-
+        // Use touch attribute when the haptic is not apply to IME.
+        if ((privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) == 0) {
+            return TOUCH_VIBRATION_ATTRIBUTES;
+        }
         return new VibrationAttributes.Builder(TOUCH_VIBRATION_ATTRIBUTES)
                 .setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
                 .build();
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 5c15ccb..4437a2d 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -65,6 +65,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
+import android.view.HapticFeedbackConstants;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -439,13 +440,13 @@
 
     @Override // Binder call
     public void performHapticFeedback(int uid, int deviceId, String opPkg, int constant,
-            boolean always, String reason, boolean fromIme) {
+            String reason, int flags, int privFlags) {
         // Note that the `performHapticFeedback` method does not take a token argument from the
         // caller, and instead, uses this service as the token. This is to mitigate performance
         // impact that would otherwise be caused due to marshal latency. Haptic feedback effects are
         // short-lived, so we don't need to cancel when the process dies.
-        performHapticFeedbackInternal(
-                uid, deviceId, opPkg, constant, always, reason, /* token= */ this, fromIme);
+        performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */
+                this, flags, privFlags);
     }
 
     /**
@@ -456,8 +457,8 @@
     @VisibleForTesting
     @Nullable
     HalVibration performHapticFeedbackInternal(
-            int uid, int deviceId, String opPkg, int constant, boolean always, String reason,
-            IBinder token, boolean fromIme) {
+            int uid, int deviceId, String opPkg, int constant, String reason,
+            IBinder token, int flags, int privFlags) {
         HapticFeedbackVibrationProvider hapticVibrationProvider = getHapticVibrationProvider();
         if (hapticVibrationProvider == null) {
             Slog.e(TAG, "performHapticFeedback; haptic vibration provider not ready.");
@@ -474,9 +475,8 @@
             return null;
         }
         CombinedVibration vib = CombinedVibration.createParallel(effect);
-        VibrationAttributes attrs =
-                hapticVibrationProvider.getVibrationAttributesForHapticFeedback(
-                        constant, /* bypassVibrationIntensitySetting= */ always, fromIme);
+        VibrationAttributes attrs = hapticVibrationProvider.getVibrationAttributesForHapticFeedback(
+                constant, flags, privFlags);
         reason = "performHapticFeedback(constant=" + constant + "): " + reason;
         VibratorFrameworkStatsLogger.logPerformHapticsFeedbackIfKeyboard(uid, constant);
         return vibrateWithoutPermissionCheck(uid, deviceId, opPkg, vib, attrs, reason, token);
@@ -2295,10 +2295,11 @@
 
             IBinder deathBinder = commonOptions.background ? VibratorManagerService.this
                     : mShellCallbacksToken;
+            int flags = commonOptions.force
+                    ? HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING : 0;
             HalVibration vib = performHapticFeedbackInternal(Binder.getCallingUid(),
                     Context.DEVICE_ID_DEFAULT, SHELL_PACKAGE_NAME, constant,
-                    /* always= */ commonOptions.force, /* reason= */ commonOptions.description,
-                    deathBinder, false /* fromIme */);
+                    /* reason= */ commonOptions.description, deathBinder, flags, /* privFlags */ 0);
             maybeWaitOnVibration(vib, commonOptions);
 
             return 0;
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index 68a4172..2755a80 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -26,13 +26,14 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.CameraCompatTaskInfo;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.ProtoLogGroup;
 import com.android.window.flags.Flags;
 
 /**
@@ -56,6 +57,9 @@
 
     private boolean mIsCameraCompatTreatmentPending = false;
 
+    @Nullable
+    private Task mCameraTask;
+
     CameraCompatFreeformPolicy(@NonNull DisplayContent displayContent,
             @NonNull CameraStateMonitor cameraStateMonitor,
             @NonNull ActivityRefresher activityRefresher) {
@@ -116,6 +120,7 @@
         final int newCameraCompatMode = getCameraCompatMode(cameraActivity);
         if (newCameraCompatMode != existingCameraCompatMode) {
             mIsCameraCompatTreatmentPending = true;
+            mCameraTask = cameraActivity.getTask();
             cameraActivity.mAppCompatController.getAppCompatCameraOverrides()
                     .setFreeformCameraCompatMode(newCameraCompatMode);
             forceUpdateActivityAndTask(cameraActivity);
@@ -127,18 +132,22 @@
     }
 
     @Override
-    public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity,
-            @NonNull String cameraId) {
-        if (isActivityForCameraIdRefreshing(cameraId)) {
-            ProtoLog.v(ProtoLogGroup.WM_DEBUG_STATES,
-                    "Display id=%d is notified that Camera %s is closed but activity is"
-                            + " still refreshing. Rescheduling an update.",
-                    mDisplayContent.mDisplayId, cameraId);
-            return false;
+    public boolean onCameraClosed(@NonNull String cameraId) {
+        // Top activity in the same task as the camera activity, or `null` if the task is
+        // closed.
+        final ActivityRecord topActivity = mCameraTask != null
+                ? mCameraTask.getTopActivity(/* isFinishing */ false, /* includeOverlays */ false)
+                : null;
+        if (topActivity != null) {
+            if (isActivityForCameraIdRefreshing(topActivity, cameraId)) {
+                ProtoLog.v(ProtoLogGroup.WM_DEBUG_STATES,
+                        "Display id=%d is notified that Camera %s is closed but activity is"
+                                + " still refreshing. Rescheduling an update.",
+                        mDisplayContent.mDisplayId, cameraId);
+                return false;
+            }
         }
-        cameraActivity.mAppCompatController.getAppCompatCameraOverrides()
-                .setFreeformCameraCompatMode(CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE);
-        forceUpdateActivityAndTask(cameraActivity);
+        mCameraTask = null;
         mIsCameraCompatTreatmentPending = false;
         return true;
     }
@@ -186,10 +195,9 @@
                 && !activity.isEmbedded();
     }
 
-    private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) {
-        final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
-                /* considerKeyguardState= */ true);
-        if (topActivity == null || !isTreatmentEnabledForActivity(topActivity)
+    private boolean isActivityForCameraIdRefreshing(@NonNull ActivityRecord topActivity,
+            @NonNull String cameraId) {
+        if (!isTreatmentEnabledForActivity(topActivity)
                 || mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) {
             return false;
         }
diff --git a/services/core/java/com/android/server/wm/CameraStateMonitor.java b/services/core/java/com/android/server/wm/CameraStateMonitor.java
index a54141c..068fc00 100644
--- a/services/core/java/com/android/server/wm/CameraStateMonitor.java
+++ b/services/core/java/com/android/server/wm/CameraStateMonitor.java
@@ -61,9 +61,6 @@
     @NonNull
     private final Handler mHandler;
 
-    @Nullable
-    private ActivityRecord mCameraActivity;
-
     // Bi-directional map between package names and active camera IDs since we need to 1) get a
     // camera id by a package name when resizing the window; 2) get a package name by a camera id
     // when camera connection is closed and we need to clean up our records.
@@ -91,13 +88,13 @@
                 @Override
                 public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) {
                     synchronized (mWmService.mGlobalLock) {
-                        notifyCameraOpened(cameraId, packageId);
+                        notifyCameraOpenedWithDelay(cameraId, packageId);
                     }
                 }
                 @Override
                 public void onCameraClosed(@NonNull String cameraId) {
                     synchronized (mWmService.mGlobalLock) {
-                        notifyCameraClosed(cameraId);
+                        notifyCameraClosedWithDelay(cameraId);
                     }
                 }
             };
@@ -131,8 +128,8 @@
         mCameraStateListeners.remove(listener);
     }
 
-    private void notifyCameraOpened(
-            @NonNull String cameraId, @NonNull String packageName) {
+    private void notifyCameraOpenedWithDelay(@NonNull String cameraId,
+            @NonNull String packageName) {
         // If an activity is restarting or camera is flipping, the camera connection can be
         // quickly closed and reopened.
         mScheduledToBeRemovedCameraIdSet.remove(cameraId);
@@ -142,25 +139,30 @@
         // Some apps can’t handle configuration changes coming at the same time with Camera setup so
         // delaying orientation update to accommodate for that.
         mScheduledCompatModeUpdateCameraIdSet.add(cameraId);
-        mHandler.postDelayed(
-                () -> {
-                    synchronized (mWmService.mGlobalLock) {
-                        if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) {
-                            // Camera compat mode update has happened already or was cancelled
-                            // because camera was closed.
-                            return;
-                        }
-                        mCameraIdPackageBiMapping.put(packageName, cameraId);
-                        mCameraActivity = findCameraActivity(packageName);
-                        if (mCameraActivity == null || mCameraActivity.getTask() == null) {
-                            return;
-                        }
-                        notifyListenersCameraOpened(mCameraActivity, cameraId);
-                    }
-                },
+        mHandler.postDelayed(() -> notifyCameraOpenedInternal(cameraId, packageName),
                 CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS);
     }
 
+    private void notifyCameraOpenedInternal(@NonNull String cameraId, @NonNull String packageName) {
+        synchronized (mWmService.mGlobalLock) {
+            if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) {
+                // Camera compat mode update has happened already or was cancelled
+                // because camera was closed.
+                return;
+            }
+            mCameraIdPackageBiMapping.put(packageName, cameraId);
+            // If there are multiple activities of the same package name and none of
+            // them are the top running activity, we do not apply treatment (rather than
+            // guessing and applying it to the wrong activity).
+            final ActivityRecord cameraActivity =
+                    findUniqueActivityWithPackageName(packageName);
+            if (cameraActivity == null || cameraActivity.getTask() == null) {
+                return;
+            }
+            notifyListenersCameraOpened(cameraActivity, cameraId);
+        }
+    }
+
     private void notifyListenersCameraOpened(@NonNull ActivityRecord cameraActivity,
             @NonNull String cameraId) {
         for (int i = 0; i < mCameraStateListeners.size(); i++) {
@@ -174,7 +176,13 @@
         }
     }
 
-    private void notifyCameraClosed(@NonNull String cameraId) {
+    /**
+     * Processes camera closed, and schedules notifying listeners.
+     *
+     * <p>The delay is introduced to avoid flickering when switching between front and back camera,
+     * and when an activity is refreshed due to camera compat treatment.
+     */
+    private void notifyCameraClosedWithDelay(@NonNull String cameraId) {
         ProtoLog.v(WM_DEBUG_STATES,
                 "Display id=%d is notified that Camera %s is closed.",
                 mDisplayContent.mDisplayId, cameraId);
@@ -217,9 +225,10 @@
                 // Already reconnected to this camera, no need to clean up.
                 return;
             }
-            if (mCameraActivity != null && mCurrentListenerForCameraActivity != null) {
+
+            if (mCurrentListenerForCameraActivity != null) {
                 boolean closeSuccessful =
-                        mCurrentListenerForCameraActivity.onCameraClosed(mCameraActivity, cameraId);
+                        mCurrentListenerForCameraActivity.onCameraClosed(cameraId);
                 if (closeSuccessful) {
                     mCameraIdPackageBiMapping.removeCameraId(cameraId);
                     mCurrentListenerForCameraActivity = null;
@@ -231,8 +240,14 @@
     }
 
     // TODO(b/335165310): verify that this works in multi instance and permission dialogs.
+    /**
+     * Finds a visible activity with the given package name.
+     *
+     * <p>If there are multiple visible activities with a given package name, and none of them are
+     * the `topRunningActivity`, returns null.
+     */
     @Nullable
-    private ActivityRecord findCameraActivity(@NonNull String packageName) {
+    private ActivityRecord findUniqueActivityWithPackageName(@NonNull String packageName) {
         final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
                 /* considerKeyguardState= */ true);
         if (topActivity != null && topActivity.packageName.equals(packageName)) {
@@ -277,11 +292,11 @@
         // TODO(b/336474959): try to decouple `cameraId` from the listeners.
         boolean onCameraOpened(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId);
         /**
-         * Notifies the compat listener that an activity has closed the camera.
+         * Notifies the compat listener that camera is closed.
          *
          * @return true if cleanup has been successful - the notifier might try again if false.
          */
         // TODO(b/336474959): try to decouple `cameraId` from the listeners.
-        boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId);
+        boolean onCameraClosed(@NonNull String cameraId);
     }
 }
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index 50ac801..66653ca 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -164,17 +164,16 @@
     private void calculateAndCentreInitialBounds(Task task,
             LaunchParamsController.LaunchParams outParams) {
         // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
-        final Rect stableBounds = new Rect();
-        task.getDisplayArea().getStableRect(stableBounds);
+        final Rect screenBounds = task.getDisplayArea().getBounds();
         // The desired dimensions that a fully resizable window should take when initially entering
         // desktop mode. Calculated as a percentage of the available display area as defined by the
         // DESKTOP_MODE_INITIAL_BOUNDS_SCALE.
-        final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredWidth = (int) (screenBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight = (int) (screenBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
         outParams.mBounds.right = desiredWidth;
         outParams.mBounds.bottom = desiredHeight;
-        outParams.mBounds.offset(stableBounds.centerX() - outParams.mBounds.centerX(),
-                stableBounds.centerY() - outParams.mBounds.centerY());
+        outParams.mBounds.offset(screenBounds.centerX() - outParams.mBounds.centerX(),
+                screenBounds.centerY() - outParams.mBounds.centerY());
     }
 
     private void initLogBuilder(Task task, ActivityRecord activity) {
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 9998e1a..1a0124a 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -342,12 +342,19 @@
     }
 
     @Override
-    public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity,
-            @NonNull String cameraId) {
+    public boolean onCameraClosed(@NonNull String cameraId) {
+        // Top activity in the same task as the camera activity, or `null` if the task is
+        // closed.
+        final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+                /* considerKeyguardState= */ true);
+        if (topActivity == null) {
+            return true;
+        }
+
         synchronized (this) {
             // TODO(b/336474959): Once refresh is implemented in `CameraCompatFreeformPolicy`,
             // consider checking this in CameraStateMonitor before notifying the listeners (this).
-            if (isActivityForCameraIdRefreshing(cameraId)) {
+            if (isActivityForCameraIdRefreshing(topActivity, cameraId)) {
                 ProtoLog.v(WM_DEBUG_ORIENTATION,
                         "Display id=%d is notified that camera is closed but activity is"
                                 + " still refreshing. Rescheduling an update.",
@@ -355,15 +362,15 @@
                 return false;
             }
         }
+
         ProtoLog.v(WM_DEBUG_ORIENTATION,
                 "Display id=%d is notified that Camera is closed, updating rotation.",
                 mDisplayContent.mDisplayId);
-        final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
-                /* considerKeyguardState= */ true);
-        if (topActivity == null
-                // Checking whether an activity in fullscreen rather than the task as this
-                // camera compat treatment doesn't cover activity embedding.
-                || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+        // Checking whether an activity in fullscreen rather than the task as this camera compat
+        // treatment doesn't cover activity embedding.
+        // TODO(b/350495350): Consider checking whether this activity is the camera activity, or
+        // whether the top activity has the same task as the one which opened camera.
+        if (topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
             return true;
         }
         recomputeConfigurationForCameraCompatIfNeeded(topActivity);
@@ -372,14 +379,13 @@
     }
 
     // TODO(b/336474959): Do we need cameraId here?
-    private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) {
-        final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
-                /* considerKeyguardState= */ true);
-        if (!isTreatmentEnabledForActivity(topActivity)
-                || !mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) {
+    private boolean isActivityForCameraIdRefreshing(@NonNull ActivityRecord activity,
+            @NonNull String cameraId) {
+        if (!isTreatmentEnabledForActivity(activity)
+                || !mCameraStateMonitor.isCameraWithIdRunningForActivity(activity, cameraId)) {
             return false;
         }
-        return mActivityRefresher.isActivityRefreshing(topActivity);
+        return mActivityRefresher.isActivityRefreshing(activity);
     }
 
     private void recomputeConfigurationForCameraCompatIfNeeded(
diff --git a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
index bf99ccd..d79c11ce 100644
--- a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
+++ b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
@@ -408,9 +408,8 @@
             final boolean intersectsTopCutout = topDisplayCutout.intersects(
                     width - (windowWidth / 2), 0,
                     width + (windowWidth / 2), topDisplayCutout.bottom);
-            if (mClingWindow != null &&
-                    (windowWidth < 0 || (width > 0 && intersectsTopCutout))) {
-                final View iconView = mClingWindow.findViewById(R.id.immersive_cling_icon);
+            if (windowWidth < 0 || (width > 0 && intersectsTopCutout)) {
+                final View iconView = findViewById(R.id.immersive_cling_icon);
                 RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)
                         iconView.getLayoutParams();
                 lp.topMargin = topDisplayCutout.bottom;
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index f5108f5b..c26684f 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -324,19 +324,19 @@
     }
 
     @Override
-    public boolean performHapticFeedback(int effectId, boolean always, boolean fromIme) {
+    public boolean performHapticFeedback(int effectId, int flags, int privFlags) {
         final long ident = Binder.clearCallingIdentity();
         try {
-            return mService.mPolicy.performHapticFeedback(mUid, mPackageName,
-                        effectId, always, null, fromIme);
+            return mService.mPolicy.performHapticFeedback(mUid, mPackageName, effectId, null, flags,
+                    privFlags);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
     }
 
     @Override
-    public void performHapticFeedbackAsync(int effectId, boolean always, boolean fromIme) {
-        performHapticFeedback(effectId, always, fromIme);
+    public void performHapticFeedbackAsync(int effectId, int flags, int privFlags) {
+        performHapticFeedback(effectId, flags, privFlags);
     }
 
     /* Drag/drop */
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 2a3e945..f6a68d5 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.ActivityOptions.ANIM_CUSTOM;
 import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
@@ -1897,7 +1898,8 @@
         }
     }
 
-    private void overrideAnimationOptionsToInfoIfNecessary(@NonNull TransitionInfo info) {
+    @VisibleForTesting
+    void overrideAnimationOptionsToInfoIfNecessary(@NonNull TransitionInfo info) {
         if (mOverrideOptions == null) {
             return;
         }
@@ -1914,12 +1916,28 @@
                     changes.get(i).setAnimationOptions(mOverrideOptions);
                     // TODO(b/295805497): Extract mBackgroundColor from AnimationOptions.
                     changes.get(i).setBackgroundColor(mOverrideOptions.getBackgroundColor());
+                } else if (shouldApplyAnimOptionsToEmbeddedTf(container.asTaskFragment())) {
+                    // We only override AnimationOptions because backgroundColor should be from
+                    // TaskFragmentAnimationParams.
+                    changes.get(i).setAnimationOptions(mOverrideOptions);
                 }
             }
         }
         updateActivityTargetForCrossProfileAnimation(info);
     }
 
+    private boolean shouldApplyAnimOptionsToEmbeddedTf(@Nullable TaskFragment taskFragment) {
+        if (taskFragment == null || !taskFragment.isEmbedded()) {
+            return false;
+        }
+        if (taskFragment.getAnimationParams().hasOverrideAnimation()) {
+            // Always respect animation overrides from TaskFragmentAnimationParams.
+            return false;
+        }
+        // ActivityEmbedding animation adapter only support custom animation
+        return mOverrideOptions != null && mOverrideOptions.getType() == ANIM_CUSTOM;
+    }
+
     /**
      * Updates activity open target if {@link #mOverrideOptions} is
      * {@link ANIM_OPEN_CROSS_PROFILE_APPS}.
@@ -1929,8 +1947,7 @@
             return;
         }
         for (int i = 0; i < mTargets.size(); ++i) {
-            final ActivityRecord activity = mTargets.get(i).mContainer
-                    .asActivityRecord();
+            final ActivityRecord activity = mTargets.get(i).mContainer.asActivityRecord();
             final TransitionInfo.Change change = info.getChanges().get(i);
             if (activity == null || change.getMode() != TRANSIT_OPEN) {
                 continue;
@@ -2126,6 +2143,16 @@
     }
 
     /**
+
+     * Wallpaper will set itself as target if it wants to keep itself visible without a target.
+     */
+    private static boolean wallpaperIsOwnTarget(WallpaperWindowToken wallpaper) {
+        final WindowState target =
+                wallpaper.getDisplayContent().mWallpaperController.getWallpaperTarget();
+        return target != null && target.isDescendantOf(wallpaper);
+    }
+
+    /**
      * Reset waitingToshow for all wallpapers, and commit the visibility of the visible ones
      */
     private void commitVisibleWallpapers(SurfaceControl.Transaction t) {
@@ -2133,8 +2160,13 @@
         for (int i = mParticipants.size() - 1; i >= 0; --i) {
             final WallpaperWindowToken wallpaper = mParticipants.valueAt(i).asWallpaperToken();
             if (wallpaper != null) {
-                if (!wallpaper.isVisible() && wallpaper.isVisibleRequested()) {
+                if (!wallpaper.isVisible() && (wallpaper.isVisibleRequested()
+                        || (Flags.ensureWallpaperInTransitions() && showWallpaper))) {
                     wallpaper.commitVisibility(showWallpaper);
+                } else if (Flags.ensureWallpaperInTransitions() && wallpaper.isVisible()
+                        && !showWallpaper && !wallpaper.getDisplayContent().isKeyguardLocked()
+                        && !wallpaperIsOwnTarget(wallpaper)) {
+                    wallpaper.setVisibleRequested(false);
                 }
                 if (showWallpaper && Flags.ensureWallpaperInTransitions()
                         && wallpaper.isVisibleRequested()
@@ -2556,11 +2588,10 @@
             if (wc.asWindowState() != null) continue;
 
             final ChangeInfo changeInfo = changes.get(wc);
-            // Reject no-ops, unless wallpaper
-            if (!changeInfo.hasChanged()
-                    && (!Flags.ensureWallpaperInTransitions() || wc.asWallpaperToken() == null)) {
+            // Reject no-ops
+            if (!changeInfo.hasChanged()) {
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
-                        "  Rejecting as no-op: %s", wc);
+                        "  Rejecting as no-op: %s  vis: %b", wc, wc.isVisibleRequested());
                 continue;
             }
             targets.add(changeInfo);
diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java
index 3044abd..cdb14ab 100644
--- a/services/core/java/com/android/server/wm/TransparentPolicy.java
+++ b/services/core/java/com/android/server/wm/TransparentPolicy.java
@@ -162,10 +162,6 @@
         mTransparentPolicyState.clearInheritedCompatDisplayInsets();
     }
 
-    TransparentPolicyState getTransparentPolicyState() {
-        return mTransparentPolicyState;
-    }
-
     /**
      * In case of translucent activities, it consumes the {@link ActivityRecord} of the first opaque
      * activity beneath using the given consumer and returns {@code true}.
@@ -176,7 +172,7 @@
 
     @NonNull
     Optional<ActivityRecord> getFirstOpaqueActivity() {
-        return isRunning() ? Optional.of(mTransparentPolicyState.mFirstOpaqueActivity)
+        return isRunning() ? Optional.ofNullable(mTransparentPolicyState.mFirstOpaqueActivity)
                 : Optional.empty();
     }
 
@@ -216,10 +212,6 @@
                 SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
     }
 
-    private void inheritConfiguration(ActivityRecord firstOpaque) {
-        mTransparentPolicyState.inheritFromOpaque(firstOpaque);
-    }
-
     /**
      * Encapsulate the state for the current translucent activity when the transparent policy
      * has started.
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index ec7406a..4231149 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -179,6 +179,19 @@
                 <xs:element name="supportsVrr" type="xs:boolean" minOccurs="0">
                     <xs:annotation name="final"/>
                 </xs:element>
+                <!-- Table that translates doze brightness sensor values to brightness values in
+                the float scale [0, 1]; -1 means the current brightness should be kept.
+                The following formula should be used for conversion between nits and the float
+                scale: float = (nits - minNits) / (maxNits - minNits). minNits and maxNits are
+                defined in screenBrightnessMap. -->
+                <xs:element type="float-array" name="dozeBrightnessSensorValueToBrightness">
+                    <xs:annotation name="final"/>
+                </xs:element>
+                <!-- The default screen brightness in the scale [0, 1] to use while the device is
+                dozing. -->
+                <xs:element type="nonNegativeDecimal" name="defaultDozeBrightness">
+                    <xs:annotation name="final"/>
+                </xs:element>
             </xs:sequence>
         </xs:complexType>
     </xs:element>
@@ -859,6 +872,12 @@
         </xs:sequence>
     </xs:complexType>
 
+    <xs:complexType name="float-array">
+        <xs:sequence>
+            <xs:element name="item" type="nonNegativeDecimal" minOccurs="0" maxOccurs="unbounded"/>
+        </xs:sequence>
+    </xs:complexType>
+
     <xs:complexType name="usiVersion">
         <xs:element name="majorVersion" type="xs:nonNegativeInteger"
                     minOccurs="1" maxOccurs="1">
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 68d74cf..cec2787 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -125,9 +125,11 @@
     method public final java.math.BigInteger getAmbientLightHorizonLong();
     method public final java.math.BigInteger getAmbientLightHorizonShort();
     method public com.android.server.display.config.AutoBrightness getAutoBrightness();
+    method public final java.math.BigDecimal getDefaultDozeBrightness();
     method @Nullable public final com.android.server.display.config.DensityMapping getDensityMapping();
     method @NonNull public final com.android.server.display.config.Thresholds getDisplayBrightnessChangeThresholds();
     method public final com.android.server.display.config.Thresholds getDisplayBrightnessChangeThresholdsIdle();
+    method public final com.android.server.display.config.FloatArray getDozeBrightnessSensorValueToBrightness();
     method public final com.android.server.display.config.EvenDimmerMode getEvenDimmer();
     method @Nullable public final com.android.server.display.config.HdrBrightnessConfig getHdrBrightnessConfig();
     method public com.android.server.display.config.HighBrightnessMode getHighBrightnessMode();
@@ -163,9 +165,11 @@
     method public final void setAmbientLightHorizonLong(java.math.BigInteger);
     method public final void setAmbientLightHorizonShort(java.math.BigInteger);
     method public void setAutoBrightness(com.android.server.display.config.AutoBrightness);
+    method public final void setDefaultDozeBrightness(java.math.BigDecimal);
     method public final void setDensityMapping(@Nullable com.android.server.display.config.DensityMapping);
     method public final void setDisplayBrightnessChangeThresholds(@NonNull com.android.server.display.config.Thresholds);
     method public final void setDisplayBrightnessChangeThresholdsIdle(com.android.server.display.config.Thresholds);
+    method public final void setDozeBrightnessSensorValueToBrightness(com.android.server.display.config.FloatArray);
     method public final void setEvenDimmer(com.android.server.display.config.EvenDimmerMode);
     method public final void setHdrBrightnessConfig(@Nullable com.android.server.display.config.HdrBrightnessConfig);
     method public void setHighBrightnessMode(com.android.server.display.config.HighBrightnessMode);
@@ -214,6 +218,11 @@
     method public void setTransitionPoint(java.math.BigDecimal);
   }
 
+  public class FloatArray {
+    ctor public FloatArray();
+    method public java.util.List<java.math.BigDecimal> getItem();
+  }
+
   public class HbmTiming {
     ctor public HbmTiming();
     method @NonNull public final java.math.BigInteger getTimeMaxSecs_all();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e122fe0..032d6b5 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1131,9 +1131,9 @@
         }
 
         @Override
-        public void onUserUnlocked(@NonNull TargetUser user) {
-            if (user.isPreCreated()) return;
-            mService.handleOnUserUnlocked(user.getUserIdentifier());
+        public void onUserSwitching(@NonNull TargetUser from, @NonNull TargetUser to) {
+            if (to.isPreCreated()) return;
+            mService.handleOnUserSwitching(from.getUserIdentifier(), to.getUserIdentifier());
         }
     }
 
@@ -3831,8 +3831,8 @@
         mDevicePolicyEngine.handleUnlockUser(userId);
     }
 
-    void handleOnUserUnlocked(int userId) {
-        showNewUserDisclaimerIfNecessary(userId);
+    void handleOnUserSwitching(int fromUserId, int toUserId) {
+        showNewUserDisclaimerIfNecessary(toUserId);
     }
 
     void handleStopUser(int userId) {
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 8f630af..ce68b86 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -21,6 +21,7 @@
 import static com.android.compatibility.common.util.SystemUtil.eventually;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
@@ -33,18 +34,20 @@
 import android.graphics.Insets;
 import android.os.RemoteException;
 import android.provider.Settings;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
 import android.util.Log;
 import android.view.WindowManagerGlobal;
+import android.view.WindowManagerPolicyConstants;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
 
+import androidx.annotation.NonNull;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
 
 import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper;
 import com.android.apps.inputmethod.simpleime.testing.TestActivity;
@@ -66,6 +69,10 @@
     private static final String TAG = "SimpleIMSTest";
     private static final String INPUT_METHOD_SERVICE_NAME = ".SimpleInputMethodService";
     private static final String EDIT_TEXT_DESC = "Input box";
+    private static final String INPUT_METHOD_NAV_BACK_ID =
+            "android:id/input_method_nav_back";
+    private static final String INPUT_METHOD_NAV_IME_SWITCHER_ID =
+            "android:id/input_method_nav_ime_switcher";
     private static final long TIMEOUT_IN_SECONDS = 3;
     private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
             "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1";
@@ -697,6 +704,151 @@
         assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
     }
 
+    /**
+     * Verifies that clicking on the IME navigation bar back button hides the IME.
+     */
+    @Test
+    public void testBackButtonClick() throws Exception {
+        boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+                .hasNavigationBar(mInputMethodService.getDisplayId());
+        assumeTrue("Must have a navigation bar", hasNavigationBar);
+        assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled());
+
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        verifyInputViewStatusOnMainSync(
+                () -> {
+                    // Ensure the IME navigation bar and the IME switch button are drawn.
+                    mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+                            InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
+                                    | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
+                    );
+                    assertThat(mActivity.showImeWithWindowInsetsController()).isTrue();
+                },
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        final var backButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_BACK_ID);
+        backButtonUiObject.click();
+        mInstrumentation.waitForIdleSync();
+
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
+    }
+
+    /**
+     * Verifies that long clicking on the IME navigation bar back button hides the IME.
+     */
+    @Test
+    public void testBackButtonLongClick() throws Exception {
+        boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+                .hasNavigationBar(mInputMethodService.getDisplayId());
+        assumeTrue("Must have a navigation bar", hasNavigationBar);
+        assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled());
+
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        verifyInputViewStatusOnMainSync(
+                () -> {
+                    // Ensure the IME navigation bar and the IME switch button are drawn.
+                    mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+                            InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
+                                    | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
+                    );
+                    assertThat(mActivity.showImeWithWindowInsetsController()).isTrue();
+                },
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        final var backButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_BACK_ID);
+        backButtonUiObject.longClick();
+        mInstrumentation.waitForIdleSync();
+
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
+    }
+
+    /**
+     * Verifies that clicking on the IME switch button shows the Input Method Switcher Menu.
+     */
+    @Test
+    public void testImeSwitchButtonClick() throws Exception {
+        boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+                .hasNavigationBar(mInputMethodService.getDisplayId());
+        assumeTrue("Must have a navigation bar", hasNavigationBar);
+        assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled());
+
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        verifyInputViewStatusOnMainSync(
+                () -> {
+                    // Ensure the IME navigation bar and the IME switch button are drawn.
+                    mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+                            InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
+                                    | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
+                    );
+                    assertThat(mActivity.showImeWithWindowInsetsController()).isTrue();
+                },
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        final var imm = mContext.getSystemService(InputMethodManager.class);
+
+        final var imeSwitchButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_IME_SWITCHER_ID);
+        imeSwitchButtonUiObject.click();
+        mInstrumentation.waitForIdleSync();
+
+        assertWithMessage("Input Method Switcher Menu is shown")
+                .that(isInputMethodPickerShown(imm))
+                .isTrue();
+
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        // Hide the Picker menu before finishing.
+        mUiDevice.pressBack();
+    }
+
+    /**
+     * Verifies that long clicking on the IME switch button shows the Input Method Switcher Menu.
+     */
+    @Test
+    public void testImeSwitchButtonLongClick() throws Exception {
+        boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+                .hasNavigationBar(mInputMethodService.getDisplayId());
+        assumeTrue("Must have a navigation bar", hasNavigationBar);
+        assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled());
+
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        verifyInputViewStatusOnMainSync(
+                () -> {
+                    // Ensure the IME navigation bar and the IME switch button are drawn.
+                    mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+                            InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
+                                    | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
+                    );
+                    assertThat(mActivity.showImeWithWindowInsetsController()).isTrue();
+                },
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        final var imm = mContext.getSystemService(InputMethodManager.class);
+
+        final var imeSwitchButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_IME_SWITCHER_ID);
+        imeSwitchButtonUiObject.longClick();
+        mInstrumentation.waitForIdleSync();
+
+        assertWithMessage("Input Method Switcher Menu is shown")
+                .that(isInputMethodPickerShown(imm))
+                .isTrue();
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        // Hide the Picker menu before finishing.
+        mUiDevice.pressBack();
+    }
+
     private void verifyInputViewStatus(
             Runnable runnable, boolean expected, boolean inputViewStarted)
             throws InterruptedException {
@@ -844,6 +996,32 @@
         return SystemUtil.runShellCommandOrThrow(cmd);
     }
 
+    /**
+     * Checks if the Input Method Switcher Menu is shown. This runs by adopting the Shell's
+     * permission to ensure we have TEST_INPUT_METHOD permission.
+     */
+    private static boolean isInputMethodPickerShown(@NonNull InputMethodManager imm) {
+        return SystemUtil.runWithShellPermissionIdentity(imm::isInputMethodPickerShown);
+    }
+
+    @NonNull
+    private UiObject2 getUiObjectById(@NonNull String id) {
+        final var uiObject = mUiDevice.wait(
+                Until.findObject(By.res(id)),
+                TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS));
+        assertThat(uiObject).isNotNull();
+        return uiObject;
+    }
+
+    /**
+     * Returns {@code true} if the navigation mode is gesture nav, and {@code false} otherwise.
+     */
+    private boolean isGestureNavEnabled() {
+        return mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_navBarInteractionMode)
+                == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
+    }
+
     private void clickOnEditorText() {
         // Find the editText and click it.
         UiObject2 editTextUiObject =
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index e81cf9d..dc03732 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -16,15 +16,26 @@
 
 package com.android.server.inputmethod;
 
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_RECENT;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_STATIC;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.SwitchMode;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import android.content.ComponentName;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
@@ -35,6 +46,7 @@
 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ControllerImpl;
 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -51,6 +63,9 @@
     private static final String SYSTEM_LOCALE = "en_US";
     private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @NonNull
     private static InputMethodSubtype createTestSubtype(@NonNull String locale) {
         return new InputMethodSubtypeBuilder()
@@ -170,7 +185,7 @@
             subtype = createTestSubtype(currentItem.mSubtypeName.toString());
         }
         final ImeSubtypeListItem nextIme = controller.getNextInputMethod(onlyCurrentIme,
-                currentItem.mImi, subtype);
+                currentItem.mImi, subtype, MODE_STATIC, true /* forward */);
         assertEquals(nextItem, nextIme);
     }
 
@@ -185,15 +200,16 @@
         }
     }
 
-    private void onUserAction(@NonNull ControllerImpl controller,
+    private boolean onUserAction(@NonNull ControllerImpl controller,
             @NonNull ImeSubtypeListItem subtypeListItem) {
         InputMethodSubtype subtype = null;
         if (subtypeListItem.mSubtypeName != null) {
             subtype = createTestSubtype(subtypeListItem.mSubtypeName.toString());
         }
-        controller.onUserActionLocked(subtypeListItem.mImi, subtype);
+        return controller.onUserActionLocked(subtypeListItem.mImi, subtype);
     }
 
+    @RequiresFlagsDisabled(Flags.FLAG_IME_SWITCHER_REVAMP)
     @Test
     public void testControllerImpl() {
         final List<ImeSubtypeListItem> disabledItems = createDisabledImeSubtypes();
@@ -213,7 +229,7 @@
         final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
 
         final ControllerImpl controller = ControllerImpl.createFrom(
-                null /* currentInstance */, enabledItems);
+                null /* currentInstance */, enabledItems, new ArrayList<>());
 
         // switching-aware loop
         assertRotationOrder(controller, false /* onlyCurrentIme */,
@@ -257,6 +273,7 @@
                 disabledSubtypeUnawareIme, null);
     }
 
+    @RequiresFlagsDisabled(Flags.FLAG_IME_SWITCHER_REVAMP)
     @Test
     public void testControllerImplWithUserAction() {
         final List<ImeSubtypeListItem> enabledItems = createEnabledImeSubtypes();
@@ -270,7 +287,7 @@
         final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
 
         final ControllerImpl controller = ControllerImpl.createFrom(
-                null /* currentInstance */, enabledItems);
+                null /* currentInstance */, enabledItems, new ArrayList<>());
 
         // === switching-aware loop ===
         assertRotationOrder(controller, false /* onlyCurrentIme */,
@@ -320,7 +337,7 @@
         // Rotation order should be preserved when created with the same subtype list.
         final List<ImeSubtypeListItem> sameEnabledItems = createEnabledImeSubtypes();
         final ControllerImpl newController = ControllerImpl.createFrom(controller,
-                sameEnabledItems);
+                sameEnabledItems, new ArrayList<>());
         assertRotationOrder(newController, false /* onlyCurrentIme */,
                 subtypeAwareIme, latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
         assertRotationOrder(newController, false /* onlyCurrentIme */,
@@ -332,7 +349,7 @@
                 latinIme_en_us, latinIme_fr, subtypeAwareIme, switchingUnawareLatinIme_en_uk,
                 switchUnawareJapaneseIme_ja_jp, subtypeUnawareIme);
         final ControllerImpl anotherController = ControllerImpl.createFrom(controller,
-                differentEnabledItems);
+                differentEnabledItems, new ArrayList<>());
         assertRotationOrder(anotherController, false /* onlyCurrentIme */,
                 latinIme_en_us, latinIme_fr, subtypeAwareIme);
         assertRotationOrder(anotherController, false /* onlyCurrentIme */,
@@ -370,6 +387,7 @@
         assertFalse(item_en_us_allcaps.mIsSystemLanguage);
     }
 
+    @RequiresFlagsDisabled(Flags.FLAG_IME_SWITCHER_REVAMP)
     @SuppressWarnings("SelfComparison")
     @Test
     public void testImeSubtypeListComparator() {
@@ -471,4 +489,739 @@
             assertNotEquals(ime2, ime1);
         }
     }
+
+    /** Verifies the static mode. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testModeStatic() {
+        final var items = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+                List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+        addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+                null, true /* supportsSwitchingToNextInputMethod */);
+
+        final var english = items.get(0);
+        final var french = items.get(1);
+        final var italian = items.get(2);
+        final var simple = items.get(3);
+        final var latinIme = List.of(english, french, italian);
+        final var simpleIme = List.of(simple);
+
+        final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme",
+                List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+        addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme",
+                null, true /* supportsSwitchingToNextInputMethod */);
+
+        final var hardwareEnglish = hardwareItems.get(0);
+        final var hardwareFrench = hardwareItems.get(1);
+        final var hardwareItalian = hardwareItems.get(2);
+        final var hardwareSimple = hardwareItems.get(3);
+        final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian);
+        final var hardwareSimpleIme = List.of(hardwareSimple);
+
+        final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+                hardwareItems);
+
+        final int mode = MODE_STATIC;
+
+        // Static mode matches the given items order.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                items, List.of(latinIme, simpleIme));
+
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        // Set french IME as most recent.
+        assertTrue("Recency updated for french IME", onUserAction(controller, french));
+
+        // Static mode is not influenced by recency updates on non-hardware item.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                items, List.of(latinIme, simpleIme));
+
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        assertTrue("Recency updated for french hardware IME",
+                onUserAction(controller, hardwareFrench));
+
+        // Static mode is not influenced by recency updates on hardware item.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                items, List.of(latinIme, simpleIme));
+
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+    }
+
+    /** Verifies the recency mode. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testModeRecent() {
+        final var items = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+                List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+        addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+                null, true /* supportsSwitchingToNextInputMethod */);
+
+        final var english = items.get(0);
+        final var french = items.get(1);
+        final var italian = items.get(2);
+        final var simple = items.get(3);
+        final var latinIme = List.of(english, french, italian);
+        final var simpleIme = List.of(simple);
+
+        final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme",
+                List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+        addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme",
+                null, true /* supportsSwitchingToNextInputMethod */);
+
+        final var hardwareEnglish = hardwareItems.get(0);
+        final var hardwareFrench = hardwareItems.get(1);
+        final var hardwareItalian = hardwareItems.get(2);
+        final var hardwareSimple = hardwareItems.get(3);
+        final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian);
+        final var hardwareSimpleIme = List.of(hardwareSimple);
+
+        final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+                hardwareItems);
+
+        final int mode = MODE_RECENT;
+
+        // Recency order is initialized to static order.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                items, List.of(latinIme, simpleIme));
+
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        assertTrue("Recency updated for french IME", onUserAction(controller, french));
+        final var recencyItems = List.of(french, english, italian, simple);
+        final var recencyLatinIme = List.of(french, english, italian);
+        final var recencySimpleIme = List.of(simple);
+
+        // The order of non-hardware items is updated.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+        // The order of hardware items remains unchanged for an action on a non-hardware item.
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        assertFalse("Recency not updated again for same IME", onUserAction(controller, french));
+
+        // The order of non-hardware items remains unchanged.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+        // The order of hardware items remains unchanged.
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        assertTrue("Recency updated for french hardware IME",
+                onUserAction(controller, hardwareFrench));
+
+        final var recencyHardwareItems =
+                List.of(hardwareFrench, hardwareEnglish, hardwareItalian, hardwareSimple);
+        final var recencyHardwareLatinIme =
+                List.of(hardwareFrench, hardwareEnglish, hardwareItalian);
+        final var recencyHardwareSimpleIme = List.of(hardwareSimple);
+
+        // The order of non-hardware items is unchanged.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+        // The order of hardware items is updated.
+        assertNextOrder(controller, true /* forHardware */, mode,
+                recencyHardwareItems, List.of(recencyHardwareLatinIme, recencyHardwareSimpleIme));
+    }
+
+    /** Verifies the auto mode. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testModeAuto() {
+        final var items = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+                List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+        addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+                null, true /* supportsSwitchingToNextInputMethod */);
+
+        final var english = items.get(0);
+        final var french = items.get(1);
+        final var italian = items.get(2);
+        final var simple = items.get(3);
+        final var latinIme = List.of(english, french, italian);
+        final var simpleIme = List.of(simple);
+
+        final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme",
+                List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+        addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme",
+                null, true /* supportsSwitchingToNextInputMethod */);
+
+        final var hardwareEnglish = hardwareItems.get(0);
+        final var hardwareFrench = hardwareItems.get(1);
+        final var hardwareItalian = hardwareItems.get(2);
+        final var hardwareSimple = hardwareItems.get(3);
+        final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian);
+        final var hardwareSimpleIme = List.of(hardwareSimple);
+
+        final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+                hardwareItems);
+
+        final int mode = MODE_AUTO;
+
+        // Auto mode resolves to static order initially.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                items, List.of(latinIme, simpleIme));
+
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        // User action on french IME.
+        assertTrue("Recency updated for french IME", onUserAction(controller, french));
+
+        final var recencyItems = List.of(french, english, italian, simple);
+        final var recencyLatinIme = List.of(french, english, italian);
+        final var recencySimpleIme = List.of(simple);
+
+        // Auto mode resolves to recency order for the first forward after user action, and to
+        // static order for the backwards direction.
+        assertNextOrder(controller, false /* forHardware */, mode, true /* forward */,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+        assertNextOrder(controller, false /* forHardware */, mode, false /* forward */,
+                items.reversed(), List.of(latinIme.reversed(), simpleIme.reversed()));
+
+        // Auto mode resolves to recency order for the first forward after user action,
+        // but the recency was not updated for hardware items, so it's equivalent to static order.
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        // Change IME, reset user action having happened.
+        controller.onInputMethodSubtypeChanged();
+
+        // Auto mode resolves to static order as there was no user action since changing IMEs.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                items, List.of(latinIme, simpleIme));
+
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        // User action on french IME again.
+        assertFalse("Recency not updated again for same IME", onUserAction(controller, french));
+
+        // Auto mode still resolves to static order, as a user action on the currently most
+        // recent IME has no effect.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                items, List.of(latinIme, simpleIme));
+
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        // User action on hardware french IME.
+        assertTrue("Recency updated for french hardware IME",
+                onUserAction(controller, hardwareFrench));
+
+        final var recencyHardware =
+                List.of(hardwareFrench, hardwareEnglish, hardwareItalian, hardwareSimple);
+        final var recencyHardwareLatin =
+                List.of(hardwareFrench, hardwareEnglish, hardwareItalian);
+        final var recencyHardwareSimple = List.of(hardwareSimple);
+
+        // Auto mode resolves to recency order for the first forward direction after a user action
+        // on a hardware IME, and to static order for the backwards direction.
+        assertNextOrder(controller, false /* forHardware */, mode, true /* forward */,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+        assertNextOrder(controller, false /* forHardware */, mode, false /* forward */,
+                items.reversed(), List.of(latinIme.reversed(), simpleIme.reversed()));
+
+        assertNextOrder(controller, true /* forHardware */, mode, true /* forward */,
+                recencyHardware, List.of(recencyHardwareLatin, recencyHardwareSimple));
+
+        assertNextOrder(controller, true /* forHardware */, mode, false /* forward */,
+                hardwareItems.reversed(),
+                List.of(hardwareLatinIme.reversed(), hardwareSimpleIme.reversed()));
+    }
+
+    /**
+     * Verifies that the recency order is preserved only when updating with an equal list of items.
+     */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testUpdateList() {
+        final var items = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+                List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+        addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+                null, true /* supportsSwitchingToNextInputMethod */);
+
+        final var english = items.get(0);
+        final var french = items.get(1);
+        final var italian = items.get(2);
+        final var simple = items.get(3);
+
+        final var latinIme = List.of(english, french, italian);
+        final var simpleIme = List.of(simple);
+
+        final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme",
+                List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+        addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme",
+                null, true /* supportsSwitchingToNextInputMethod */);
+
+        final var hardwareEnglish = hardwareItems.get(0);
+        final var hardwareFrench = hardwareItems.get(1);
+        final var hardwareItalian = hardwareItems.get(2);
+        final var hardwareSimple = hardwareItems.get(3);
+
+        final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian);
+        final var hardwareSimpleIme = List.of(hardwareSimple);
+
+        final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+                hardwareItems);
+
+        final int mode = MODE_RECENT;
+
+        // Recency order is initialized to static order.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                items, List.of(latinIme, simpleIme));
+
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        // User action on french IME.
+        assertTrue("Recency updated for french IME", onUserAction(controller, french));
+
+        final var equalItems = new ArrayList<>(items);
+        final var otherItems = new ArrayList<>(items);
+        otherItems.remove(simple);
+
+        final var equalController = ControllerImpl.createFrom(controller, equalItems,
+                hardwareItems);
+        final var otherController = ControllerImpl.createFrom(controller, otherItems,
+                hardwareItems);
+
+        final var recencyItems = List.of(french, english, italian, simple);
+        final var recencyLatinIme = List.of(french, english, italian);
+        final var recencySimpleIme = List.of(simple);
+
+        assertNextOrder(controller, false /* forHardware */, mode,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+        // The order of equal non-hardware items is unchanged.
+        assertNextOrder(equalController, false /* forHardware */, mode,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+        // The order of other hardware items is reset.
+        assertNextOrder(otherController, false /* forHardware */, mode,
+                latinIme, List.of(latinIme));
+
+        // The order of hardware remains unchanged.
+        assertNextOrder(controller, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        assertNextOrder(equalController, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        assertNextOrder(otherController, true /* forHardware */, mode,
+                hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+        assertTrue("Recency updated for french hardware IME",
+                onUserAction(controller, hardwareFrench));
+
+        final var equalHardwareItems = new ArrayList<>(hardwareItems);
+        final var otherHardwareItems = new ArrayList<>(hardwareItems);
+        otherHardwareItems.remove(hardwareSimple);
+
+        final var equalHardwareController = ControllerImpl.createFrom(controller, items,
+                equalHardwareItems);
+        final var otherHardwareController = ControllerImpl.createFrom(controller, items,
+                otherHardwareItems);
+
+        final var recencyHardwareItems =
+                List.of(hardwareFrench, hardwareEnglish, hardwareItalian, hardwareSimple);
+        final var recencyHardwareLatinIme =
+                List.of(hardwareFrench, hardwareEnglish, hardwareItalian);
+        final var recencyHardwareSimpleIme = List.of(hardwareSimple);
+
+        // The order of non-hardware items remains unchanged.
+        assertNextOrder(controller, false /* forHardware */, mode,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+        assertNextOrder(equalHardwareController, false /* forHardware */, mode,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+        assertNextOrder(otherHardwareController, false /* forHardware */, mode,
+                recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+        assertNextOrder(controller, true /* forHardware */, mode,
+                recencyHardwareItems, List.of(recencyHardwareLatinIme, recencyHardwareSimpleIme));
+
+        // The order of equal hardware items is unchanged.
+        assertNextOrder(equalHardwareController, true /* forHardware */, mode,
+                recencyHardwareItems, List.of(recencyHardwareLatinIme, recencyHardwareSimpleIme));
+
+        // The order of other hardware items is reset.
+        assertNextOrder(otherHardwareController, true /* forHardware */, mode,
+                hardwareLatinIme, List.of(hardwareLatinIme));
+    }
+
+    /** Verifies that switch aware and switch unaware IMEs are combined together. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testSwitchAwareAndUnawareCombined() {
+        final var items = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(items, "switchAware", "switchAware",
+                null, true /* supportsSwitchingToNextInputMethod*/);
+        addTestImeSubtypeListItems(items, "switchUnaware", "switchUnaware",
+                null, false /* supportsSwitchingToNextInputMethod*/);
+
+        final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(hardwareItems, "hardwareSwitchAware", "hardwareSwitchAware",
+                null, true /* supportsSwitchingToNextInputMethod*/);
+        addTestImeSubtypeListItems(hardwareItems, "hardwareSwitchUnaware", "hardwareSwitchUnaware",
+                null, false /* supportsSwitchingToNextInputMethod*/);
+
+        final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+                hardwareItems);
+
+        for (int mode = MODE_STATIC; mode <= MODE_AUTO; mode++) {
+            assertNextOrder(controller, false /* forHardware */, false /* onlyCurrentIme */,
+                    mode, true /* forward */, items);
+            assertNextOrder(controller, false /* forHardware */, false /* onlyCurrentIme */,
+                    mode, false /* forward */, items.reversed());
+
+            assertNextOrder(controller, true /* forHardware */, false /* onlyCurrentIme */,
+                    mode, true /* forward */, hardwareItems);
+            assertNextOrder(controller, true /* forHardware */, false /* onlyCurrentIme */,
+                    mode, false /* forward */, hardwareItems.reversed());
+        }
+    }
+
+    /** Verifies that an empty controller can't take any actions. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testEmptyList() {
+        final var items = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+                List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+        final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
+                List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+        final var controller = ControllerImpl.createFrom(null /* currentInstance */, List.of(),
+                List.of());
+
+        assertNoAction(controller, false /* forHardware */, items);
+        assertNoAction(controller, true /* forHardware */, hardwareItems);
+    }
+
+    /** Verifies that a controller with a single item can't take any actions. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testSingleItemList() {
+        final var items = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+                List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+        final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
+                List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+        final var controller = ControllerImpl.createFrom(null /* currentInstance */,
+                List.of(items.get(0)), List.of(hardwareItems.get(0)));
+
+        assertNoAction(controller, false /* forHardware */, items);
+        assertNoAction(controller, true /* forHardware */, hardwareItems);
+    }
+
+    /** Verifies that a controller can't take any actions for unknown items. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testUnknownItems() {
+        final var items = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+                List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+        final var unknownItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(unknownItems, "UnknownIme", "UnknownIme",
+                List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+        final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
+                List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+        final var unknownHardwareItems = new ArrayList<ImeSubtypeListItem>();
+        addTestImeSubtypeListItems(unknownHardwareItems, "HardwareUnknownIme", "HardwareUnknownIme",
+                List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+        final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+                hardwareItems);
+
+        assertNoAction(controller, false /* forHardware */, unknownItems);
+        assertNoAction(controller, true /* forHardware */, unknownHardwareItems);
+    }
+
+    /** Verifies that the IME name does influence the comparison order. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testCompareImeName() {
+        final var component = new ComponentName("com.example.ime", "Ime");
+        final var imeX = createTestItem(component, "ImeX", "A", "en_US", 0);
+        final var imeY = createTestItem(component, "ImeY", "A", "en_US", 0);
+
+        assertTrue("Smaller IME name should be smaller.", imeX.compareTo(imeY) < 0);
+        assertTrue("Larger IME name should be larger.", imeY.compareTo(imeX) > 0);
+    }
+
+    /** Verifies that the IME ID does influence the comparison order. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testCompareImeId() {
+        final var component1 = new ComponentName("com.example.ime1", "Ime");
+        final var component2 = new ComponentName("com.example.ime2", "Ime");
+        final var ime1 = createTestItem(component1, "Ime", "A", "en_US", 0);
+        final var ime2 = createTestItem(component2, "Ime", "A", "en_US", 0);
+
+        assertTrue("Smaller IME ID should be smaller.", ime1.compareTo(ime2) < 0);
+        assertTrue("Larger IME ID should be larger.", ime2.compareTo(ime1) > 0);
+    }
+
+    /** Verifies that comparison on self returns an equal order. */
+    @SuppressWarnings("SelfComparison")
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testCompareSelf() {
+        final var component = new ComponentName("com.example.ime", "Ime");
+        final var item = createTestItem(component, "Ime", "A", "en_US", 0);
+
+        assertEquals("Item should have the same order to itself.", 0, item.compareTo(item));
+    }
+
+    /** Verifies that comparison on an equivalent item returns an equal order. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testCompareEquivalent() {
+        final var component = new ComponentName("com.example.ime", "Ime");
+        final var item = createTestItem(component, "Ime", "A", "en_US", 0);
+        final var equivalent = createTestItem(component, "Ime", "A", "en_US", 0);
+
+        assertEquals("Equivalent items should have the same order.", 0, item.compareTo(equivalent));
+    }
+
+    /**
+     * Verifies that the system locale and system language do not the influence comparison order.
+     */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testCompareSystemLocaleSystemLanguage() {
+        final var component = new ComponentName("com.example.ime", "Ime");
+        final var japanese = createTestItem(component, "Ime", "A", "ja_JP", 0);
+        final var systemLanguage = createTestItem(component, "Ime", "A", "en_GB", 0);
+        final var systemLocale = createTestItem(component, "Ime", "A", "en_US", 0);
+
+        assertFalse(japanese.mIsSystemLanguage);
+        assertFalse(japanese.mIsSystemLocale);
+        assertTrue(systemLanguage.mIsSystemLanguage);
+        assertFalse(systemLanguage.mIsSystemLocale);
+        assertTrue(systemLocale.mIsSystemLanguage);
+        assertTrue(systemLocale.mIsSystemLocale);
+
+        assertEquals("System language shouldn't influence comparison over non-system language.",
+                0, japanese.compareTo(systemLanguage));
+        assertEquals("System locale shouldn't influence comparison over non-system locale.",
+                0, japanese.compareTo(systemLocale));
+        assertEquals("System locale shouldn't influence comparison over system language.",
+                0, systemLanguage.compareTo(systemLocale));
+    }
+
+    /** Verifies that the subtype name does not influence the comparison order. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testCompareSubtypeName() {
+        final var component = new ComponentName("com.example.ime", "Ime");
+        final var subtypeA = createTestItem(component, "Ime", "A", "en_US", 0);
+        final var subtypeB = createTestItem(component, "Ime", "B", "en_US", 0);
+
+        assertEquals("Subtype name shouldn't influence comparison.",
+                0, subtypeA.compareTo(subtypeB));
+    }
+
+    /** Verifies that the subtype index does not influence the comparison order. */
+    @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+    @Test
+    public void testCompareSubtypeIndex() {
+        final var component = new ComponentName("com.example.ime", "Ime");
+        final var subtype0 = createTestItem(component, "Ime1", "A", "en_US", 0);
+        final var subtype1 = createTestItem(component, "Ime1", "A", "en_US", 1);
+
+        assertEquals("Subtype index shouldn't influence comparison.",
+                0, subtype0.compareTo(subtype1));
+    }
+
+    /**
+     * Verifies that the controller's next item order matches the given one, and cycles back at
+     * the end, both across all IMEs, and also per each IME. If a single item is given, verifies
+     * that no next item is returned.
+     *
+     * @param controller  the controller to use for finding the next items.
+     * @param forHardware whether to find the next hardware item, or software item.
+     * @param mode        the switching mode.
+     * @param forward     whether to search forwards or backwards in the list.
+     * @param allItems    the list of items across all IMEs.
+     * @param perImeItems the list of lists of items per IME.
+     */
+    private static void assertNextOrder(@NonNull ControllerImpl controller, boolean forHardware,
+            @SwitchMode int mode, boolean forward, @NonNull List<ImeSubtypeListItem> allItems,
+            @NonNull List<List<ImeSubtypeListItem>> perImeItems) {
+        assertNextOrder(controller, forHardware, false /* onlyCurrentIme */, mode,
+                forward, allItems);
+
+        for (var imeItems : perImeItems) {
+            assertNextOrder(controller, forHardware, true /* onlyCurrentIme */, mode,
+                    forward, imeItems);
+        }
+    }
+
+    /**
+     * Verifies that the controller's next item order matches the given one, and cycles back at
+     * the end, both across all IMEs, and also per each IME. This checks the forward direction
+     * with the given items, and the backwards order with the items reversed. If a single item is
+     * given, verifies that no next item is returned.
+     *
+     * @param controller  the controller to use for finding the next items.
+     * @param forHardware whether to find the next hardware item, or software item.
+     * @param mode        the switching mode.
+     * @param allItems    the list of items across all IMEs.
+     * @param perImeItems the list of lists of items per IME.
+     */
+    private static void assertNextOrder(@NonNull ControllerImpl controller, boolean forHardware,
+            @SwitchMode int mode, @NonNull List<ImeSubtypeListItem> allItems,
+            @NonNull List<List<ImeSubtypeListItem>> perImeItems) {
+        assertNextOrder(controller, forHardware, false /* onlyCurrentIme */, mode,
+                true /* forward */, allItems);
+        assertNextOrder(controller, forHardware, false /* onlyCurrentIme */, mode,
+                false /* forward */, allItems.reversed());
+
+        for (var imeItems : perImeItems) {
+            assertNextOrder(controller, forHardware, true /* onlyCurrentIme */, mode,
+                    true /* forward */, imeItems);
+            assertNextOrder(controller, forHardware, true /* onlyCurrentIme */, mode,
+                    false /* forward */, imeItems.reversed());
+        }
+    }
+
+    /**
+     * Verifies that the controller's next item order (starting from the first one in {@code items}
+     * matches the given on, and cycles back at the end. If a single item is given, verifies that
+     * no next item is returned.
+     *
+     * @param controller     the controller to use for finding the next items.
+     * @param forHardware    whether to find the next hardware item, or software item.
+     * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+     * @param mode           the switching mode.
+     * @param forward        whether to search forwards or backwards in the list.
+     * @param items          the list of items to verify, in the expected order.
+     */
+    private static void assertNextOrder(@NonNull ControllerImpl controller,
+            boolean forHardware, boolean onlyCurrentIme, @SwitchMode int mode, boolean forward,
+            @NonNull List<ImeSubtypeListItem> items) {
+        final int numItems = items.size();
+        if (numItems == 0) {
+            return;
+        } else if (numItems == 1) {
+            // Single item controllers should never return a next item.
+            assertNextItem(controller, forHardware, onlyCurrentIme, mode, forward, items.get(0),
+                    null /* expectedNext*/);
+            return;
+        }
+
+        var item = items.get(0);
+
+        final var expectedNextItems = new ArrayList<>(items);
+        // Add first item in the last position of expected order, to ensure the order is cyclic.
+        expectedNextItems.add(item);
+
+        final var nextItems = new ArrayList<>();
+        // Add first item in the first position of actual order, to ensure the order is cyclic.
+        nextItems.add(item);
+
+        // Compute the nextItems starting from the first given item, and compare the order.
+        for (int i = 0; i < numItems; i++) {
+            item = getNextItem(controller, forHardware, onlyCurrentIme, mode, forward, item);
+            assertNotNull("Next item shouldn't be null.", item);
+            nextItems.add(item);
+        }
+
+        assertEquals("Rotation order doesn't match.", expectedNextItems, nextItems);
+    }
+
+    /**
+     * Verifies that the controller gets the expected next value from the given item.
+     *
+     * @param controller     the controller to sue for finding the next value.
+     * @param forHardware    whether to find the next hardware item, or software item.
+     * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+     * @param mode           the switching mode.
+     * @param forward        whether to search forwards or backwards in the list.
+     * @param item           the item to find the next value from.
+     * @param expectedNext   the expected next value.
+     */
+    private static void assertNextItem(@NonNull ControllerImpl controller,
+            boolean forHardware, boolean onlyCurrentIme, @SwitchMode int mode, boolean forward,
+            @NonNull ImeSubtypeListItem item, @Nullable ImeSubtypeListItem expectedNext) {
+        final var nextItem = getNextItem(controller, forHardware, onlyCurrentIme, mode, forward,
+                item);
+        assertEquals("Next item doesn't match.", expectedNext, nextItem);
+    }
+
+    /**
+     * Gets the next value from the given item.
+     *
+     * @param controller     the controller to use for finding the next value.
+     * @param forHardware    whether to find the next hardware item, or software item.
+     * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+     * @param mode           the switching mode.
+     * @param forward        whether to search forwards or backwards in the list.
+     * @param item           the item to find the next value from.
+     * @return the next item found, otherwise {@code null}.
+     */
+    @Nullable
+    private static ImeSubtypeListItem getNextItem(@NonNull ControllerImpl controller,
+            boolean forHardware, boolean onlyCurrentIme, @SwitchMode int mode, boolean forward,
+            @NonNull ImeSubtypeListItem item) {
+        final var subtype = item.mSubtypeName != null
+                ? createTestSubtype(item.mSubtypeName.toString()) : null;
+        return forHardware
+                ? controller.getNextInputMethodForHardware(
+                        onlyCurrentIme, item.mImi, subtype, mode, forward)
+                : controller.getNextInputMethod(
+                        onlyCurrentIme, item.mImi, subtype, mode, forward);
+    }
+
+    /**
+     * Verifies that no next items can be found, and the recency cannot be updated for the
+     * given items.
+     *
+     * @param controller  the controller to verify the items on.
+     * @param forHardware whether to try finding the next hardware item, or software item.
+     * @param items       the list of items to verify.
+     */
+    private void assertNoAction(@NonNull ControllerImpl controller, boolean forHardware,
+            @NonNull List<ImeSubtypeListItem> items) {
+        for (var item : items) {
+            for (int mode = MODE_STATIC; mode <= MODE_AUTO; mode++) {
+                assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode,
+                        false /* forward */, item, null /* expectedNext */);
+                assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode,
+                        true /* forward */, item, null /* expectedNext */);
+                assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode,
+                        false /* forward */, item, null /* expectedNext */);
+                assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode,
+                        true /* forward */, item, null /* expectedNext */);
+            }
+
+            assertFalse("User action shouldn't have updated the recency.",
+                    onUserAction(controller, item));
+        }
+    }
 }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
index 81fb1a0..d59f28b 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
@@ -98,8 +98,8 @@
         assertThat(allUserData.get(0).mBindingController.getUserId()).isEqualTo(ANY_USER_ID);
     }
 
-    private List<UserDataRepository.UserData> collectUserData(UserDataRepository repository) {
-        final var collected = new ArrayList<UserDataRepository.UserData>();
+    private List<UserData> collectUserData(UserDataRepository repository) {
+        final var collected = new ArrayList<UserData>();
         repository.forAllUserData(userData -> collected.add(userData));
         return collected;
     }
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 7138306..dec4634 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -1046,6 +1046,77 @@
     }
 
     @Test
+    public void testWriteReadBaseRevisionCode() {
+        Settings settings = makeSettings();
+        PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
+        packageSetting.setAppId(Process.FIRST_APPLICATION_UID);
+        packageSetting.setPkg(PackageImpl.forTesting(PACKAGE_NAME_1).hideAsParsed()
+                .setUid(packageSetting.getAppId())
+                .hideAsFinal());
+
+        final int revisionCode = 311;
+        packageSetting.setBaseRevisionCode(revisionCode);
+        settings.mPackages.put(PACKAGE_NAME_1, packageSetting);
+
+        settings.writeLPr(computer, /* sync= */ true);
+        settings.mPackages.clear();
+
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        assertThat(settings.getPackageLPr(PACKAGE_NAME_1).getBaseRevisionCode(), is(revisionCode));
+    }
+
+    @Test
+    public void testHasPkg_writeReadSplitVersions() {
+        Settings settings = makeSettings();
+        PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
+        packageSetting.setAppId(Process.FIRST_APPLICATION_UID);
+        packageSetting.setPkg(PackageImpl.forTesting(PACKAGE_NAME_1).hideAsParsed()
+                .setUid(packageSetting.getAppId())
+                .hideAsFinal());
+
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        packageSetting.setSplitNames(new String[] { splitOne, splitTwo});
+        packageSetting.setSplitRevisionCodes(new int[] { revisionOne, revisionTwo});
+        settings.mPackages.put(PACKAGE_NAME_1, packageSetting);
+
+        settings.writeLPr(computer, /* sync= */ true);
+        settings.mPackages.clear();
+
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1);
+        assertThat(resultSetting.getSplitNames().length, is(0));
+        assertThat(resultSetting.getSplitRevisionCodes().length, is(0));
+    }
+
+    @Test
+    public void testNoPkg_writeReadSplitVersions() {
+        Settings settings = makeSettings();
+        PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
+        packageSetting.setAppId(Process.FIRST_APPLICATION_UID);
+
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        packageSetting.setSplitNames(new String[] { splitOne, splitTwo});
+        packageSetting.setSplitRevisionCodes(new int[] { revisionOne, revisionTwo});
+        settings.mPackages.put(PACKAGE_NAME_1, packageSetting);
+
+        settings.writeLPr(computer, /* sync= */ true);
+        settings.mPackages.clear();
+
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1);
+        assertThat(resultSetting.getSplitNames()[0], is(splitOne));
+        assertThat(resultSetting.getSplitNames()[1], is(splitTwo));
+        assertThat(resultSetting.getSplitRevisionCodes()[0], is(revisionOne));
+        assertThat(resultSetting.getSplitRevisionCodes()[1], is(revisionTwo));
+    }
+
+    @Test
     public void testWriteReadArchiveState() {
         Settings settings = makeSettings();
         PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
index 2d4a29b..f690b1b 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
@@ -18,8 +18,6 @@
 
 import static org.junit.Assert.assertEquals;
 
-import android.hardware.display.BrightnessInfo;
-
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -114,10 +112,7 @@
                 .append("\n    mBrightnessAdjustmentFlag:")
                 .append(displayBrightnessState.getBrightnessAdjustmentFlag())
                 .append("\n    mIsUserInitiatedChange:")
-                .append(displayBrightnessState.isUserInitiatedChange())
-                .append("\n    mBrightnessMaxReason:")
-                .append(BrightnessInfo.briMaxReasonToString(
-                        displayBrightnessState.getBrightnessMaxReason()));
+                .append(displayBrightnessState.isUserInitiatedChange());
         return sb.toString();
     }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 3437923..d450683 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -965,6 +965,51 @@
         assertThat(supportedModeData.vsyncRate).isEqualTo(240);
     }
 
+    @Test
+    public void testDozeBrightness_Ddc() throws IOException {
+        when(mFlags.isDozeBrightnessFloatEnabled()).thenReturn(true);
+        setupDisplayDeviceConfigFromDisplayConfigFile();
+
+        assertArrayEquals(new float[]{ -1, 0.1f, 0.2f, 0.3f, 0.4f },
+                mDisplayDeviceConfig.getDozeBrightnessSensorValueToBrightness(), SMALL_DELTA);
+        assertEquals(0.25f, mDisplayDeviceConfig.getDefaultDozeBrightness(), SMALL_DELTA);
+    }
+
+    @Test
+    public void testDefaultDozeBrightness_FallBackToConfigXmlFloat() throws IOException {
+        setupDisplayDeviceConfigFromConfigResourceFile();
+        when(mFlags.isDozeBrightnessFloatEnabled()).thenReturn(true);
+        when(mResources.getFloat(com.android.internal.R.dimen.config_screenBrightnessDozeFloat))
+                .thenReturn(0.31f);
+        when(mResources.getInteger(com.android.internal.R.integer.config_screenBrightnessDoze))
+                .thenReturn(90);
+
+        // Empty display config file
+        setupDisplayDeviceConfigFromDisplayConfigFile(
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                        + "<displayConfiguration />\n");
+
+        assertEquals(0.31f, mDisplayDeviceConfig.getDefaultDozeBrightness(), ZERO_DELTA);
+    }
+
+    @Test
+    public void testDefaultDozeBrightness_FallBackToConfigXmlInt() throws IOException {
+        setupDisplayDeviceConfigFromConfigResourceFile();
+        when(mFlags.isDozeBrightnessFloatEnabled()).thenReturn(true);
+        when(mResources.getFloat(com.android.internal.R.dimen.config_screenBrightnessDozeFloat))
+                .thenReturn(DisplayDeviceConfig.INVALID_BRIGHTNESS_IN_CONFIG);
+        when(mResources.getInteger(com.android.internal.R.integer.config_screenBrightnessDoze))
+                .thenReturn(90);
+
+        // Empty display config file
+        setupDisplayDeviceConfigFromDisplayConfigFile(
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                        + "<displayConfiguration />\n");
+
+        assertEquals(brightnessIntToFloat(90),
+                mDisplayDeviceConfig.getDefaultDozeBrightness(), ZERO_DELTA);
+    }
+
     private String getValidLuxThrottling() {
         return "<luxThrottling>\n"
                 + "    <brightnessLimitMap>\n"
@@ -1708,6 +1753,16 @@
                 +           "</point>"
                 +       "</luxThresholds>"
                 +   "</idleScreenRefreshRateTimeout>"
+                +   "<dozeBrightnessSensorValueToBrightness>\n"
+                +       "<item>-1</item>\n"
+                +       "<item>0.1</item>\n"
+                +       "<item>0.2</item>\n"
+                +       "<item>0.3</item>\n"
+                +       "<item>0.4</item>\n"
+                +   "</dozeBrightnessSensorValueToBrightness>\n"
+                +   "<defaultDozeBrightness>"
+                +       "0.25"
+                +   "</defaultDozeBrightness>\n"
                 + "</displayConfiguration>\n";
     }
 
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 5c29156..624c897 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -117,6 +117,7 @@
     private static final String SECOND_FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_789";
     private static final float PROX_SENSOR_MAX_RANGE = 5;
     private static final float DOZE_SCALE_FACTOR = 0.34f;
+    private static final float DEFAULT_DOZE_BRIGHTNESS = 0.121f;
 
     private static final float BRIGHTNESS_RAMP_RATE_MINIMUM = 0.0f;
     private static final float BRIGHTNESS_RAMP_RATE_FAST_DECREASE = 0.3f;
@@ -2051,9 +2052,6 @@
 
     @Test
     public void testDefaultDozeBrightness() {
-        float brightness = 0.121f;
-        when(mPowerManagerMock.getBrightnessConstraint(
-                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE)).thenReturn(brightness);
         mContext.getOrCreateTestableResources().addOverride(
                 com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
@@ -2069,15 +2067,25 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.animator).animateTo(eq(brightness), /* linearSecondTarget= */ anyFloat(),
-                eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
+        verify(mHolder.animator).animateTo(eq(DEFAULT_DOZE_BRIGHTNESS),
+                /* linearSecondTarget= */ anyFloat(), eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE),
+                eq(false));
+
+        // The display device changes and the default doze brightness changes
+        setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
+                mHolder.config, /* isEnabled= */ true);
+        when(mHolder.config.getDefaultDozeBrightness()).thenReturn(DEFAULT_DOZE_BRIGHTNESS / 2);
+        mHolder.dpc.onDisplayChanged(mHolder.hbmMetadata, Layout.NO_LEAD_DISPLAY);
+
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(DEFAULT_DOZE_BRIGHTNESS / 2),
+                /* linearSecondTarget= */ anyFloat(), eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE),
+                eq(false));
     }
 
     @Test
     public void testDefaultDozeBrightness_ShouldNotBeUsedIfAutoBrightnessAllowedInDoze() {
-        float brightness = 0.121f;
-        when(mPowerManagerMock.getBrightnessConstraint(
-                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE)).thenReturn(brightness);
         mContext.getOrCreateTestableResources().addOverride(
                 com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true);
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
@@ -2093,7 +2101,7 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.animator, never()).animateTo(eq(brightness),
+        verify(mHolder.animator, never()).animateTo(eq(DEFAULT_DOZE_BRIGHTNESS),
                 /* linearSecondTarget= */ anyFloat(), /* rate= */ anyFloat(),
                 /* ignoreAnimationLimits= */ anyBoolean());
     }
@@ -2151,6 +2159,8 @@
                 new SensorData(Sensor.STRING_TYPE_LIGHT, null));
         when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
                 .thenReturn(new int[0]);
+        when(displayDeviceConfigMock.getDefaultDozeBrightness())
+                .thenReturn(DEFAULT_DOZE_BRIGHTNESS);
 
         when(displayDeviceConfigMock.getBrightnessRampFastDecrease())
                 .thenReturn(BRIGHTNESS_RAMP_RATE_FAST_DECREASE);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
index 93dfbcb..e982153 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
@@ -30,6 +30,7 @@
 
 import android.content.Context;
 import android.hardware.SensorManager;
+import android.hardware.display.BrightnessInfo;
 import android.hardware.display.DisplayManagerInternal;
 import android.os.Handler;
 import android.os.PowerManager;
@@ -154,6 +155,12 @@
     }
 
     @Test
+    public void testMaxReasonIsNoneOnInit() {
+        assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE,
+                mClamperController.getBrightnessMaxReason());
+    }
+
+    @Test
     public void testOnDisplayChanged_DelegatesToClamper() {
         mClamperController.onDisplayChanged(mMockDisplayDeviceData);
 
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 753db12..b9e99dd 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -36,6 +36,8 @@
         "-Werror",
     ],
     static_libs: [
+        "a11ychecker-protos-java-proto-lite",
+        "aatf",
         "cts-input-lib",
         "frameworks-base-testutils",
         "services.accessibility",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
new file mode 100644
index 0000000..90d4275
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 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.accessibility.a11ychecker;
+
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_CLASS_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_VERSION_CODE;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_VERSION_CODE;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_WINDOW_TITLE;
+import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClassNameCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityCheckerUtilsTest {
+
+    PackageManager mMockPackageManager;
+
+    @Before
+    public void setUp() throws PackageManager.NameNotFoundException {
+        mMockPackageManager = getMockPackageManagerWithInstalledApps();
+    }
+
+    @Test
+    public void processResults_happyPath_setsAllFields() {
+        AccessibilityNodeInfo mockNodeInfo =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setViewIdResourceName("TargetNode")
+                        .build();
+        AccessibilityHierarchyCheckResult result1 =
+                new AccessibilityHierarchyCheckResult(
+                        SpeakableTextPresentCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.WARNING, null, 1,
+                        null);
+        AccessibilityHierarchyCheckResult result2 =
+                new AccessibilityHierarchyCheckResult(
+                        TouchTargetSizeCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null);
+        AccessibilityHierarchyCheckResult result3 =
+                new AccessibilityHierarchyCheckResult(
+                        ClassNameCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.INFO, null, 5, null);
+        AccessibilityHierarchyCheckResult result4 =
+                new AccessibilityHierarchyCheckResult(
+                        ClickableSpanCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN, null, 5,
+                        null);
+
+        Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+                AccessibilityCheckerUtils.processResults(
+                        mockNodeInfo,
+                        List.of(result1, result2, result3, result4),
+                        null,
+                        mMockPackageManager,
+                        new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                                TEST_A11Y_SERVICE_CLASS_NAME));
+
+        assertThat(atoms).containsExactly(
+                createAtom(A11yCheckerProto.AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK,
+                        A11yCheckerProto.AccessibilityCheckResultType.WARNING, 1),
+                createAtom(A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+                        A11yCheckerProto.AccessibilityCheckResultType.ERROR, 2)
+        );
+    }
+
+    @Test
+    public void processResults_packageNameNotFound_returnsEmptySet()
+            throws PackageManager.NameNotFoundException {
+        when(mMockPackageManager.getPackageInfo("com.uninstalled.app", 0))
+                .thenThrow(PackageManager.NameNotFoundException.class);
+        AccessibilityNodeInfo mockNodeInfo =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setPackageName("com.uninstalled.app")
+                        .setViewIdResourceName("TargetNode")
+                        .build();
+        AccessibilityHierarchyCheckResult result1 =
+                new AccessibilityHierarchyCheckResult(
+                        TouchTargetSizeCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.WARNING, null, 1,
+                        null);
+        AccessibilityHierarchyCheckResult result2 =
+                new AccessibilityHierarchyCheckResult(
+                        TouchTargetSizeCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null);
+
+        Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+                AccessibilityCheckerUtils.processResults(
+                        mockNodeInfo,
+                        List.of(result1, result2),
+                        null,
+                        mMockPackageManager,
+                        new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                                TEST_A11Y_SERVICE_CLASS_NAME));
+
+        assertThat(atoms).isEmpty();
+    }
+
+    @Test
+    public void getActivityName_hasWindowStateChangedEvent_returnsActivityName() {
+        AccessibilityEvent accessibilityEvent =
+                AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        accessibilityEvent.setPackageName(TEST_APP_PACKAGE_NAME);
+        accessibilityEvent.setClassName(TEST_ACTIVITY_NAME);
+
+        assertThat(AccessibilityCheckerUtils.getActivityName(mMockPackageManager,
+                accessibilityEvent)).isEqualTo("MainActivity");
+    }
+
+    // Makes sure the AccessibilityHierarchyCheck class to enum mapping is up to date with the
+    // latest prod preset.
+    @Test
+    public void checkClassToEnumMap_hasAllLatestPreset() {
+        ImmutableSet<AccessibilityHierarchyCheck> checkPreset =
+                AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(
+                        AccessibilityCheckPreset.LATEST);
+        Set<Class<? extends AccessibilityHierarchyCheck>> latestCheckClasses =
+                checkPreset.stream().map(AccessibilityHierarchyCheck::getClass).collect(
+                        Collectors.toUnmodifiableSet());
+
+        assertThat(AccessibilityCheckerUtils.CHECK_CLASS_TO_ENUM_MAP.keySet())
+                .containsExactlyElementsIn(latestCheckClasses);
+    }
+
+
+    private static A11yCheckerProto.AccessibilityCheckResultReported createAtom(
+            A11yCheckerProto.AccessibilityCheckClass checkClass,
+            A11yCheckerProto.AccessibilityCheckResultType resultType,
+            int resultId) {
+        return A11yCheckerProto.AccessibilityCheckResultReported.newBuilder()
+                .setPackageName(TEST_APP_PACKAGE_NAME)
+                .setAppVersionCode(TEST_APP_VERSION_CODE)
+                .setUiElementPath(TEST_APP_PACKAGE_NAME + ":TargetNode")
+                .setWindowTitle(TEST_WINDOW_TITLE)
+                .setActivityName("")
+                .setSourceComponentName(new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                        TEST_A11Y_SERVICE_CLASS_NAME).flattenToString())
+                .setSourceVersionCode(TEST_A11Y_SERVICE_SOURCE_VERSION_CODE)
+                .setResultCheckClass(checkClass)
+                .setResultType(resultType)
+                .setResultId(resultId)
+                .build();
+    }
+
+}
diff --git a/core/tests/coretests/src/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
similarity index 80%
rename from core/tests/coretests/src/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
rename to services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
index 438277b..a53f42e 100644
--- a/core/tests/coretests/src/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package android.view.accessibility.a11ychecker;
+package com.android.server.accessibility.a11ychecker;
 
-import static android.view.accessibility.a11ychecker.MockAccessibilityNodeInfoBuilder.PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -36,7 +36,7 @@
 @RunWith(AndroidJUnit4.class)
 public class AccessibilityNodePathBuilderTest {
 
-    public static final String RESOURCE_ID_PREFIX = PACKAGE_NAME + ":id/";
+    public static final String RESOURCE_ID_PREFIX = TEST_APP_PACKAGE_NAME + ":id/";
 
     @Test
     public void createNodePath_pathWithResourceNames() {
@@ -55,11 +55,11 @@
                         .build();
 
         assertThat(AccessibilityNodePathBuilder.createNodePath(child))
-                .isEqualTo(PACKAGE_NAME + ":root_node/parent_node[1]/child_node[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node/parent_node[1]/child_node[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
-                .isEqualTo(PACKAGE_NAME + ":root_node/parent_node[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node/parent_node[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(root))
-                .isEqualTo(PACKAGE_NAME + ":root_node");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node");
     }
 
     @Test
@@ -81,11 +81,11 @@
                         .build();
 
         assertThat(AccessibilityNodePathBuilder.createNodePath(child))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout/RecyclerView[1]/TextView[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/RecyclerView[1]/TextView[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout/RecyclerView[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/RecyclerView[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(root))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout");
     }
 
     @Test
@@ -105,11 +105,11 @@
                         .build();
 
         assertThat(AccessibilityNodePathBuilder.createNodePath(child1))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout/child1[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/child1[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(child2))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout/TextView[2]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/TextView[2]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout");
     }
 
     @Test
@@ -133,13 +133,13 @@
                         .build();
 
         assertThat(AccessibilityNodePathBuilder.createNodePath(child1))
-                .isEqualTo(PACKAGE_NAME + ":parentId/childId[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/childId[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(child2))
-                .isEqualTo(PACKAGE_NAME + ":parentId/child/Id/With/Slash[2]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/child/Id/With/Slash[2]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(child3))
-                .isEqualTo(PACKAGE_NAME + ":parentId/childIdWithoutPrefix[3]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/childIdWithoutPrefix[3]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
-                .isEqualTo(PACKAGE_NAME + ":parentId");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId");
     }
 
 }
diff --git a/core/tests/coretests/src/android/view/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
similarity index 72%
rename from core/tests/coretests/src/android/view/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
rename to services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
index e363f0c..7cd3535 100644
--- a/core/tests/coretests/src/android/view/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
@@ -14,21 +14,33 @@
  * limitations under the License.
  */
 
-package android.view.accessibility.a11ychecker;
+package com.android.server.accessibility.a11ychecker;
+
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_WINDOW_TITLE;
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
 
 import java.util.List;
 
 final class MockAccessibilityNodeInfoBuilder {
-    static final String PACKAGE_NAME = "com.example.app";
     private final AccessibilityNodeInfo mMockNodeInfo = mock(AccessibilityNodeInfo.class);
 
     MockAccessibilityNodeInfoBuilder() {
-        when(mMockNodeInfo.getPackageName()).thenReturn(PACKAGE_NAME);
+        setPackageName(TEST_APP_PACKAGE_NAME);
+
+        AccessibilityWindowInfo windowInfo = new AccessibilityWindowInfo();
+        windowInfo.setTitle(TEST_WINDOW_TITLE);
+        when(mMockNodeInfo.getWindow()).thenReturn(windowInfo);
+    }
+
+    MockAccessibilityNodeInfoBuilder setPackageName(String packageName) {
+        when(mMockNodeInfo.getPackageName()).thenReturn(packageName);
+        return this;
     }
 
     MockAccessibilityNodeInfoBuilder setClassName(String className) {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS
new file mode 100644
index 0000000..7bdc029
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS
@@ -0,0 +1 @@
+include /services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
new file mode 100644
index 0000000..a04bbee
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 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.accessibility.a11ychecker;
+
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+import org.mockito.Mockito;
+
+public class TestUtils {
+    static final String TEST_APP_PACKAGE_NAME = "com.example.app";
+    static final int TEST_APP_VERSION_CODE = 12321;
+    static final String TEST_ACTIVITY_NAME = "com.example.app.MainActivity";
+    static final String TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME = "com.assistive.app";
+    static final String TEST_A11Y_SERVICE_CLASS_NAME = "MyA11yService";
+    static final int TEST_A11Y_SERVICE_SOURCE_VERSION_CODE = 333555;
+    static final String TEST_WINDOW_TITLE = "Example window";
+
+    static PackageManager getMockPackageManagerWithInstalledApps()
+            throws PackageManager.NameNotFoundException {
+        PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
+        ActivityInfo testActivityInfo = getTestActivityInfo();
+        ComponentName testActivityComponentName = new ComponentName(TEST_APP_PACKAGE_NAME,
+                TEST_ACTIVITY_NAME);
+
+        when(mockPackageManager.getActivityInfo(testActivityComponentName, 0))
+                .thenReturn(testActivityInfo);
+        when(mockPackageManager.getPackageInfo(TEST_APP_PACKAGE_NAME, 0))
+                .thenReturn(createPackageInfo(TEST_APP_PACKAGE_NAME, TEST_APP_VERSION_CODE,
+                        testActivityInfo));
+        when(mockPackageManager.getPackageInfo(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, 0))
+                .thenReturn(createPackageInfo(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                        TEST_A11Y_SERVICE_SOURCE_VERSION_CODE, null));
+        return mockPackageManager;
+    }
+
+    static ActivityInfo getTestActivityInfo() {
+        ActivityInfo activityInfo = new ActivityInfo();
+        activityInfo.packageName = TEST_APP_PACKAGE_NAME;
+        activityInfo.name = TEST_ACTIVITY_NAME;
+        return activityInfo;
+    }
+
+    static PackageInfo createPackageInfo(String packageName, int versionCode,
+            @Nullable ActivityInfo activityInfo) {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = packageName;
+        packageInfo.setLongVersionCode(versionCode);
+        if (activityInfo != null) {
+            packageInfo.activities = new ActivityInfo[]{activityInfo};
+        }
+        return packageInfo;
+
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TimeToLiveHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/TimeToLiveHelperTest.java
index 8b46c8c..ad6c233 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TimeToLiveHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TimeToLiveHelperTest.java
@@ -29,6 +29,8 @@
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.Uri;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
@@ -36,6 +38,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 import com.android.server.UiServiceTestCase;
+import com.android.server.pm.PackageManagerService;
 
 import org.junit.After;
 import org.junit.Before;
@@ -125,6 +128,50 @@
     }
 
     @Test
+    public void testTimeoutExpires_notifAlreadyCanceled() {
+        NotificationRecord r = getRecord("testTimeoutExpires", 1);
+
+        mHelper.scheduleTimeoutLocked(r, 1);
+        mHelper.cancelScheduledTimeoutLocked(r);
+
+        Intent intent = new Intent("com.android.server.notification.TimeToLiveHelper")
+                .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
+                .setData(new Uri.Builder()
+                        .scheme("timeout")
+                        .appendPath(r.getKey())
+                        .build())
+                .putExtra(EXTRA_KEY, r.getKey())
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+
+        mHelper.mNotificationTimeoutReceiver.onReceive(mContext, intent);
+
+        assertThat(mHelper.mKeys).isEmpty();
+    }
+
+    @Test
+    public void testTimeoutExpires_laterNotifAlreadyCanceled() {
+        NotificationRecord r = getRecord("testTimeoutExpires", 1);
+        NotificationRecord r2 = getRecord("testTimeoutExpires", 2);
+
+        mHelper.scheduleTimeoutLocked(r, 1);
+        mHelper.scheduleTimeoutLocked(r2, 2);
+        mHelper.cancelScheduledTimeoutLocked(r2);
+
+        Intent intent = new Intent("com.android.server.notification.TimeToLiveHelper")
+                .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
+                .setData(new Uri.Builder()
+                        .scheme("timeout")
+                        .appendPath(r2.getKey())
+                        .build())
+                .putExtra(EXTRA_KEY, r2.getKey())
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+
+        mHelper.mNotificationTimeoutReceiver.onReceive(mContext, intent);
+
+        assertThat(mHelper.mKeys).isEmpty();
+    }
+
+    @Test
     public void testTimeout_earlierEntryAddedSecond() {
         NotificationRecord later = getRecord("testTimeoutSecond", 2);
         mHelper.scheduleTimeoutLocked(later, 1);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
index 633a3c9..901c036 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
@@ -54,6 +54,7 @@
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.AtomicFile;
 import android.util.SparseArray;
+import android.view.HapticFeedbackConstants;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -292,7 +293,7 @@
 
         for (int effectId : BIOMETRIC_FEEDBACK_CONSTANTS) {
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* bypassVibrationIntensitySetting= */ false, /* fromIme= */ false);
+                    effectId, /* flags */ 0, /* privFlags */ 0);
             assertThat(attrs.getUsage()).isEqualTo(VibrationAttributes.USAGE_COMMUNICATION_REQUEST);
         }
     }
@@ -302,8 +303,7 @@
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
         VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                SAFE_MODE_ENABLED, /* bypassVibrationIntensitySetting= */ false,
-                false /* fromIme*/);
+                SAFE_MODE_ENABLED, /* flags */ 0, /* privFlags */ 0);
 
         assertThat(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)).isFalse();
     }
@@ -313,7 +313,8 @@
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
         VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                SAFE_MODE_ENABLED, /* bypassVibrationIntensitySetting= */ true, false /* fromIme*/);
+                SAFE_MODE_ENABLED,
+                /* flags */ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, /* privFlags */ 0);
 
         assertThat(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)).isTrue();
     }
@@ -325,7 +326,7 @@
 
         for (int effectId : SCROLL_FEEDBACK_CONSTANTS) {
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* bypassVibrationIntensitySetting= */ false, false /* fromIme*/);
+                    effectId, /* flags */ 0, /* privFlags */ 0);
             assertWithMessage("Expected FLAG_BYPASS_INTERRUPTION_POLICY for effect " + effectId)
                    .that(attrs.isFlagSet(FLAG_BYPASS_INTERRUPTION_POLICY)).isTrue();
         }
@@ -338,7 +339,7 @@
 
         for (int effectId : SCROLL_FEEDBACK_CONSTANTS) {
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* bypassVibrationIntensitySetting= */ false, false /* fromIme*/);
+                    effectId, /* flags */ 0, /* privFlags */ 0);
             assertWithMessage("Expected no FLAG_BYPASS_INTERRUPTION_POLICY for effect " + effectId)
                    .that(attrs.isFlagSet(FLAG_BYPASS_INTERRUPTION_POLICY)).isFalse();
         }
@@ -351,7 +352,8 @@
 
         for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* bypassVibrationIntensitySetting= */ false, true /* fromIme*/);
+                    effectId, /* flags */ 0,
+                    HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
             assertWithMessage("Expected USAGE_TOUCH for effect " + effectId)
                     .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH);
             assertWithMessage("Expected no CATEGORY_KEYBOARD for effect " + effectId)
@@ -366,7 +368,7 @@
 
         for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* bypassVibrationIntensitySetting= */ false, false /* fromIme*/);
+                    effectId, /* flags */ 0, /* privFlags */ 0);
             assertWithMessage("Expected USAGE_TOUCH for effect " + effectId)
                     .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH);
             assertWithMessage("Expected CATEGORY_KEYBOARD for effect " + effectId)
@@ -381,7 +383,8 @@
 
         for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* bypassVibrationIntensitySetting= */ false, true /* fromIme*/);
+                    effectId, /* flags */ 0,
+                    HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
             assertWithMessage("Expected USAGE_TOUCH for effect " + effectId)
                     .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH);
             assertWithMessage("Expected CATEGORY_KEYBOARD for effect " + effectId)
@@ -398,7 +401,8 @@
 
         for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* bypassVibrationIntensitySetting= */ false, true /* fromIme*/);
+                    effectId, /* flags */ 0,
+                    HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
             assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
                     + effectId)
                     .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse();
@@ -414,7 +418,7 @@
 
         for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* bypassVibrationIntensitySetting= */ false, false /* fromIme*/);
+                    effectId, /* flags */ 0, /* privFlags */ 0);
             assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
                     + effectId)
                     .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse();
@@ -430,7 +434,8 @@
 
         for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* bypassVibrationIntensitySetting= */ false, true /* fromIme*/);
+                    effectId, /* flags */ 0,
+                    HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
             assertWithMessage("Expected FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
                     + effectId)
                     .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isTrue();
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 1875284..ef944db 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -2661,9 +2661,10 @@
 
     private HalVibration performHapticFeedbackAndWaitUntilFinished(VibratorManagerService service,
                 int constant, boolean always) throws InterruptedException {
-        HalVibration vib =
-                service.performHapticFeedbackInternal(UID, Context.DEVICE_ID_DEFAULT, PACKAGE_NAME,
-                        constant, always, "some reason", service, false /* fromIme */);
+        HalVibration vib = service.performHapticFeedbackInternal(UID, Context.DEVICE_ID_DEFAULT,
+                PACKAGE_NAME, constant, "some reason", service,
+                always ? HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING : 0 /* flags */,
+                0 /* privFlags */);
         if (vib != null) {
             vib.waitForEnd();
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index 564c29f..11e6d90 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -23,7 +23,6 @@
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
@@ -176,6 +175,7 @@
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity);
         mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
         callOnActivityConfigurationChanging(mActivity);
@@ -185,34 +185,6 @@
     }
 
     @Test
-    public void testReconnectedToDifferentCamera_activatesCameraCompatModeAndRefresh()
-            throws Exception {
-        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
-
-        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
-        mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
-        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_2, TEST_PACKAGE_1);
-        callOnActivityConfigurationChanging(mActivity);
-
-        assertInCameraCompatMode();
-        assertActivityRefreshRequested(/* refreshRequested */ true);
-    }
-
-    @Test
-    public void testCameraDisconnected_deactivatesCameraCompatMode() {
-        configureActivityAndDisplay(SCREEN_ORIENTATION_PORTRAIT, ORIENTATION_LANDSCAPE,
-                WINDOWING_MODE_FREEFORM);
-        // Open camera and test for compat treatment
-        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
-        assertInCameraCompatMode();
-
-        // Close camera and test for revert
-        mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
-
-        assertNotInCameraCompatMode();
-    }
-
-    @Test
     public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
index e468fd8..1c8dc05 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
@@ -256,8 +256,7 @@
         }
 
         @Override
-        public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity,
-                @NonNull String cameraId) {
+        public boolean onCameraClosed(@NonNull String cameraId) {
             mOnCameraClosedCounter++;
             boolean returnValue = mOnCameraClosedReturnValue;
             // If false, return false only the first time, so it doesn't fall in the infinite retry
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index a4bec64..ad3fba3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -166,9 +166,9 @@
                 ACTIVITY_TYPE_STANDARD).setDisplay(display).build();
 
         final int desiredWidth =
-                (int) (DISPLAY_STABLE_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+                (int) (DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
         final int desiredHeight =
-                (int) (DISPLAY_STABLE_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+                (int) (DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
         assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
         assertEquals(desiredWidth, mResult.mBounds.width());
         assertEquals(desiredHeight, mResult.mBounds.height());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index 38ad9a7..4b0668f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -270,8 +270,8 @@
     }
 
     @Override
-    public boolean performHapticFeedback(int uid, String packageName, int effectId,
-            boolean always, String reason, boolean fromIme) {
+    public boolean performHapticFeedback(int uid, String packageName, int effectId, String reason,
+            int flags, int privFlags) {
         return false;
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 95d37eb..7d01b79 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -35,6 +35,7 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.window.TransitionInfo.FLAG_CONFIG_AT_END;
+import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
 import static android.window.TransitionInfo.FLAG_FILLS_TASK;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
@@ -77,13 +78,14 @@
 
 import android.app.ActivityManager;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.IBinder;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.view.SurfaceControl;
@@ -95,6 +97,7 @@
 import android.window.ITransitionPlayer;
 import android.window.RemoteTransition;
 import android.window.SystemPerformanceHinter;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentOrganizer;
 import android.window.TransitionInfo;
 
@@ -104,7 +107,6 @@
 import com.android.internal.graphics.ColorUtils;
 import com.android.window.flags.Flags;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -120,16 +122,15 @@
  * Build/Install/Run:
  *  atest WmTests:TransitionTests
  */
-@EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
 @SmallTest
 @Presubmit
 @RunWith(WindowTestRunner.class)
 public class TransitionTests extends WindowTestsBase {
     final SurfaceControl.Transaction mMockT = mock(SurfaceControl.Transaction.class);
     private BLASTSyncEngine mSyncEngine;
+    private Transition mTransition;
+    private TransitionInfo mInfo;
 
-    @Rule
-    public SetFlagsRule mRule = new SetFlagsRule();
     private Transition createTestTransition(int transitType, TransitionController controller) {
         final Transition transition = new Transition(transitType, 0 /* flags */, controller,
                 controller.mSyncEngine);
@@ -1994,6 +1995,221 @@
         assertEquals(expectedBackgroundColor, info.getChanges().get(1).getBackgroundColor());
     }
 
+    @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+    @Test
+    public void testOverrideAnimationOptionsToInfoIfNecessary_disableAnimOptionsPerChange() {
+        initializeOverrideAnimationOptionsTest();
+        TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions
+                .makeCommonAnimOptions("testPackage");
+        mTransition.setOverrideAnimation(options, null /* startCallback */,
+                null /* finishCallback */);
+
+        mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo);
+
+        assertEquals(options, mInfo.getAnimationOptions());
+    }
+
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+    @Test
+    public void testOverrideAnimationOptionsToInfoIfNecessary_nonCustomAnimOptions() {
+        initializeOverrideAnimationOptionsTest();
+        TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions
+                .makeCommonAnimOptions("testPackage");
+        mTransition.setOverrideAnimation(options, null /* startCallback */,
+                null /* finishCallback */);
+
+        mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo);
+
+        final TransitionInfo.Change displayChange = mInfo.getChanges().get(0);
+        final TransitionInfo.Change taskChange = mInfo.getChanges().get(1);
+        final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2);
+        final TransitionInfo.Change activityChange = mInfo.getChanges().get(3);
+
+        assertNull("Display change's AnimationOptions must not be overridden.",
+                displayChange.getAnimationOptions());
+        assertNull("Task change's AnimationOptions must not be overridden.",
+                taskChange.getAnimationOptions());
+        assertNull("Embedded TF change's AnimationOptions must not be overridden.",
+                embeddedTfChange.getAnimationOptions());
+        assertEquals("Activity change's AnimationOptions must be overridden.",
+                options, activityChange.getAnimationOptions());
+    }
+
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+    @Test
+    public void testOverrideAnimationOptionsToInfoIfNecessary_crossProfileAnimOptions() {
+        initializeOverrideAnimationOptionsTest();
+        TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions
+                .makeCrossProfileAnimOptions();
+        mTransition.setOverrideAnimation(options, null /* startCallback */,
+                null /* finishCallback */);
+
+        final TransitionInfo.Change displayChange = mInfo.getChanges().get(0);
+        final TransitionInfo.Change taskChange = mInfo.getChanges().get(1);
+        final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2);
+        final TransitionInfo.Change activityChange = mInfo.getChanges().get(3);
+        activityChange.setMode(TRANSIT_OPEN);
+
+        mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo);
+
+        assertNull("Display change's AnimationOptions must not be overridden.",
+                displayChange.getAnimationOptions());
+        assertNull("Task change's AnimationOptions must not be overridden.",
+                taskChange.getAnimationOptions());
+        assertNull("Embedded TF change's AnimationOptions must not be overridden.",
+                embeddedTfChange.getAnimationOptions());
+        assertEquals("Activity change's AnimationOptions must be overridden.",
+                options, activityChange.getAnimationOptions());
+        assertTrue(activityChange.hasFlags(FLAG_CROSS_PROFILE_OWNER_THUMBNAIL));
+    }
+
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+    @Test
+    public void testOverrideAnimationOptionsToInfoIfNecessary_customAnimOptions() {
+        initializeOverrideAnimationOptionsTest();
+        TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions
+                .makeCustomAnimOptions("testPackage", Resources.ID_NULL,
+                        TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID,
+                        TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID,
+                        Color.GREEN, false /* overrideTaskTransition */);
+        mTransition.setOverrideAnimation(options, null /* startCallback */,
+                null /* finishCallback */);
+
+        mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo);
+
+        final TransitionInfo.Change displayChange = mInfo.getChanges().get(0);
+        final TransitionInfo.Change taskChange = mInfo.getChanges().get(1);
+        final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2);
+        final TransitionInfo.Change activityChange = mInfo.getChanges().get(3);
+
+        assertNull("Display change's AnimationOptions must not be overridden.",
+                displayChange.getAnimationOptions());
+        assertNull("Task change's AnimationOptions must not be overridden.",
+                taskChange.getAnimationOptions());
+        assertEquals("Embedded TF change's AnimationOptions must be overridden.",
+                options, embeddedTfChange.getAnimationOptions());
+        assertEquals("Embedded TF change's background color must not be overridden.",
+                0, embeddedTfChange.getBackgroundColor());
+        assertEquals("Activity change's AnimationOptions must be overridden.",
+                options, activityChange.getAnimationOptions());
+        assertEquals("Activity change's background color must be overridden.",
+                options.getBackgroundColor(), activityChange.getBackgroundColor());
+    }
+
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+    @Test
+    public void testOverrideAnimationOptionsToInfoIfNecessary_haveTaskFragmentAnimParams() {
+        initializeOverrideAnimationOptionsTest();
+
+        final TaskFragment embeddedTf = mTransition.mTargets.get(2).mContainer.asTaskFragment();
+        embeddedTf.setAnimationParams(new TaskFragmentAnimationParams.Builder()
+                .setAnimationBackgroundColor(Color.RED)
+                .setOpenAnimationResId(0x12345678)
+                .build());
+
+        TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions
+                .makeCustomAnimOptions("testPackage", Resources.ID_NULL,
+                        TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID,
+                        TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID,
+                        Color.GREEN, false /* overrideTaskTransition */);
+        mTransition.setOverrideAnimation(options, null /* startCallback */,
+                null /* finishCallback */);
+
+        final TransitionInfo.Change displayChange = mInfo.getChanges().get(0);
+        final TransitionInfo.Change taskChange = mInfo.getChanges().get(1);
+        final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2);
+        final TransitionInfo.Change activityChange = mInfo.getChanges().get(3);
+
+        final int expectedColor = embeddedTf.getAnimationParams().getAnimationBackgroundColor();
+        embeddedTfChange.setBackgroundColor(expectedColor);
+        final TransitionInfo.AnimationOptions expectedOptions = TransitionInfo.AnimationOptions
+                .makeCustomAnimOptions("testPackage", 0x12345678,
+                        TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID,
+                        TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID,
+                        0, false /* overrideTaskTransition */);
+        embeddedTfChange.setAnimationOptions(expectedOptions);
+
+        mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo);
+
+        assertNull("Display change's AnimationOptions must not be overridden.",
+                displayChange.getAnimationOptions());
+        assertNull("Task change's AnimationOptions must not be overridden.",
+                taskChange.getAnimationOptions());
+        assertEquals("Embedded TF change's AnimationOptions must be overridden.",
+                expectedOptions, embeddedTfChange.getAnimationOptions());
+        assertEquals("Embedded TF change's background color must not be overridden.",
+                expectedColor, embeddedTfChange.getBackgroundColor());
+        assertEquals("Activity change's AnimationOptions must be overridden.",
+                options, activityChange.getAnimationOptions());
+        assertEquals("Activity change's background color must be overridden.",
+                options.getBackgroundColor(), activityChange.getBackgroundColor());
+    }
+
+    @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+    @Test
+    public void testOverrideAnimationOptionsToInfoIfNecessary_customAnimOptionsWithTaskOverride() {
+        initializeOverrideAnimationOptionsTest();
+        TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions
+                .makeCustomAnimOptions("testPackage", Resources.ID_NULL,
+                        TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID,
+                        TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID,
+                        Color.GREEN, true /* overrideTaskTransition */);
+        mTransition.setOverrideAnimation(options, null /* startCallback */,
+                null /* finishCallback */);
+
+        mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo);
+
+        final TransitionInfo.Change displayChange = mInfo.getChanges().get(0);
+        final TransitionInfo.Change taskChange = mInfo.getChanges().get(1);
+        final TransitionInfo.Change embeddedTfChange = mInfo.getChanges().get(2);
+        final TransitionInfo.Change activityChange = mInfo.getChanges().get(3);
+
+        assertNull("Display change's AnimationOptions must not be overridden.",
+                displayChange.getAnimationOptions());
+        assertEquals("Task change's AnimationOptions must be overridden.",
+                options, taskChange.getAnimationOptions());
+        assertEquals("Task change's background color must be overridden.",
+                options.getBackgroundColor(), taskChange.getBackgroundColor());
+        assertEquals("Embedded TF change's AnimationOptions must be overridden.",
+                options, embeddedTfChange.getAnimationOptions());
+        assertEquals("Embedded TF change's background color must be overridden.",
+                0, embeddedTfChange.getBackgroundColor());
+        assertEquals("Activity change's AnimationOptions must be overridden.",
+                options, activityChange.getAnimationOptions());
+        assertEquals("Activity change's background color must be overridden.",
+                options.getBackgroundColor(), activityChange.getBackgroundColor());
+    }
+
+    private void initializeOverrideAnimationOptionsTest() {
+        mTransition = createTestTransition(TRANSIT_OPEN);
+
+        // Test set AnimationOptions for Activity and Task.
+        final Task task = createTask(mDisplayContent);
+        // Create an embedded TaskFragment.
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        registerTaskFragmentOrganizer(
+                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()));
+        final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord nonEmbeddedActivity = createActivityRecord(task);
+        mWm.mCurrentUserId = nonEmbeddedActivity.mUserId;
+
+        mTransition.mTargets = new ArrayList<>();
+        mTransition.mTargets.add(new Transition.ChangeInfo(mDisplayContent));
+        mTransition.mTargets.add(new Transition.ChangeInfo(task));
+        mTransition.mTargets.add(new Transition.ChangeInfo(embeddedTf));
+        mTransition.mTargets.add(new Transition.ChangeInfo(nonEmbeddedActivity));
+
+        mInfo = new TransitionInfo(TRANSIT_OPEN, 0 /* flags */);
+        mInfo.addChange(new TransitionInfo.Change(mDisplayContent.mRemoteToken
+                .toWindowContainerToken(), mDisplayContent.getAnimationLeash()));
+        mInfo.addChange(new TransitionInfo.Change(task.mRemoteToken.toWindowContainerToken(),
+                task.getAnimationLeash()));
+        mInfo.addChange(new TransitionInfo.Change(embeddedTf.mRemoteToken.toWindowContainerToken(),
+                embeddedTf.getAnimationLeash()));
+        mInfo.addChange(new TransitionInfo.Change(null /* container */,
+                nonEmbeddedActivity.getAnimationLeash()));
+    }
+
     @Test
     public void testTransitionVisibleChange() {
         registerTestTransitionPlayer();
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index aebae4e..4b83b65 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1078,6 +1078,11 @@
      * @hide
      */
     public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED = 5;
+    /**
+     * Datagram type indicating that the message to be sent or received is of type SMS.
+     * @hide
+     */
+    public static final int DATAGRAM_TYPE_SMS = 6;
 
     /** @hide */
     @IntDef(prefix = "DATAGRAM_TYPE_", value = {
@@ -1086,7 +1091,8 @@
             DATAGRAM_TYPE_LOCATION_SHARING,
             DATAGRAM_TYPE_KEEP_ALIVE,
             DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP,
-            DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED
+            DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED,
+            DATAGRAM_TYPE_SMS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface DatagramType {}
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 238f2af..5121f667 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -119,7 +119,7 @@
     ): UiObject2? {
         if (
             wmHelper.getWindow(innerHelper)?.windowingMode !=
-                WindowingMode.WINDOWING_MODE_FREEFORM.value
+            WindowingMode.WINDOWING_MODE_FREEFORM.value
         )
             error("expected a freeform window with caption but window is not in freeform mode")
         val captions =
@@ -147,7 +147,17 @@
         val endX = startX + horizontalChange
 
         // drag the specified corner of the window to the end coordinate.
-        device.drag(startX, startY, endX, endY, 100)
+        dragWindow(startX, startY, endX, endY, wmHelper, device)
+    }
+
+    /** Drag a window from a source coordinate to a destination coordinate. */
+    fun dragWindow(
+        startX: Int, startY: Int,
+        endX: Int, endY: Int,
+        wmHelper: WindowManagerStateHelper,
+        device: UiDevice
+    ) {
+        device.drag(startX, startY, endX, endY, /* steps= */ 100)
         wmHelper
             .StateSyncBuilder()
             .withAppTransitionIdle()
diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
index d0148fb..abfe549 100644
--- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
+++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
@@ -136,11 +136,20 @@
         assumeTrue(enableVectorCursors())
         assumeTrue(enableVectorCursorA11ySettings())
 
+        val theme: Resources.Theme = context.getResources().newTheme()
+        theme.setTo(context.getTheme())
+        theme.applyStyle(
+            PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK),
+            /* force= */ true)
+        theme.applyStyle(
+            PointerIcon.vectorStrokeStyleToResource(
+                PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE),
+            /* force= */ true)
         val pointerScale = 2f
 
         val pointerIcon =
             PointerIcon.getLoadedSystemIcon(
-                context,
+                ContextThemeWrapper(context, theme),
                 PointerIcon.TYPE_ARROW,
                 /* useLargeIcons= */ false,
                 pointerScale)
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
index f385179..5f9a710 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
@@ -54,6 +54,7 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -110,6 +111,7 @@
     }
 
     @Test
+    @Ignore("b/352823913")
     public void passengerShowImeNotAffectDriver() throws Exception {
         assertDriverImeHidden();
         assertPassengerImeHidden();
diff --git a/tools/lint/fix/README.md b/tools/lint/fix/README.md
index a5ac2be..18bda92 100644
--- a/tools/lint/fix/README.md
+++ b/tools/lint/fix/README.md
@@ -6,7 +6,7 @@
 
 It's a python script that runs the framework linter,
 and then (optionally) copies modified files back into the source tree.\
-Why python, you ask?  Because python is cool ¯\_(ツ)_/¯.
+Why python, you ask? Because python is cool ¯\\\_(ツ)\_/¯.
 
 Incidentally, this exposes a much simpler way to run individual lint checks
 against individual modules, so it's useful beyond applying fixes.
@@ -15,7 +15,7 @@
 
 Lint is not allowed to modify source files directly via lint's `--apply-suggestions` flag.
 As a compromise, soong zips up the (potentially) modified sources and leaves them in an intermediate
-directory.  This script runs the lint, unpacks those files, and copies them back into the tree.
+directory. This script runs the lint, unpacks those files, and copies them back into the tree.
 
 ## How do I run it?
 **WARNING: You probably want to commit/stash any changes to your working tree before doing this...**