Merge changes from topic "DW_WINDOW_DRAG" into main

* changes:
  Add base scenario test for desktop mode window drag.
  Add instrumentation for window drag CUJ in Desktop Windowing mode.
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/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..d560399 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -101,6 +101,7 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.Trace;
+import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.InputType;
 import android.text.Layout;
@@ -4341,6 +4342,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(), UserHandle.myUserId());
+    }
+
+    /**
      * 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/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/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/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/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index cc880e1..48fb2b3 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -91,6 +91,13 @@
 }
 
 flag {
+  name: "camera_compat_fullscreen_pick_same_task_activity"
+  namespace: "large_screen_experiences_app_compat"
+  description: "Limit undo of camera compat treatment to the same task that started the treatment."
+  bug: "350495350"
+}
+
+flag {
   name: "app_compat_refactoring"
   namespace: "large_screen_experiences_app_compat"
   description: "Whether the changes about app compat refactoring are enabled./n"
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/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
index 63623c7..82ae76a 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, int userId);
     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..f50f02e 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, int)}
+     */
+    @AnyThread
+    public void onImeSwitchButtonClickFromClient(int displayId, int userId) {
+        final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
+        if (ops == null) {
+            return;
+        }
+        try {
+            ops.onImeSwitchButtonClickFromClient(displayId, userId);
+        } 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_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index c07fd38..7c62615 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -27,6 +27,7 @@
 #include <android_media_audiopolicy.h>
 #include <android_os_Parcel.h>
 #include <audiomanager/AudioManager.h>
+#include <android-base/properties.h>
 #include <binder/IBinder.h>
 #include <jni.h>
 #include <media/AidlConversion.h>
@@ -41,8 +42,10 @@
 #include <system/audio_policy.h>
 #include <utils/Log.h>
 
+#include <thread>
 #include <optional>
 #include <sstream>
+#include <memory>
 #include <vector>
 
 #include "android_media_AudioAttributes.h"
@@ -261,6 +264,13 @@
     jfieldID mMixerBehavior;
 } gAudioMixerAttributesField;
 
+static struct {
+    jclass clazz;
+    jmethodID run;
+} gRunnableClassInfo;
+
+static JavaVM* gVm;
+
 static Mutex gLock;
 
 enum AudioError {
@@ -3362,6 +3372,55 @@
     return enabled;
 }
 
+class JavaSystemPropertyListener {
+  public:
+    JavaSystemPropertyListener(JNIEnv* env, jobject javaCallback, std::string sysPropName) :
+            mCallback(env->NewGlobalRef(javaCallback)),
+            mCachedProperty(android::base::CachedProperty{std::move(sysPropName)}) {
+        mListenerThread = std::thread([this]() mutable {
+            JNIEnv* threadEnv = GetOrAttachJNIEnvironment(gVm);
+            while (!mCleanupSignal.load()) {
+                using namespace std::chrono_literals;
+                // 1s timeout so this thread can read the cleanup signal to (slowly) be able to
+                // be destroyed.
+                std::string newVal = mCachedProperty.WaitForChange(1000ms) ?: "";
+                if (newVal != "" && mLastVal != newVal) {
+                    threadEnv->CallVoidMethod(mCallback, gRunnableClassInfo.run);
+                    mLastVal = std::move(newVal);
+                }
+            }
+            });
+    }
+
+    ~JavaSystemPropertyListener() {
+        mCleanupSignal.store(true);
+        mListenerThread.join();
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm);
+        env->DeleteGlobalRef(mCallback);
+    }
+
+  private:
+    jobject mCallback;
+    android::base::CachedProperty mCachedProperty;
+    std::thread mListenerThread;
+    std::atomic<bool> mCleanupSignal{false};
+    std::string mLastVal = "";
+};
+
+std::vector<std::unique_ptr<JavaSystemPropertyListener>> gSystemPropertyListeners;
+std::mutex gSysPropLock{};
+
+static void android_media_AudioSystem_listenForSystemPropertyChange(JNIEnv *env,  jobject thiz,
+        jstring sysProp,
+        jobject javaCallback) {
+    ScopedUtfChars sysPropChars{env, sysProp};
+    auto listener = std::make_unique<JavaSystemPropertyListener>(env, javaCallback,
+            std::string{sysPropChars.c_str()});
+    std::unique_lock _l{gSysPropLock};
+    gSystemPropertyListeners.push_back(std::move(listener));
+}
+
+
 // ----------------------------------------------------------------------------
 
 #define MAKE_AUDIO_SYSTEM_METHOD(x) \
@@ -3534,7 +3593,12 @@
                                 android_media_AudioSystem_clearPreferredMixerAttributes),
          MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency),
          MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled),
-         MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled)};
+         MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled),
+         MAKE_JNI_NATIVE_METHOD("listenForSystemPropertyChange",
+                                "(Ljava/lang/String;Ljava/lang/Runnable;)V",
+                                android_media_AudioSystem_listenForSystemPropertyChange),
+
+        };
 
 static const JNINativeMethod gEventHandlerMethods[] =
         {MAKE_JNI_NATIVE_METHOD("native_setup", "(Ljava/lang/Object;)V",
@@ -3816,6 +3880,12 @@
     gAudioMixerAttributesField.mMixerBehavior =
             GetFieldIDOrDie(env, audioMixerAttributesClass, "mMixerBehavior", "I");
 
+    jclass runnableClazz = FindClassOrDie(env, "java/lang/Runnable");
+    gRunnableClassInfo.clazz = MakeGlobalRefOrDie(env, runnableClazz);
+    gRunnableClassInfo.run = GetMethodIDOrDie(env, runnableClazz, "run", "()V");
+
+    LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&gVm) != 0);
+
     AudioSystem::addErrorCallback(android_media_AudioSystem_error_callback);
 
     RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
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/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/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 5807246..d46b2d6 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
@@ -1045,14 +1045,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 +1060,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(
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/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 8558a77..c56671a 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}
@@ -1451,7 +1451,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 +1545,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 +1558,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 +1582,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 +1596,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 +1642,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 +1670,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 +1683,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 +1718,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 +1773,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 +1811,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 +1886,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 +1899,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 +1934,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/AudioSystem.java b/media/java/android/media/AudioSystem.java
index d148afd..52b5ff7 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2649,4 +2649,11 @@
      * @hide
      */
     public static native boolean isBluetoothVariableLatencyEnabled();
+
+    /**
+     * Register a native listener for system property sysprop
+     * @param callback the listener which fires when the property changes
+     * @hide
+     */
+    public static native void listenForSystemPropertyChange(String sysprop, Runnable callback);
 }
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..4013d84 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)
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index df5644b..2645360 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -46,20 +46,6 @@
             </intent-filter>
         </provider>
 
-        <provider android:name="com.android.settingslib.spa.slice.SpaSliceProvider"
-            android:authorities="com.android.spa.gallery.slice.provider"
-            android:exported="true" >
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.app.slice.category.SLICE" />
-            </intent-filter>
-        </provider>
-
-        <receiver
-            android:name="com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver"
-            android:exported="false">
-        </receiver>
-
         <activity
             android:name="com.android.settingslib.spa.debug.BlankActivity"
             android:exported="true">
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 91bd791..ffd2879 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -55,7 +55,6 @@
 import com.android.settingslib.spa.gallery.ui.CopyablePageProvider
 import com.android.settingslib.spa.gallery.scaffold.ScrollablePagerPageProvider
 import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
-import com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver
 
 /**
  * Enum to define all SPP name here.
@@ -120,9 +119,7 @@
     override val logger = DebugLogger()
 
     override val browseActivityClass = GalleryMainActivity::class.java
-    override val sliceBroadcastReceiverClass = SpaSliceBroadcastReceiver::class.java
 
     // For debugging
     override val searchProviderAuthorities = "com.android.spa.gallery.search.provider"
-    override val sliceProviderAuthorities = "com.android.spa.gallery.slice.provider"
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt
index 96de1a7..6d1d346 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt
@@ -27,7 +27,6 @@
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.common.EntrySearchData
-import com.android.settingslib.spa.framework.common.EntrySliceData
 import com.android.settingslib.spa.framework.common.EntryStatusData
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
@@ -35,10 +34,8 @@
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.createIntent
 import com.android.settingslib.spa.gallery.R
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
-import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_SUMMARY
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.AUTO_UPDATE_PREFERENCE_TITLE
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_SUMMARY
@@ -48,15 +45,10 @@
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_KEYWORDS
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_SUMMARY
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_TITLE
-import com.android.settingslib.spa.slice.createBrowsePendingIntent
-import com.android.settingslib.spa.slice.provider.createDemoActionSlice
-import com.android.settingslib.spa.slice.provider.createDemoBrowseSlice
-import com.android.settingslib.spa.slice.provider.createDemoSlice
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro
 import com.android.settingslib.spa.widget.ui.SettingsIcon
-import kotlinx.coroutines.delay
 
 private const val TAG = "PreferencePage"
 
@@ -139,26 +131,6 @@
                             override val enabled = { model.asyncEnable.value }
                         }
                     )
-                }
-                .setSliceDataFn { sliceUri, _ ->
-                    val createSliceImpl = { s: String ->
-                        createDemoBrowseSlice(
-                            sliceUri = sliceUri,
-                            title = ASYNC_PREFERENCE_TITLE,
-                            summary = s,
-                        )
-                    }
-                    return@setSliceDataFn object : EntrySliceData() {
-                        init {
-                            postValue(createSliceImpl("(loading)"))
-                        }
-
-                        override suspend fun asyncRunner() {
-                            spaLogger.message(TAG, "Async entry loading")
-                            delay(2000L)
-                            postValue(createSliceImpl(ASYNC_PREFERENCE_SUMMARY))
-                        }
-                    }
                 }.build()
         )
         entryList.add(
@@ -176,28 +148,6 @@
                             }
                         }
                     )
-                }
-                .setSliceDataFn { sliceUri, args ->
-                    val createSliceImpl = { v: Int ->
-                        createDemoActionSlice(
-                            sliceUri = sliceUri,
-                            title = MANUAL_UPDATE_PREFERENCE_TITLE,
-                            summary = "manual update value $v",
-                        )
-                    }
-
-                    return@setSliceDataFn object : EntrySliceData() {
-                        private var tick = args?.getString("init")?.toInt() ?: 0
-
-                        init {
-                            postValue(createSliceImpl(tick))
-                        }
-
-                        override suspend fun asyncAction() {
-                            tick++
-                            postValue(createSliceImpl(tick))
-                        }
-                    }
                 }.build()
         )
         entryList.add(
@@ -216,33 +166,6 @@
                             }
                         }
                     )
-                }
-                .setSliceDataFn { sliceUri, args ->
-                    val createSliceImpl = { v: Int ->
-                        createDemoBrowseSlice(
-                            sliceUri = sliceUri,
-                            title = AUTO_UPDATE_PREFERENCE_TITLE,
-                            summary = "auto update value $v",
-                        )
-                    }
-
-                    return@setSliceDataFn object : EntrySliceData() {
-                        private var tick = args?.getString("init")?.toInt() ?: 0
-
-                        init {
-                            postValue(createSliceImpl(tick))
-                        }
-
-                        override suspend fun asyncRunner() {
-                            spaLogger.message(TAG, "autoUpdater.active")
-                            while (true) {
-                                delay(1000L)
-                                tick++
-                                spaLogger.message(TAG, "autoUpdater.value $tick")
-                                postValue(createSliceImpl(tick))
-                            }
-                        }
-                    }
                 }.build()
         )
 
@@ -272,22 +195,6 @@
                     clickRoute = SettingsPageProviderEnum.PREFERENCE.name
                 )
             }
-            .setSliceDataFn { sliceUri, _ ->
-                val intent = owner.createIntent()?.createBrowsePendingIntent()
-                    ?: return@setSliceDataFn null
-                return@setSliceDataFn object : EntrySliceData() {
-                    init {
-                        postValue(
-                            createDemoSlice(
-                                sliceUri = sliceUri,
-                                title = PAGE_TITLE,
-                                summary = "Injected Entry",
-                                intent = intent,
-                            )
-                        )
-                    }
-                }
-            }
     }
 
     override fun getTitle(arguments: Bundle?): String {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index 2d956d5..6e5132b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -17,12 +17,10 @@
 package com.android.settingslib.spa.framework.common
 
 import android.app.Activity
-import android.content.BroadcastReceiver
 import android.content.Context
 import android.util.Log
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.platform.LocalContext
-import com.android.settingslib.spa.slice.SettingsSliceDataRepository
 
 private const val TAG = "SpaEnvironment"
 
@@ -69,8 +67,6 @@
 
     val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) }
 
-    val sliceDataRepository = lazy { SettingsSliceDataRepository(entryRepository.value) }
-
     // The application context. Use local context as fallback when applicationContext is not
     // available (e.g. in Robolectric test).
     val appContext: Context = context.applicationContext ?: context
@@ -81,11 +77,9 @@
     // Specify class name of browse activity and slice broadcast receiver, which is used to
     // generate the necessary intents.
     open val browseActivityClass: Class<out Activity>? = null
-    open val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? = null
 
     // Specify provider authorities for debugging purpose.
     open val searchProviderAuthorities: String? = null
-    open val sliceProviderAuthorities: String? = null
 
     // TODO: add other environment setup here.
     companion object {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
deleted file mode 100644
index 7a4750d..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.slice
-
-import android.net.Uri
-import android.util.Log
-import com.android.settingslib.spa.framework.common.EntrySliceData
-import com.android.settingslib.spa.framework.common.SettingsEntryRepository
-import com.android.settingslib.spa.framework.util.getEntryId
-
-private const val TAG = "SliceDataRepository"
-
-class SettingsSliceDataRepository(private val entryRepository: SettingsEntryRepository) {
-    // The map of slice uri to its EntrySliceData, a.k.a. LiveData<Slice?>
-    private val sliceDataMap: MutableMap<String, EntrySliceData> = mutableMapOf()
-
-    // Note: mark this function synchronized, so that we can get the same livedata during the
-    // whole lifecycle of a Slice.
-    @Synchronized
-    fun getOrBuildSliceData(sliceUri: Uri): EntrySliceData? {
-        val sliceString = sliceUri.getSliceId() ?: return null
-        return sliceDataMap[sliceString] ?: buildLiveDataImpl(sliceUri)?.let {
-            sliceDataMap[sliceString] = it
-            it
-        }
-    }
-
-    fun getActiveSliceData(sliceUri: Uri): EntrySliceData? {
-        val sliceString = sliceUri.getSliceId() ?: return null
-        val sliceData = sliceDataMap[sliceString] ?: return null
-        return if (sliceData.isActive()) sliceData else null
-    }
-
-    private fun buildLiveDataImpl(sliceUri: Uri): EntrySliceData? {
-        Log.d(TAG, "buildLiveData: $sliceUri")
-
-        val entryId = sliceUri.getEntryId() ?: return null
-        val entry = entryRepository.getEntry(entryId) ?: return null
-        if (!entry.hasSliceSupport) return null
-        val arguments = sliceUri.getRuntimeArguments()
-        return entry.getSliceData(runtimeArguments = arguments, sliceUri = sliceUri)
-    }
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
index f362890..ec89c7c 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
@@ -16,23 +16,10 @@
 
 package com.android.settingslib.spa.slice
 
-import android.app.Activity
-import android.app.PendingIntent
-import android.content.BroadcastReceiver
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
 import android.net.Uri
 import android.os.Bundle
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.util.KEY_DESTINATION
 import com.android.settingslib.spa.framework.util.KEY_HIGHLIGHT_ENTRY
-import com.android.settingslib.spa.framework.util.SESSION_SLICE
-import com.android.settingslib.spa.framework.util.SPA_INTENT_RESERVED_KEYS
-import com.android.settingslib.spa.framework.util.appendSpaParams
-import com.android.settingslib.spa.framework.util.getDestination
-import com.android.settingslib.spa.framework.util.getEntryId
 
 // Defines SliceUri, which contains special query parameters:
 //  -- KEY_DESTINATION: The route that this slice is navigated to.
@@ -45,25 +32,6 @@
     return getQueryParameter(KEY_HIGHLIGHT_ENTRY)
 }
 
-fun SliceUri.getDestination(): String? {
-    return getQueryParameter(KEY_DESTINATION)
-}
-
-fun SliceUri.getRuntimeArguments(): Bundle {
-    val params = Bundle()
-    for (queryName in queryParameterNames) {
-        if (SPA_INTENT_RESERVED_KEYS.contains(queryName)) continue
-        params.putString(queryName, getQueryParameter(queryName))
-    }
-    return params
-}
-
-fun SliceUri.getSliceId(): String? {
-    val entryId = getEntryId() ?: return null
-    val params = getRuntimeArguments()
-    return "${entryId}_$params"
-}
-
 fun Uri.Builder.appendSpaParams(
     destination: String? = null,
     entryId: String? = null,
@@ -79,72 +47,3 @@
     return this
 }
 
-fun Uri.Builder.fromEntry(
-    entry: SettingsEntry,
-    authority: String?,
-    runtimeArguments: Bundle? = null
-): Uri.Builder {
-    if (authority == null) return this
-    val sp = entry.containerPage()
-    return scheme("content").authority(authority).appendSpaParams(
-        destination = sp.buildRoute(),
-        entryId = entry.id,
-        runtimeArguments = runtimeArguments
-    )
-}
-
-fun SliceUri.createBroadcastPendingIntent(): PendingIntent? {
-    val context = SpaEnvironmentFactory.instance.appContext
-    val sliceBroadcastClass =
-        SpaEnvironmentFactory.instance.sliceBroadcastReceiverClass ?: return null
-    val entryId = getEntryId() ?: return null
-    return createBroadcastPendingIntent(context, sliceBroadcastClass, entryId)
-}
-
-fun SliceUri.createBrowsePendingIntent(): PendingIntent? {
-    val context = SpaEnvironmentFactory.instance.appContext
-    val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
-    val destination = getDestination() ?: return null
-    val entryId = getEntryId()
-    return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
-}
-
-fun Intent.createBrowsePendingIntent(): PendingIntent? {
-    val context = SpaEnvironmentFactory.instance.appContext
-    val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
-    val destination = getDestination() ?: return null
-    val entryId = getEntryId()
-    return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
-}
-
-private fun createBrowsePendingIntent(
-    context: Context,
-    browseActivityClass: Class<out Activity>,
-    destination: String,
-    entryId: String?
-): PendingIntent {
-    val intent = Intent().setComponent(ComponentName(context, browseActivityClass))
-        .appendSpaParams(destination, entryId, SESSION_SLICE)
-        .apply {
-            // Set both extra and data (which is a Uri) in Slice Intent:
-            // 1) extra is used in SPA navigation framework
-            // 2) data is used in Slice framework
-            data = Uri.Builder().appendSpaParams(destination, entryId).build()
-            flags = Intent.FLAG_ACTIVITY_NEW_TASK
-        }
-
-    return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
-}
-
-private fun createBroadcastPendingIntent(
-    context: Context,
-    sliceBroadcastClass: Class<out BroadcastReceiver>,
-    entryId: String
-): PendingIntent {
-    val intent = Intent().setComponent(ComponentName(context, sliceBroadcastClass))
-        .apply { data = Uri.Builder().appendSpaParams(entryId = entryId).build() }
-    return PendingIntent.getBroadcast(
-        context, 0 /* requestCode */, intent,
-        PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
-    )
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceBroadcastReceiver.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceBroadcastReceiver.kt
deleted file mode 100644
index 39cb431..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceBroadcastReceiver.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.slice
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-
-class SpaSliceBroadcastReceiver : BroadcastReceiver() {
-    override fun onReceive(context: Context?, intent: Intent?) {
-        val sliceRepository by SpaEnvironmentFactory.instance.sliceDataRepository
-        val sliceUri = intent?.data ?: return
-        val sliceData = sliceRepository.getActiveSliceData(sliceUri) ?: return
-        sliceData.doAction()
-    }
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceProvider.kt
deleted file mode 100644
index 3496f02..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceProvider.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.slice
-
-import android.net.Uri
-import android.util.Log
-import androidx.lifecycle.Observer
-import androidx.slice.Slice
-import androidx.slice.SliceProvider
-import com.android.settingslib.spa.framework.common.EntrySliceData
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
-
-private const val TAG = "SpaSliceProvider"
-
-class SpaSliceProvider : SliceProvider(), Observer<Slice?> {
-    private fun getOrPutSliceData(sliceUri: Uri): EntrySliceData? {
-        if (!SpaEnvironmentFactory.isReady()) return null
-        val sliceRepository by SpaEnvironmentFactory.instance.sliceDataRepository
-        return sliceRepository.getOrBuildSliceData(sliceUri)
-    }
-
-    override fun onBindSlice(sliceUri: Uri): Slice? {
-        if (context == null) return null
-        Log.d(TAG, "onBindSlice: $sliceUri")
-        return getOrPutSliceData(sliceUri)?.value
-    }
-
-    override fun onSlicePinned(sliceUri: Uri) {
-        Log.d(TAG, "onSlicePinned: $sliceUri")
-        super.onSlicePinned(sliceUri)
-        val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
-        runBlocking {
-            withContext(Dispatchers.Main) {
-                sliceLiveData.observeForever(this@SpaSliceProvider)
-            }
-        }
-    }
-
-    override fun onSliceUnpinned(sliceUri: Uri) {
-        Log.d(TAG, "onSliceUnpinned: $sliceUri")
-        super.onSliceUnpinned(sliceUri)
-        val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
-        runBlocking {
-            withContext(Dispatchers.Main) {
-                sliceLiveData.removeObserver(this@SpaSliceProvider)
-            }
-        }
-    }
-
-    override fun onChanged(value: Slice?) {
-        val uri = value?.uri ?: return
-        Log.d(TAG, "onChanged: $uri")
-        context?.contentResolver?.notifyChange(uri, null)
-    }
-
-    override fun onCreateSliceProvider(): Boolean {
-        Log.d(TAG, "onCreateSliceProvider")
-        return true
-    }
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
deleted file mode 100644
index 007f47b..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.slice.presenter
-
-import android.net.Uri
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.slice.widget.SliceLiveData
-import androidx.slice.widget.SliceView
-
-@Composable
-fun SliceDemo(sliceUri: Uri) {
-    val context = LocalContext.current
-    val lifecycleOwner = LocalLifecycleOwner.current
-    val sliceData = remember {
-        SliceLiveData.fromUri(context, sliceUri)
-    }
-
-    HorizontalDivider()
-    AndroidView(
-        factory = { localContext ->
-            val view = SliceView(localContext)
-            view.setShowTitleItems(true)
-            view.isScrollable = false
-            view
-        },
-        update = { view -> sliceData.observe(lifecycleOwner, view) }
-    )
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
deleted file mode 100644
index e4a7386..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.slice.provider
-
-import android.app.PendingIntent
-import android.content.Context
-import android.net.Uri
-import androidx.core.R
-import androidx.core.graphics.drawable.IconCompat
-import androidx.slice.Slice
-import androidx.slice.SliceManager
-import androidx.slice.builders.ListBuilder
-import androidx.slice.builders.SliceAction
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.slice.createBroadcastPendingIntent
-import com.android.settingslib.spa.slice.createBrowsePendingIntent
-
-fun createDemoBrowseSlice(sliceUri: Uri, title: String, summary: String): Slice? {
-    val intent = sliceUri.createBrowsePendingIntent() ?: return null
-    return createDemoSlice(sliceUri, title, summary, intent)
-}
-
-fun createDemoActionSlice(sliceUri: Uri, title: String, summary: String): Slice? {
-    val intent = sliceUri.createBroadcastPendingIntent() ?: return null
-    return createDemoSlice(sliceUri, title, summary, intent)
-}
-
-fun createDemoSlice(sliceUri: Uri, title: String, summary: String, intent: PendingIntent): Slice? {
-    val context = SpaEnvironmentFactory.instance.appContext
-    if (!SliceManager.getInstance(context).pinnedSlices.contains(sliceUri)) return null
-    return ListBuilder(context, sliceUri, ListBuilder.INFINITY)
-        .addRow(ListBuilder.RowBuilder().apply {
-            setPrimaryAction(createSliceAction(context, intent))
-            setTitle(title)
-            setSubtitle(summary)
-        }).build()
-}
-
-private fun createSliceAction(context: Context, intent: PendingIntent): SliceAction {
-    return SliceAction.create(
-        intent,
-        IconCompat.createWithResource(context, R.drawable.notification_action_background),
-        ListBuilder.ICON_IMAGE,
-        "Enter app"
-    )
-}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
deleted file mode 100644
index 341a4a5..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.slice
-
-import android.content.Context
-import android.net.Uri
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.lifecycle.Observer
-import androidx.slice.Slice
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.framework.common.createSettingsPage
-import com.android.settingslib.spa.framework.util.genEntryId
-import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
-import com.android.settingslib.spa.tests.testutils.SppHome
-import com.android.settingslib.spa.tests.testutils.SppLayer2
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class SettingsSliceDataRepositoryTest {
-    @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
-
-    private val context: Context = ApplicationProvider.getApplicationContext()
-    private val spaEnvironment =
-        SpaEnvironmentForTest(context, listOf(SppHome.createSettingsPage()))
-    private val sliceDataRepository by spaEnvironment.sliceDataRepository
-
-    @Test
-    fun getOrBuildSliceDataTest() {
-        SpaEnvironmentFactory.reset(spaEnvironment)
-
-        // Slice empty
-        assertThat(sliceDataRepository.getOrBuildSliceData(Uri.EMPTY)).isNull()
-
-        // Slice supported
-        val page = SppLayer2.createSettingsPage()
-        val entryId = genEntryId("Layer2Entry1", page)
-        val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build()
-        assertThat(sliceUri.getDestination()).isEqualTo("SppLayer2")
-        assertThat(sliceUri.getSliceId()).isEqualTo("${entryId}_Bundle[{}]")
-        val sliceData = sliceDataRepository.getOrBuildSliceData(sliceUri)
-        assertThat(sliceData).isNotNull()
-        assertThat(sliceDataRepository.getOrBuildSliceData(sliceUri)).isSameInstanceAs(sliceData)
-
-        // Slice unsupported
-        val entryId2 = genEntryId("Layer2Entry2", page)
-        val sliceUri2 = Uri.Builder().appendSpaParams(page.buildRoute(), entryId2).build()
-        assertThat(sliceUri2.getDestination()).isEqualTo("SppLayer2")
-        assertThat(sliceUri2.getSliceId()).isEqualTo("${entryId2}_Bundle[{}]")
-        assertThat(sliceDataRepository.getOrBuildSliceData(sliceUri2)).isNull()
-    }
-
-    @Test
-    fun getActiveSliceDataTest() {
-        SpaEnvironmentFactory.reset(spaEnvironment)
-
-        val page = SppLayer2.createSettingsPage()
-        val entryId = genEntryId("Layer2Entry1", page)
-        val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build()
-
-        // build slice data first
-        val sliceData = sliceDataRepository.getOrBuildSliceData(sliceUri)
-
-        // slice data is inactive
-        assertThat(sliceData!!.isActive()).isFalse()
-        assertThat(sliceDataRepository.getActiveSliceData(sliceUri)).isNull()
-
-        // slice data is active
-        val observer = Observer<Slice?> { }
-        sliceData.observeForever(observer)
-        assertThat(sliceData.isActive()).isTrue()
-        assertThat(sliceDataRepository.getActiveSliceData(sliceUri)).isSameInstanceAs(sliceData)
-
-        // slice data is inactive again
-        sliceData.removeObserver(observer)
-        assertThat(sliceData.isActive()).isFalse()
-        assertThat(sliceDataRepository.getActiveSliceData(sliceUri)).isNull()
-    }
-}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt
index d1c4e51..b489afd 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt
@@ -16,91 +16,27 @@
 
 package com.android.settingslib.spa.slice
 
-import android.content.Context
-import android.content.Intent
 import android.net.Uri
 import androidx.core.os.bundleOf
-import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 class SliceUtilTest {
-    private val context: Context = ApplicationProvider.getApplicationContext()
-    private val spaEnvironment = SpaEnvironmentForTest(context)
-
     @Test
     fun sliceUriTest() {
         assertThat(Uri.EMPTY.getEntryId()).isNull()
-        assertThat(Uri.EMPTY.getDestination()).isNull()
-        assertThat(Uri.EMPTY.getRuntimeArguments().size()).isEqualTo(0)
-        assertThat(Uri.EMPTY.getSliceId()).isNull()
 
         // valid slice uri
         val dest = "myRoute"
         val entryId = "myEntry"
         val sliceUriWithoutParams = Uri.Builder().appendSpaParams(dest, entryId).build()
         assertThat(sliceUriWithoutParams.getEntryId()).isEqualTo(entryId)
-        assertThat(sliceUriWithoutParams.getDestination()).isEqualTo(dest)
-        assertThat(sliceUriWithoutParams.getRuntimeArguments().size()).isEqualTo(0)
-        assertThat(sliceUriWithoutParams.getSliceId()).isEqualTo("${entryId}_Bundle[{}]")
 
         val sliceUriWithParams =
             Uri.Builder().appendSpaParams(dest, entryId, bundleOf("p1" to "v1")).build()
         assertThat(sliceUriWithParams.getEntryId()).isEqualTo(entryId)
-        assertThat(sliceUriWithParams.getDestination()).isEqualTo(dest)
-        assertThat(sliceUriWithParams.getRuntimeArguments().size()).isEqualTo(1)
-        assertThat(sliceUriWithParams.getSliceId()).isEqualTo("${entryId}_Bundle[{p1=v1}]")
-    }
-
-    @Test
-    fun createBroadcastPendingIntentTest() {
-        SpaEnvironmentFactory.reset(spaEnvironment)
-
-        // Empty Slice Uri
-        assertThat(Uri.EMPTY.createBroadcastPendingIntent()).isNull()
-
-        // Valid Slice Uri
-        val dest = "myRoute"
-        val entryId = "myEntry"
-        val sliceUriWithoutParams = Uri.Builder().appendSpaParams(dest, entryId).build()
-        val pendingIntent = sliceUriWithoutParams.createBroadcastPendingIntent()
-        assertThat(pendingIntent).isNotNull()
-        assertThat(pendingIntent!!.isBroadcast).isTrue()
-        assertThat(pendingIntent.isImmutable).isFalse()
-    }
-
-    @Test
-    fun createBrowsePendingIntentTest() {
-        SpaEnvironmentFactory.reset(spaEnvironment)
-
-        // Empty Slice Uri
-        assertThat(Uri.EMPTY.createBrowsePendingIntent()).isNull()
-
-        // Empty Intent
-        assertThat(Intent().createBrowsePendingIntent()).isNull()
-
-        // Valid Slice Uri
-        val dest = "myRoute"
-        val entryId = "myEntry"
-        val sliceUri = Uri.Builder().appendSpaParams(dest, entryId).build()
-        val pendingIntent = sliceUri.createBrowsePendingIntent()
-        assertThat(pendingIntent).isNotNull()
-        assertThat(pendingIntent!!.isActivity).isTrue()
-        assertThat(pendingIntent.isImmutable).isTrue()
-
-        // Valid Intent
-        val intent = Intent().apply {
-            putExtra("spaActivityDestination", dest)
-            putExtra("highlightEntry", entryId)
-        }
-        val pendingIntent2 = intent.createBrowsePendingIntent()
-        assertThat(pendingIntent2).isNotNull()
-        assertThat(pendingIntent2!!.isActivity).isTrue()
-        assertThat(pendingIntent2.isImmutable).isTrue()
     }
 }
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
index 22a5ca3..4f8fd79 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
@@ -216,8 +216,6 @@
     context: Context,
     rootPages: List<SettingsPage> = emptyList(),
     override val browseActivityClass: Class<out Activity>? = BlankActivity::class.java,
-    override val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? =
-        BlankSliceBroadcastReceiver::class.java,
     override val logger: SpaLogger = object : SpaLogger {}
 ) : SpaEnvironment(context) {
 
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..26b5741 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
@@ -17,6 +17,7 @@
 package com.android.settingslib.notification.data.repository
 
 import android.app.NotificationManager
+import android.app.NotificationManager.EXTRA_NOTIFICATION_POLICY
 import android.content.BroadcastReceiver
 import android.content.ContentResolver
 import android.content.Context
@@ -74,7 +75,7 @@
                 val receiver =
                     object : BroadcastReceiver() {
                         override fun onReceive(context: Context?, intent: Intent?) {
-                            intent?.action?.let { action -> launch { send(action) } }
+                            intent?.let { launch { send(it) } }
                         }
                     }
 
@@ -112,7 +113,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 +129,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/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..4bd5cc4 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,6 +22,7 @@
 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.provider.Settings.Global
@@ -126,6 +127,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 +195,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/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..bd4710b 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
@@ -40,6 +40,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 +65,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
@@ -963,9 +965,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
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/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index d95b388..20b1303 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -184,31 +184,33 @@
     ): Swipes {
         val fromSource =
             startedPosition?.let { position ->
-                layoutImpl.swipeSourceDetector.source(
-                    fromScene.targetSize,
-                    position.round(),
-                    layoutImpl.density,
-                    orientation,
-                )
+                layoutImpl.swipeSourceDetector
+                    .source(
+                        fromScene.targetSize,
+                        position.round(),
+                        layoutImpl.density,
+                        orientation,
+                    )
+                    ?.resolve(layoutImpl.layoutDirection)
             }
 
         val upOrLeft =
-            Swipe(
+            Swipe.Resolved(
                 direction =
                     when (orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Left
-                        Orientation.Vertical -> SwipeDirection.Up
+                        Orientation.Horizontal -> SwipeDirection.Resolved.Left
+                        Orientation.Vertical -> SwipeDirection.Resolved.Up
                     },
                 pointerCount = pointersDown,
                 fromSource = fromSource,
             )
 
         val downOrRight =
-            Swipe(
+            Swipe.Resolved(
                 direction =
                     when (orientation) {
-                        Orientation.Horizontal -> SwipeDirection.Right
-                        Orientation.Vertical -> SwipeDirection.Down
+                        Orientation.Horizontal -> SwipeDirection.Resolved.Right
+                        Orientation.Vertical -> SwipeDirection.Resolved.Down
                     },
                 pointerCount = pointersDown,
                 fromSource = fromSource,
@@ -833,10 +835,10 @@
 
 /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
 private class Swipes(
-    val upOrLeft: Swipe?,
-    val downOrRight: Swipe?,
-    val upOrLeftNoSource: Swipe?,
-    val downOrRightNoSource: Swipe?,
+    val upOrLeft: Swipe.Resolved?,
+    val downOrRight: Swipe.Resolved?,
+    val upOrLeftNoSource: Swipe.Resolved?,
+    val downOrRightNoSource: Swipe.Resolved?,
 ) {
     /** The [UserActionResult] associated to up and down swipes. */
     var upOrLeftResult: UserActionResult? = null
@@ -844,7 +846,7 @@
 
     fun computeSwipesResults(fromScene: Scene): Pair<UserActionResult?, UserActionResult?> {
         val userActions = fromScene.userActions
-        fun result(swipe: Swipe?): UserActionResult? {
+        fun result(swipe: Swipe.Resolved?): UserActionResult? {
             return userActions[swipe ?: return null]
         }
 
@@ -940,25 +942,27 @@
                 when {
                     amount < 0f -> {
                         val actionUpOrLeft =
-                            Swipe(
+                            Swipe.Resolved(
                                 direction =
                                     when (orientation) {
-                                        Orientation.Horizontal -> SwipeDirection.Left
-                                        Orientation.Vertical -> SwipeDirection.Up
+                                        Orientation.Horizontal -> SwipeDirection.Resolved.Left
+                                        Orientation.Vertical -> SwipeDirection.Resolved.Up
                                     },
                                 pointerCount = pointersInfo().pointersDown,
+                                fromSource = null,
                             )
                         fromScene.userActions[actionUpOrLeft]
                     }
                     amount > 0f -> {
                         val actionDownOrRight =
-                            Swipe(
+                            Swipe.Resolved(
                                 direction =
                                     when (orientation) {
-                                        Orientation.Horizontal -> SwipeDirection.Right
-                                        Orientation.Vertical -> SwipeDirection.Down
+                                        Orientation.Horizontal -> SwipeDirection.Resolved.Right
+                                        Orientation.Vertical -> SwipeDirection.Resolved.Down
                                     },
                                 pointerCount = pointersInfo().pointersDown,
+                                fromSource = null,
                             )
                         fromScene.userActions[actionDownOrRight]
                     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
index b0dc3a1..97c0cef 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
@@ -21,14 +21,28 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 
 /** The edge of a [SceneTransitionLayout]. */
-enum class Edge : SwipeSource {
-    Left,
-    Right,
-    Top,
-    Bottom,
+enum class Edge(private val resolveEdge: (LayoutDirection) -> Resolved) : SwipeSource {
+    Top(resolveEdge = { Resolved.Top }),
+    Bottom(resolveEdge = { Resolved.Bottom }),
+    Left(resolveEdge = { Resolved.Left }),
+    Right(resolveEdge = { Resolved.Right }),
+    Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
+    End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
+
+    override fun resolve(layoutDirection: LayoutDirection): Resolved {
+        return resolveEdge(layoutDirection)
+    }
+
+    enum class Resolved : SwipeSource.Resolved {
+        Left,
+        Right,
+        Top,
+        Bottom,
+    }
 }
 
 val DefaultEdgeDetector = FixedSizeEdgeDetector(40.dp)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 936f4ba..a49f1af 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -37,7 +37,7 @@
     val key: SceneKey,
     layoutImpl: SceneTransitionLayoutImpl,
     content: @Composable SceneScope.() -> Unit,
-    actions: Map<UserAction, UserActionResult>,
+    actions: Map<UserAction.Resolved, UserActionResult>,
     zIndex: Float,
 ) {
     internal val scope = SceneScopeImpl(layoutImpl, this)
@@ -54,8 +54,8 @@
         }
 
     private fun checkValid(
-        userActions: Map<UserAction, UserActionResult>
-    ): Map<UserAction, UserActionResult> {
+        userActions: Map<UserAction.Resolved, UserActionResult>
+    ): Map<UserAction.Resolved, UserActionResult> {
         userActions.forEach { (action, result) ->
             if (key == result.toScene) {
                 error(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 45758c5..0c467b1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -28,10 +28,13 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.UserAction.Resolved
 
 /**
  * [SceneTransitionLayout] is a container that automatically animates its content whenever its state
@@ -344,34 +347,71 @@
 @Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope
 
 /** An action performed by the user. */
-sealed interface UserAction {
+sealed class UserAction {
     infix fun to(scene: SceneKey): Pair<UserAction, UserActionResult> {
         return this to UserActionResult(toScene = scene)
     }
+
+    /** Resolve this into a [Resolved] user action given [layoutDirection]. */
+    internal abstract fun resolve(layoutDirection: LayoutDirection): Resolved
+
+    /** A resolved [UserAction] that does not depend on the layout direction. */
+    internal sealed class Resolved
 }
 
 /** The user navigated back, either using a gesture or by triggering a KEYCODE_BACK event. */
-data object Back : UserAction
+data object Back : UserAction() {
+    override fun resolve(layoutDirection: LayoutDirection): Resolved = Resolved
+
+    internal object Resolved : UserAction.Resolved()
+}
 
 /** The user swiped on the container. */
 data class Swipe(
     val direction: SwipeDirection,
     val pointerCount: Int = 1,
     val fromSource: SwipeSource? = null,
-) : UserAction {
+) : UserAction() {
     companion object {
         val Left = Swipe(SwipeDirection.Left)
         val Up = Swipe(SwipeDirection.Up)
         val Right = Swipe(SwipeDirection.Right)
         val Down = Swipe(SwipeDirection.Down)
+        val Start = Swipe(SwipeDirection.Start)
+        val End = Swipe(SwipeDirection.End)
     }
+
+    override fun resolve(layoutDirection: LayoutDirection): UserAction.Resolved {
+        return Resolved(
+            direction = direction.resolve(layoutDirection),
+            pointerCount = pointerCount,
+            fromSource = fromSource?.resolve(layoutDirection),
+        )
+    }
+
+    /** A resolved [Swipe] that does not depend on the layout direction. */
+    internal data class Resolved(
+        val direction: SwipeDirection.Resolved,
+        val pointerCount: Int,
+        val fromSource: SwipeSource.Resolved?,
+    ) : UserAction.Resolved()
 }
 
-enum class SwipeDirection(val orientation: Orientation) {
-    Up(Orientation.Vertical),
-    Down(Orientation.Vertical),
-    Left(Orientation.Horizontal),
-    Right(Orientation.Horizontal),
+enum class SwipeDirection(internal val resolve: (LayoutDirection) -> Resolved) {
+    Up(resolve = { Resolved.Up }),
+    Down(resolve = { Resolved.Down }),
+    Left(resolve = { Resolved.Left }),
+    Right(resolve = { Resolved.Right }),
+    Start(resolve = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
+    End(resolve = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
+
+    /** A resolved [SwipeDirection] that does not depend on the layout direction. */
+    internal enum class Resolved(val orientation: Orientation) {
+        Up(Orientation.Vertical),
+        Down(Orientation.Vertical),
+        Left(Orientation.Horizontal),
+        Right(Orientation.Horizontal),
+    }
 }
 
 /**
@@ -386,6 +426,16 @@
     override fun equals(other: Any?): Boolean
 
     override fun hashCode(): Int
+
+    /** Resolve this into a [Resolved] swipe source given [layoutDirection]. */
+    fun resolve(layoutDirection: LayoutDirection): Resolved
+
+    /** A resolved [SwipeSource] that does not depend on the layout direction. */
+    interface Resolved {
+        override fun equals(other: Any?): Boolean
+
+        override fun hashCode(): Int
+    }
 }
 
 interface SwipeSourceDetector {
@@ -460,11 +510,13 @@
     scenes: SceneTransitionLayoutScope.() -> Unit,
 ) {
     val density = LocalDensity.current
+    val layoutDirection = LocalLayoutDirection.current
     val coroutineScope = rememberCoroutineScope()
     val layoutImpl = remember {
         SceneTransitionLayoutImpl(
                 state = state as BaseSceneTransitionLayoutState,
                 density = density,
+                layoutDirection = layoutDirection,
                 swipeSourceDetector = swipeSourceDetector,
                 transitionInterceptionThreshold = transitionInterceptionThreshold,
                 builder = scenes,
@@ -475,7 +527,7 @@
 
     // TODO(b/317014852): Move this into the SideEffect {} again once STLImpl.scenes is not a
     // SnapshotStateMap anymore.
-    layoutImpl.updateScenes(scenes)
+    layoutImpl.updateScenes(scenes, layoutDirection)
 
     SideEffect {
         if (state != layoutImpl.state) {
@@ -486,6 +538,7 @@
         }
 
         layoutImpl.density = density
+        layoutImpl.layoutDirection = layoutDirection
         layoutImpl.swipeSourceDetector = swipeSourceDetector
         layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 6095419..3e48c42 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -33,6 +33,7 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachReversed
 import com.android.compose.ui.util.lerp
@@ -45,6 +46,7 @@
 internal class SceneTransitionLayoutImpl(
     internal val state: BaseSceneTransitionLayoutState,
     internal var density: Density,
+    internal var layoutDirection: LayoutDirection,
     internal var swipeSourceDetector: SwipeSourceDetector,
     internal var transitionInterceptionThreshold: Float,
     builder: SceneTransitionLayoutScope.() -> Unit,
@@ -114,7 +116,7 @@
         private set
 
     init {
-        updateScenes(builder)
+        updateScenes(builder, layoutDirection)
 
         // DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the
         // current scene (required for SwipeTransition).
@@ -147,7 +149,10 @@
         return scenes[key] ?: error("Scene $key is not configured")
     }
 
-    internal fun updateScenes(builder: SceneTransitionLayoutScope.() -> Unit) {
+    internal fun updateScenes(
+        builder: SceneTransitionLayoutScope.() -> Unit,
+        layoutDirection: LayoutDirection,
+    ) {
         // Keep a reference of the current scenes. After processing [builder], the scenes that were
         // not configured will be removed.
         val scenesToRemove = scenes.keys.toMutableSet()
@@ -163,11 +168,13 @@
                 ) {
                     scenesToRemove.remove(key)
 
+                    val resolvedUserActions =
+                        userActions.mapKeys { it.key.resolve(layoutDirection) }
                     val scene = scenes[key]
                     if (scene != null) {
                         // Update an existing scene.
                         scene.content = content
-                        scene.userActions = userActions
+                        scene.userActions = resolvedUserActions
                         scene.zIndex = zIndex
                     } else {
                         // New scene.
@@ -176,7 +183,7 @@
                                 key,
                                 this@SceneTransitionLayoutImpl,
                                 content,
-                                userActions,
+                                resolvedUserActions,
                                 zIndex,
                             )
                     }
@@ -213,7 +220,7 @@
     @Composable
     private fun BackHandler() {
         val targetSceneForBack =
-            scene(state.transitionState.currentScene).userActions[Back]?.toScene
+            scene(state.transitionState.currentScene).userActions[Back.Resolved]?.toScene
         PredictiveBackHandler(state, coroutineScope, targetSceneForBack)
     }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 171e243..aeb6262 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -98,7 +98,9 @@
 
     /** Whether swipe should be enabled in the given [orientation]. */
     private fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean {
-        return userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
+        return userActions.keys.any {
+            it is Swipe.Resolved && it.direction.orientation == orientation
+        }
     }
 
     private fun startDragImmediately(startedPosition: Offset): Boolean {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index aa8dc38..7daefd0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -44,26 +44,26 @@
             return value
         }
 
-        return when (edge) {
-            Edge.Top ->
+        return when (edge.resolve(layoutImpl.layoutDirection)) {
+            Edge.Resolved.Top ->
                 if (startsOutsideLayoutBounds) {
                     Offset(value.x, -elementSize.height.toFloat())
                 } else {
                     Offset(value.x, 0f)
                 }
-            Edge.Left ->
+            Edge.Resolved.Left ->
                 if (startsOutsideLayoutBounds) {
                     Offset(-elementSize.width.toFloat(), value.y)
                 } else {
                     Offset(0f, value.y)
                 }
-            Edge.Bottom ->
+            Edge.Resolved.Bottom ->
                 if (startsOutsideLayoutBounds) {
                     Offset(value.x, sceneSize.height.toFloat())
                 } else {
                     Offset(value.x, (sceneSize.height - elementSize.height).toFloat())
                 }
-            Edge.Right ->
+            Edge.Resolved.Right ->
                 if (startsOutsideLayoutBounds) {
                     Offset(sceneSize.width.toFloat(), value.y)
                 } else {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index ff83d4b..7a5a84e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.Velocity
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.compose.animation.scene.NestedScrollBehavior.DuringTransitionBetweenScenes
@@ -61,8 +62,24 @@
                 canChangeScene = { canChangeScene(it) },
             )
 
-        val mutableUserActionsA = mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC)
-        val mutableUserActionsB = mutableMapOf(Swipe.Up to SceneC, Swipe.Down to SceneA)
+        var layoutDirection = LayoutDirection.Rtl
+            set(value) {
+                field = value
+                layoutImpl.updateScenes(scenesBuilder, layoutDirection)
+            }
+
+        var mutableUserActionsA = mapOf(Swipe.Up to SceneB, Swipe.Down to SceneC)
+            set(value) {
+                field = value
+                layoutImpl.updateScenes(scenesBuilder, layoutDirection)
+            }
+
+        var mutableUserActionsB = mapOf(Swipe.Up to SceneC, Swipe.Down to SceneA)
+            set(value) {
+                field = value
+                layoutImpl.updateScenes(scenesBuilder, layoutDirection)
+            }
+
         private val scenesBuilder: SceneTransitionLayoutScope.() -> Unit = {
             scene(
                 key = SceneA,
@@ -94,6 +111,7 @@
             SceneTransitionLayoutImpl(
                     state = layoutState,
                     density = Density(1f),
+                    layoutDirection = LayoutDirection.Ltr,
                     swipeSourceDetector = DefaultEdgeDetector,
                     transitionInterceptionThreshold = transitionInterceptionThreshold,
                     builder = scenesBuilder,
@@ -466,10 +484,8 @@
         dragController1.onDragStopped(velocity = -velocityThreshold)
         assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB)
 
-        mutableUserActionsA.remove(Swipe.Up)
-        mutableUserActionsA.remove(Swipe.Down)
-        mutableUserActionsB.remove(Swipe.Up)
-        mutableUserActionsB.remove(Swipe.Down)
+        mutableUserActionsA = emptyMap()
+        mutableUserActionsB = emptyMap()
 
         // start accelaratedScroll and scroll over to B -> null
         val dragController2 = onDragStartedImmediately()
@@ -495,7 +511,7 @@
         val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f)
 
-        mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC)
+        mutableUserActionsA += Swipe.Up to UserActionResult(SceneC)
         dragController1.onDragDelta(pixels = up(fractionOfScreen = 0.1f))
         // target stays B even though UserActions changed
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f)
@@ -512,7 +528,7 @@
         val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f)
 
-        mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC)
+        mutableUserActionsA += Swipe.Up to UserActionResult(SceneC)
         dragController1.onDragDelta(pixels = up(fractionOfScreen = 0.1f))
         dragController1.onDragStopped(velocity = down(fractionOfScreen = 0.1f))
 
@@ -1149,8 +1165,7 @@
             overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
         }
 
-        mutableUserActionsA.clear()
-        mutableUserActionsA[Swipe.Up] = UserActionResult(SceneB)
+        mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB))
 
         val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
         val dragController = onDragStarted(startedPosition = middle, overSlop = down(1f))
@@ -1178,8 +1193,7 @@
             overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
         }
 
-        mutableUserActionsA.clear()
-        mutableUserActionsA[Swipe.Down] = UserActionResult(SceneC)
+        mutableUserActionsA = mapOf(Swipe.Down to UserActionResult(SceneC))
 
         val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
         val dragController = onDragStarted(startedPosition = middle, overSlop = up(1f))
@@ -1220,7 +1234,8 @@
 
     @Test
     fun requireFullDistanceSwipe() = runGestureTest {
-        mutableUserActionsA[Swipe.Up] = UserActionResult(SceneB, requiresFullDistanceSwipe = true)
+        mutableUserActionsA +=
+            Swipe.Up to UserActionResult(SceneB, requiresFullDistanceSwipe = true)
 
         val controller = onDragStarted(overSlop = up(fractionOfScreen = 0.9f))
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.9f)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 25ea2ee..0766e00 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -23,11 +23,13 @@
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
@@ -37,10 +39,12 @@
 import androidx.compose.ui.test.swipeWithVelocity
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.compose.animation.scene.TestScenes.SceneA
 import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.TestScenes.SceneC
 import com.android.compose.animation.scene.subjects.assertThat
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -634,4 +638,152 @@
         // Foo should be translated by (20dp, 30dp).
         rule.onNode(isElement(TestElements.Foo)).assertPositionInRootIsEqualTo(20.dp, 30.dp)
     }
+
+    @Test
+    fun startEnd_ltrLayout() {
+        val state =
+            rule.runOnUiThread {
+                MutableSceneTransitionLayoutState(
+                    initialScene = SceneA,
+                    transitions =
+                        transitions {
+                            from(SceneA, to = SceneB) {
+                                // We go to B by swiping to the start (left in LTR), so we make
+                                // scene B appear from the end (right) edge.
+                                translate(SceneB.rootElementKey, Edge.End)
+                            }
+
+                            from(SceneA, to = SceneC) {
+                                // We go to C by swiping to the end (right in LTR), so we make
+                                // scene C appear from the start (left) edge.
+                                translate(SceneC.rootElementKey, Edge.Start)
+                            }
+                        },
+                )
+            }
+
+        val layoutSize = 200.dp
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+                scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
+                    Box(Modifier.fillMaxSize())
+                }
+                scene(SceneB) { Box(Modifier.element(SceneB.rootElementKey).fillMaxSize()) }
+                scene(SceneC) { Box(Modifier.element(SceneC.rootElementKey).fillMaxSize()) }
+            }
+        }
+
+        // Swipe to the left (start).
+        rule.onRoot().performTouchInput {
+            val middle = (layoutSize / 2).toPx()
+            down(Offset(middle, middle))
+            moveBy(Offset(-touchSlop, 0f), delayMillis = 1_000)
+        }
+
+        // Scene B should come from the right (end) edge.
+        var transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneB)
+        rule
+            .onNode(isElement(SceneB.rootElementKey))
+            .assertPositionInRootIsEqualTo(layoutSize, 0.dp)
+
+        // Release to go back to A.
+        rule.onRoot().performTouchInput { up() }
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneA)
+
+        // Swipe to the right (end).
+        rule.onRoot().performTouchInput {
+            val middle = (layoutSize / 2).toPx()
+            down(Offset(middle, middle))
+            moveBy(Offset(touchSlop, 0f), delayMillis = 1_000)
+        }
+
+        // Scene C should come from the left (start) edge.
+        transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneC)
+        rule
+            .onNode(isElement(SceneC.rootElementKey))
+            .assertPositionInRootIsEqualTo(-layoutSize, 0.dp)
+    }
+
+    @Test
+    fun startEnd_rtlLayout() {
+        val state =
+            rule.runOnUiThread {
+                MutableSceneTransitionLayoutState(
+                    initialScene = SceneA,
+                    transitions =
+                        transitions {
+                            from(SceneA, to = SceneB) {
+                                // We go to B by swiping to the start (right in RTL), so we make
+                                // scene B appear from the end (left) edge.
+                                translate(SceneB.rootElementKey, Edge.End)
+                            }
+
+                            from(SceneA, to = SceneC) {
+                                // We go to C by swiping to the end (left in RTL), so we make
+                                // scene C appear from the start (right) edge.
+                                translate(SceneC.rootElementKey, Edge.Start)
+                            }
+                        },
+                )
+            }
+
+        val layoutSize = 200.dp
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+                    scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
+                        Box(Modifier.fillMaxSize())
+                    }
+                    scene(SceneB) { Box(Modifier.element(SceneB.rootElementKey).fillMaxSize()) }
+                    scene(SceneC) { Box(Modifier.element(SceneC.rootElementKey).fillMaxSize()) }
+                }
+            }
+        }
+
+        // Swipe to the left (end).
+        rule.onRoot().performTouchInput {
+            val middle = (layoutSize / 2).toPx()
+            down(Offset(middle, middle))
+            moveBy(Offset(-touchSlop, 0f), delayMillis = 1_000)
+        }
+
+        // Scene C should come from the right (start) edge.
+        var transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneC)
+        rule
+            .onNode(isElement(SceneC.rootElementKey))
+            .assertPositionInRootIsEqualTo(layoutSize, 0.dp)
+
+        // Release to go back to A.
+        rule.onRoot().performTouchInput { up() }
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneA)
+
+        // Swipe to the right (start).
+        rule.onRoot().performTouchInput {
+            val middle = (layoutSize / 2).toPx()
+            down(Offset(middle, middle))
+            moveBy(Offset(touchSlop, 0f), delayMillis = 1_000)
+        }
+
+        // Scene C should come from the left (end) edge.
+        transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneB)
+        rule
+            .onNode(isElement(SceneB.rootElementKey))
+            .assertPositionInRootIsEqualTo(-layoutSize, 0.dp)
+    }
 }
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/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/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/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/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/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/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/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/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/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/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/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/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index da89eea..766c391 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -526,7 +526,7 @@
                 keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
                         mContext.getString(R.string.keyboard_shortcut_group_applications_assist),
                         assistIcon,
-                        KeyEvent.KEYCODE_UNKNOWN,
+                        KeyEvent.KEYCODE_A,
                         KeyEvent.META_META_ON));
             }
         }
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/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
index 97791ac..316e1f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
@@ -22,6 +22,7 @@
 import android.hardware.biometrics.BiometricSourceType
 import android.provider.Settings
 import com.android.app.tracing.ListenersTracing.forEachTraced
+import com.android.app.tracing.coroutines.launch
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -159,7 +160,7 @@
     }
 
     fun listenForQsExpandedChange() =
-        applicationScope.launch {
+        applicationScope.launch("listenForQsExpandedChange") {
             shadeInteractorLazy.get().qsExpansion.map { it > 0f }.distinctUntilChanged()
                 .collect { isQsExpanded ->
                     val changed = qsExpanded != isQsExpanded
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/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/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/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/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/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/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/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/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/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/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0d309eb..a84306b 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -739,6 +739,8 @@
     // Broadcast receiver for device connections intent broadcasts
     private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
 
+    private final Executor mAudioServerLifecycleExecutor;
+
     private IMediaProjectionManager mProjectionService; // to validate projection token
 
     /** Interface for UserManagerService. */
@@ -1059,7 +1061,8 @@
                               audioserverPermissions() ?
                                 initializeAudioServerPermissionProvider(
                                     context, audioPolicyFacade, audioserverLifecycleExecutor) :
-                                    null
+                                    null,
+                              audioserverLifecycleExecutor
                               );
         }
 
@@ -1145,13 +1148,16 @@
      *               {@link AudioSystemThread} is created as the messaging thread instead.
      * @param appOps {@link AppOpsManager} system service
      * @param enforcer Used for permission enforcing
+     * @param permissionProvider Used to push permissions to audioserver
+     * @param audioserverLifecycleExecutor Used for tasks managing audioserver lifecycle
      */
     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public AudioService(Context context, AudioSystemAdapter audioSystem,
             SystemServerAdapter systemServer, SettingsAdapter settings,
             AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy,
             @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer,
-            /* @NonNull */ AudioServerPermissionProvider permissionProvider) {
+            /* @NonNull */ AudioServerPermissionProvider permissionProvider,
+            Executor audioserverLifecycleExecutor) {
         super(enforcer);
         sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()"));
         mContext = context;
@@ -1159,6 +1165,7 @@
         mAppOps = appOps;
 
         mPermissionProvider = permissionProvider;
+        mAudioServerLifecycleExecutor = audioserverLifecycleExecutor;
 
         mAudioSystem = audioSystem;
         mSystemServer = systemServer;
@@ -1170,6 +1177,34 @@
         mBroadcastHandlerThread = new HandlerThread("AudioService Broadcast");
         mBroadcastHandlerThread.start();
 
+        // Listen to permission invalidations for the PermissionProvider
+        if (audioserverPermissions()) {
+            final Handler broadcastHandler = mBroadcastHandlerThread.getThreadHandler();
+            mAudioSystem.listenForSystemPropertyChange(PermissionManager.CACHE_KEY_PACKAGE_INFO,
+                    new Runnable() {
+                        // Roughly chosen to be long enough to suppress the autocork behavior
+                        // of the permission cache (50ms), and longer than the task could reasonably
+                        // take, even with many packages and users, while not introducing visible
+                        // permission leaks - since the app needs to restart, and trigger an action
+                        // which requires permissions from audioserver before this delay.
+                        // For RECORD_AUDIO, we are additionally protected by appops.
+                        final long UPDATE_DELAY_MS = 110;
+                        final AtomicLong scheduledUpdateTimestamp = new AtomicLong(0);
+                        @Override
+                        public void run() {
+                            var currentTime = SystemClock.uptimeMillis();
+                            if (currentTime > scheduledUpdateTimestamp.get()) {
+                                scheduledUpdateTimestamp.set(currentTime + UPDATE_DELAY_MS);
+                                broadcastHandler.postAtTime( () ->
+                                        mAudioServerLifecycleExecutor.execute(mPermissionProvider
+                                            ::onPermissionStateChanged),
+                                        currentTime + UPDATE_DELAY_MS
+                                    );
+                            }
+                        }
+            });
+        }
+
         mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem);
 
         mIsSingleVolume = AudioSystem.isSingleVolume(context);
@@ -11965,29 +12000,6 @@
             provider.onServiceStart(audioPolicy.getPermissionController());
         });
 
-        // Set up event listeners
-        // Must be kept in sync with PermissionManager
-        Runnable cacheSysPropHandler = new Runnable() {
-            private AtomicReference<SystemProperties.Handle> mHandle = new AtomicReference();
-            private AtomicLong mNonce = new AtomicLong();
-            @Override
-            public void run() {
-                if (mHandle.get() == null) {
-                    // Cache the handle
-                    mHandle.compareAndSet(null, SystemProperties.find(
-                            PermissionManager.CACHE_KEY_PACKAGE_INFO));
-                }
-                long nonce;
-                SystemProperties.Handle ref;
-                if ((ref = mHandle.get()) != null && (nonce = ref.getLong(0)) != 0 &&
-                        mNonce.getAndSet(nonce) != nonce) {
-                    audioserverExecutor.execute(() -> provider.onPermissionStateChanged());
-                }
-            }
-        };
-
-        SystemProperties.addChangeCallback(cacheSysPropHandler);
-
         IntentFilter packageUpdateFilter = new IntentFilter();
         packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED);
         packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED);
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 7f4bc74..d083c68 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -748,6 +748,10 @@
         return AudioSystem.setMasterMute(mute);
     }
 
+    public void listenForSystemPropertyChange(String systemPropertyName, Runnable callback) {
+        AudioSystem.listenForSystemPropertyChange(systemPropertyName, callback);
+    }
+
     /**
      * Part of AudioService dump
      * @param pw
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/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/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..c093c22 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;
@@ -3967,6 +3968,29 @@
         }
     }
 
+    @BinderThread
+    private void onImeSwitchButtonClickFromClient(@NonNull IBinder token, int displayId,
+            @UserIdInt int userId) {
+        userId = mActivityManagerInternal.handleIncomingUser(
+                Binder.getCallingPid(), Binder.getCallingUid(), userId, false,
+                ActivityManagerInternal.ALLOW_FULL_ONLY, "onImeSwitchButtonClickFromClient", null);
+
+        synchronized (ImfLock.class) {
+            if (!calledWithValidTokenLocked(token, userId)) {
+                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) {
@@ -4112,7 +4136,8 @@
         final var currentImi = bindingController.getSelectedMethod();
         final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController
                 .getNextInputMethodLocked(onlyCurrentIme, currentImi,
-                        bindingController.getCurrentSubtype());
+                        bindingController.getCurrentSubtype(),
+                        MODE_AUTO, true /* forward */);
         if (nextSubtype == null) {
             return false;
         }
@@ -4132,7 +4157,8 @@
             final var currentImi = bindingController.getSelectedMethod();
             final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController
                     .getNextInputMethodLocked(false /* onlyCurrentIme */, currentImi,
-                            bindingController.getCurrentSubtype());
+                            bindingController.getCurrentSubtype(),
+                            MODE_AUTO, true /* forward */);
             return nextSubtype != null;
         }
     }
@@ -5457,6 +5483,10 @@
             // Set InputMethod here
             settings.putSelectedInputMethod(imi != null ? imi.getId() : "");
         }
+
+        if (Flags.imeSwitcherRevamp()) {
+            getUserData(userId).mSwitchingController.onInputMethodSubtypeChanged();
+        }
     }
 
     @GuardedBy("ImfLock.class")
@@ -5577,11 +5607,29 @@
         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 = getUserData(userId).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 =
+                    getUserData(userId).mHardwareKeyboardShortcutController.onSubtypeSwitch(
                         currentSubtypeHandle, direction > 0);
+        }
         if (nextSubtypeHandle == null) {
             return;
         }
@@ -6905,6 +6953,12 @@
 
         @BinderThread
         @Override
+        public void onImeSwitchButtonClickFromClient(int displayId, @UserIdInt int userId) {
+            mImms.onImeSwitchButtonClickFromClient(mToken, displayId, userId);
+        }
+
+        @BinderThread
+        @Override
         public void notifyUserActionAsync() {
             mImms.notifyUserAction(mToken, mUserId);
         }
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/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/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/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/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/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/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 2a3e945..5336044 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2126,6 +2126,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 +2143,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 +2571,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/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/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/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
index 758c84a..ef9580c 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
@@ -101,7 +101,7 @@
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy,
                 mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
-                mock(AudioServerPermissionProvider.class)) {
+                mock(AudioServerPermissionProvider.class), r -> r.run()) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 2cb02bd..4645156 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -78,7 +78,7 @@
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
                 mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
-                mock(AudioServerPermissionProvider.class)) {
+                mock(AudioServerPermissionProvider.class), r -> r.run()) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index 037c3c0..b7100ea 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -87,7 +87,7 @@
                 .thenReturn(AppOpsManager.MODE_ALLOWED);
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null,
-                mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider);
+                mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider, r -> r.run());
     }
 
     /**
diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
index 27b552f..746645a 100644
--- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
@@ -78,7 +78,7 @@
         mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
                 mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
-                mock(AudioServerPermissionProvider.class));
+                mock(AudioServerPermissionProvider.class), r -> r.run());
         mTestLooper.dispatchAll();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index 8e34ee1..e45ab31 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -160,7 +160,7 @@
                 @NonNull PermissionEnforcer enforcer,
                 AudioServerPermissionProvider permissionProvider) {
             super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper,
-                    audioPolicy, looper, appOps, enforcer, permissionProvider);
+                    audioPolicy, looper, appOps, enforcer, permissionProvider, r -> r.run());
         }
 
         public void setDeviceForStream(int stream, int device) {
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/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...**