Merge "Adding more logs to keyboard/touchpad tutorial" into main
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 4350545..5db79fe 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -8288,12 +8288,12 @@
             }
             Context c = null;
             ApplicationInfo ai = info.applicationInfo;
-            if (context.getPackageName().equals(ai.packageName)) {
+            if (context != null && context.getPackageName().equals(ai.packageName)) {
                 c = context;
             } else if (mInitialApplication != null &&
                     mInitialApplication.getPackageName().equals(ai.packageName)) {
                 c = mInitialApplication;
-            } else {
+            } else if (context != null) {
                 try {
                     c = context.createPackageContext(ai.packageName,
                             Context.CONTEXT_INCLUDE_CODE);
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 52c84dc..26f919f 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -778,8 +778,18 @@
     public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
         logErrorForInvalidProfileAccess(user);
         try {
-            return convertToActivityList(mService.getLauncherActivities(mContext.getPackageName(),
-                    packageName, user), user);
+            final List<LauncherActivityInfo> activityList = convertToActivityList(
+                    mService.getLauncherActivities(
+                            mContext.getPackageName(),
+                            packageName,
+                            user
+                    ), user);
+            if (activityList.isEmpty()) {
+                // b/350144057
+                Log.d(TAG, "getActivityList: No launchable activities found for"
+                        + "packageName=" + packageName + ", user=" + user);
+            }
+            return activityList;
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
index 4ddf602..b5fb050 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
@@ -205,10 +205,6 @@
      */
     @SuppressWarnings("AndroidFrameworkCompatChange")
     public static boolean isCameraDeviceSetupSupported(CameraCharacteristics chars) {
-        if (!Flags.featureCombinationQuery()) {
-            return false;
-        }
-
         Integer queryVersion = chars.get(
                 CameraCharacteristics.INFO_SESSION_CONFIGURATION_QUERY_VERSION);
         return queryVersion != null && queryVersion > Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index 9bd4860..50c6b5b 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -165,12 +165,10 @@
         source.readTypedList(outConfigs, OutputConfiguration.CREATOR);
         // Ignore the values for hasSessionParameters and settings because we cannot reconstruct
         // the CaptureRequest object.
-        if (Flags.featureCombinationQuery()) {
-            boolean hasSessionParameters = source.readBoolean();
-            if (hasSessionParameters) {
-                CameraMetadataNative settings = new CameraMetadataNative();
-                settings.readFromParcel(source);
-            }
+        boolean hasSessionParameters = source.readBoolean();
+        if (hasSessionParameters) {
+            CameraMetadataNative settings = new CameraMetadataNative();
+            settings.readFromParcel(source);
         }
 
         if ((inputWidth > 0) && (inputHeight > 0) && (inputFormat != -1)) {
@@ -212,14 +210,12 @@
             dest.writeBoolean(/*isMultiResolution*/ false);
         }
         dest.writeTypedList(mOutputConfigurations);
-        if (Flags.featureCombinationQuery()) {
-            if (mSessionParameters != null) {
-                dest.writeBoolean(/*hasSessionParameters*/true);
-                CameraMetadataNative metadata = mSessionParameters.getNativeCopy();
-                metadata.writeToParcel(dest, /*flags*/0);
-            } else {
-                dest.writeBoolean(/*hasSessionParameters*/false);
-            }
+        if (mSessionParameters != null) {
+            dest.writeBoolean(/*hasSessionParameters*/true);
+            CameraMetadataNative metadata = mSessionParameters.getNativeCopy();
+            metadata.writeToParcel(dest, /*flags*/0);
+        } else {
+            dest.writeBoolean(/*hasSessionParameters*/false);
         }
     }
 
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 1a309c6..983bbc3 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -119,6 +119,13 @@
 }
 
 flag {
+    namespace: "input_native"
+    name: "use_key_gesture_event_handler_multi_press_gestures"
+    description: "Use KeyGestureEvent handler APIs to control multi key press gestures"
+    bug: "358569822"
+}
+
+flag {
   name: "keyboard_repeat_keys"
   namespace: "input_native"
   description: "Allow configurable timeout before key repeat and repeat delay rate for key repeats"
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index a4a7a98..1ca4574 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -3763,7 +3763,8 @@
     }
 
     private static final String CACHE_KEY_IS_USER_UNLOCKED_PROPERTY =
-            "cache_key.is_user_unlocked";
+        PropertyInvalidatedCache.createPropertyName(
+            PropertyInvalidatedCache.MODULE_SYSTEM, "is_user_unlocked");
 
     private final PropertyInvalidatedCache<Integer, Boolean> mIsUserUnlockedCache =
             new PropertyInvalidatedCache<Integer, Boolean>(
@@ -6694,7 +6695,9 @@
     }
 
     /* Cache key for anything that assumes that userIds cannot be re-used without rebooting. */
-    private static final String CACHE_KEY_STATIC_USER_PROPERTIES = "cache_key.static_user_props";
+    private static final String CACHE_KEY_STATIC_USER_PROPERTIES =
+        PropertyInvalidatedCache.createPropertyName(
+            PropertyInvalidatedCache.MODULE_SYSTEM, "static_user_props");
 
     private final PropertyInvalidatedCache<Integer, String> mProfileTypeCache =
             new PropertyInvalidatedCache<Integer, String>(32, CACHE_KEY_STATIC_USER_PROPERTIES) {
@@ -6721,7 +6724,9 @@
     }
 
     /* Cache key for UserProperties object. */
-    private static final String CACHE_KEY_USER_PROPERTIES = "cache_key.user_properties";
+    private static final String CACHE_KEY_USER_PROPERTIES =
+        PropertyInvalidatedCache.createPropertyName(
+            PropertyInvalidatedCache.MODULE_SYSTEM, "user_properties");
 
     // TODO: It would be better to somehow have this as static, so that it can work cross-context.
     private final PropertyInvalidatedCache<Integer, UserProperties> mUserPropertiesCache =
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 77dde5e..d45b24e 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1046,6 +1046,8 @@
                             DEFAULT_SUPPRESSED_VISUAL_EFFECTS);
                 } else if (MANUAL_TAG.equals(tag)) {
                     rt.manualRule = readRuleXml(parser);
+                    // manualRule.enabled can never be false, but it was broken in some builds.
+                    rt.manualRule.enabled = true;
                     // Manual rule may be present prior to modes_ui if it were on, but in that
                     // case it would not have a set policy, so make note of the need to set
                     // it up later.
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index ad457ce..384add5 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -70,6 +70,7 @@
 import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -370,6 +371,7 @@
         private float mDefaultDimAmount = 0.05f;
         SurfaceControl mBbqSurfaceControl;
         BLASTBufferQueue mBlastBufferQueue;
+        IBinder mBbqApplyToken = new Binder();
         private SurfaceControl mScreenshotSurfaceControl;
         private Point mScreenshotSize = new Point();
 
@@ -2390,6 +2392,7 @@
             if (mBlastBufferQueue == null) {
                 mBlastBufferQueue = new BLASTBufferQueue("Wallpaper", mBbqSurfaceControl,
                         width, height, format);
+                mBlastBufferQueue.setApplyToken(mBbqApplyToken);
                 // We only return the Surface the first time, as otherwise
                 // it hasn't changed and there is no need to update.
                 ret = mBlastBufferQueue.createSurface();
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index f8c97eb..53935e8 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1341,7 +1341,7 @@
     public HdrCapabilities getHdrCapabilities() {
         synchronized (mLock) {
             updateDisplayInfoLocked();
-            if (mDisplayInfo.hdrCapabilities == null) {
+            if (mDisplayInfo.hdrCapabilities == null || mDisplayInfo.isForceSdr) {
                 return null;
             }
             int[] supportedHdrTypes;
@@ -1363,6 +1363,7 @@
                     supportedHdrTypes[index++] = enabledType;
                 }
             }
+
             return new HdrCapabilities(supportedHdrTypes,
                     mDisplayInfo.hdrCapabilities.mMaxLuminance,
                     mDisplayInfo.hdrCapabilities.mMaxAverageLuminance,
@@ -2087,6 +2088,7 @@
     /**
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static String stateToString(int state) {
         switch (state) {
             case STATE_UNKNOWN:
@@ -2109,6 +2111,7 @@
     }
 
     /** @hide */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static String stateReasonToString(@StateReason int reason) {
         switch (reason) {
             case STATE_REASON_UNKNOWN:
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 157cec8..cac3e3c 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -230,6 +230,9 @@
     /** The formats disabled by user **/
     public int[] userDisabledHdrTypes = {};
 
+    /** When true, all HDR capabilities are disabled **/
+    public boolean isForceSdr;
+
     /**
      * Indicates whether the display can be switched into a mode with minimal post
      * processing.
@@ -440,6 +443,7 @@
                 && colorMode == other.colorMode
                 && Arrays.equals(supportedColorModes, other.supportedColorModes)
                 && Objects.equals(hdrCapabilities, other.hdrCapabilities)
+                && isForceSdr == other.isForceSdr
                 && Arrays.equals(userDisabledHdrTypes, other.userDisabledHdrTypes)
                 && minimalPostProcessingSupported == other.minimalPostProcessingSupported
                 && logicalDensityDpi == other.logicalDensityDpi
@@ -502,6 +506,7 @@
         supportedColorModes = Arrays.copyOf(
                 other.supportedColorModes, other.supportedColorModes.length);
         hdrCapabilities = other.hdrCapabilities;
+        isForceSdr = other.isForceSdr;
         userDisabledHdrTypes = other.userDisabledHdrTypes;
         minimalPostProcessingSupported = other.minimalPostProcessingSupported;
         logicalDensityDpi = other.logicalDensityDpi;
@@ -567,6 +572,7 @@
             supportedColorModes[i] = source.readInt();
         }
         hdrCapabilities = source.readParcelable(null, android.view.Display.HdrCapabilities.class);
+        isForceSdr = source.readBoolean();
         minimalPostProcessingSupported = source.readBoolean();
         logicalDensityDpi = source.readInt();
         physicalXDpi = source.readFloat();
@@ -636,6 +642,7 @@
             dest.writeInt(supportedColorModes[i]);
         }
         dest.writeParcelable(hdrCapabilities, flags);
+        dest.writeBoolean(isForceSdr);
         dest.writeBoolean(minimalPostProcessingSupported);
         dest.writeInt(logicalDensityDpi);
         dest.writeFloat(physicalXDpi);
@@ -874,6 +881,8 @@
         sb.append(Arrays.toString(appsSupportedModes));
         sb.append(", hdrCapabilities ");
         sb.append(hdrCapabilities);
+        sb.append(", isForceSdr ");
+        sb.append(isForceSdr);
         sb.append(", userDisabledHdrTypes ");
         sb.append(Arrays.toString(userDisabledHdrTypes));
         sb.append(", minimalPostProcessingSupported ");
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index dd950e8..b21e85a 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -174,24 +174,26 @@
     @IntDef(prefix = {"POINTER_ICON_VECTOR_STYLE_FILL_"}, value = {
             POINTER_ICON_VECTOR_STYLE_FILL_BLACK,
             POINTER_ICON_VECTOR_STYLE_FILL_GREEN,
-            POINTER_ICON_VECTOR_STYLE_FILL_YELLOW,
+            POINTER_ICON_VECTOR_STYLE_FILL_RED,
             POINTER_ICON_VECTOR_STYLE_FILL_PINK,
-            POINTER_ICON_VECTOR_STYLE_FILL_BLUE
+            POINTER_ICON_VECTOR_STYLE_FILL_BLUE,
+            POINTER_ICON_VECTOR_STYLE_FILL_PURPLE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface PointerIconVectorStyleFill {}
 
     /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_BLACK = 0;
     /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_GREEN = 1;
-    /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_YELLOW = 2;
+    /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_RED = 2;
     /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_PINK = 3;
     /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_BLUE = 4;
+    /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_PURPLE = 5;
 
     // If adding a PointerIconVectorStyleFill, update END value for {@link SystemSettingsValidators}
     /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_BEGIN =
             POINTER_ICON_VECTOR_STYLE_FILL_BLACK;
     /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_END =
-            POINTER_ICON_VECTOR_STYLE_FILL_BLUE;
+            POINTER_ICON_VECTOR_STYLE_FILL_PURPLE;
 
     /** @hide */
     @IntDef(prefix = {"POINTER_ICON_VECTOR_STYLE_STROKE_"}, value = {
@@ -712,12 +714,14 @@
                     com.android.internal.R.style.PointerIconVectorStyleFillBlack;
             case POINTER_ICON_VECTOR_STYLE_FILL_GREEN ->
                     com.android.internal.R.style.PointerIconVectorStyleFillGreen;
-            case POINTER_ICON_VECTOR_STYLE_FILL_YELLOW ->
-                    com.android.internal.R.style.PointerIconVectorStyleFillYellow;
+            case POINTER_ICON_VECTOR_STYLE_FILL_RED ->
+                    com.android.internal.R.style.PointerIconVectorStyleFillRed;
             case POINTER_ICON_VECTOR_STYLE_FILL_PINK ->
                     com.android.internal.R.style.PointerIconVectorStyleFillPink;
             case POINTER_ICON_VECTOR_STYLE_FILL_BLUE ->
                     com.android.internal.R.style.PointerIconVectorStyleFillBlue;
+            case POINTER_ICON_VECTOR_STYLE_FILL_PURPLE ->
+                    com.android.internal.R.style.PointerIconVectorStyleFillPurple;
             default -> com.android.internal.R.style.PointerIconVectorStyleFillBlack;
         };
     }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e10cc28..9ff5031 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -829,6 +829,7 @@
     private final SurfaceControl mSurfaceControl = new SurfaceControl();
 
     private BLASTBufferQueue mBlastBufferQueue;
+    private IBinder mBbqApplyToken = new Binder();
 
     private final HdrRenderState mHdrRenderState = new HdrRenderState(this);
 
@@ -2743,6 +2744,10 @@
         mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,
                 mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);
         mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);
+        // If we create and destroy BBQ without recreating the SurfaceControl, we can end up
+        // queuing buffers on multiple apply tokens causing out of order buffer submissions. We
+        // fix this by setting the same apply token on all BBQs created by this VRI.
+        mBlastBufferQueue.setApplyToken(mBbqApplyToken);
         Surface blastSurface;
         if (addSchandleToVriSurface()) {
             blastSurface = mBlastBufferQueue.createSurfaceWithHandle();
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 1c4b16e..cc5e583 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -117,6 +117,13 @@
 }
 
 flag {
+    name: "enable_tile_resizing"
+    namespace: "lse_desktop_experience"
+    description: "Enables drawing a divider bar upon tiling tasks left and right in desktop mode for simultaneous resizing"
+    bug: "351769839"
+}
+
+flag {
     name: "respect_orientation_change_for_unresizeable"
     namespace: "lse_desktop_experience"
     description: "Whether to resize task to respect requested orientation change of unresizeable activity"
diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp
index 70505a4..b9c3bf7 100644
--- a/core/jni/android_graphics_BLASTBufferQueue.cpp
+++ b/core/jni/android_graphics_BLASTBufferQueue.cpp
@@ -16,16 +16,16 @@
 
 #define LOG_TAG "BLASTBufferQueue"
 
-#include <nativehelper/JNIHelp.h>
-
 #include <android_runtime/AndroidRuntime.h>
 #include <android_runtime/android_view_Surface.h>
-#include <utils/Log.h>
-#include <utils/RefBase.h>
-
+#include <android_util_Binder.h>
 #include <gui/BLASTBufferQueue.h>
 #include <gui/Surface.h>
 #include <gui/SurfaceComposerClient.h>
+#include <nativehelper/JNIHelp.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
 #include "core_jni_helpers.h"
 
 namespace android {
@@ -209,6 +209,12 @@
                           reinterpret_cast<jlong>(transaction));
 }
 
+static void nativeSetApplyToken(JNIEnv* env, jclass clazz, jlong ptr, jobject applyTokenObject) {
+    sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
+    sp<IBinder> token(ibinderForJavaObject(env, applyTokenObject));
+    return queue->setApplyToken(std::move(token));
+}
+
 static const JNINativeMethod gMethods[] = {
         /* name, signature, funcPtr */
         // clang-format off
@@ -227,6 +233,7 @@
         {"nativeSetTransactionHangCallback",
          "(JLandroid/graphics/BLASTBufferQueue$TransactionHangCallback;)V",
          (void*)nativeSetTransactionHangCallback},
+        {"nativeSetApplyToken", "(JLandroid/os/IBinder;)V", (void*)nativeSetApplyToken},
         // clang-format on
 };
 
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index dc99634..579dc91 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1509,7 +1509,7 @@
     </style>
 
     <!-- @hide -->
-    <style name="PointerIconVectorStyleFillYellow">
+    <style name="PointerIconVectorStyleFillRed">
         <item name="pointerIconVectorFill">#F55E57</item>
         <item name="pointerIconVectorFillInverse">#F55E57</item>
     </style>
@@ -1527,6 +1527,12 @@
     </style>
 
     <!-- @hide -->
+    <style name="PointerIconVectorStyleFillPurple">
+        <item name="pointerIconVectorFill">#AD72FF</item>
+        <item name="pointerIconVectorFillInverse">#AD72FF</item>
+    </style>
+
+    <!-- @hide -->
     <style name="PointerIconVectorStyleStrokeWhite">
         <item name="pointerIconVectorStroke">@color/white</item>
         <item name="pointerIconVectorStrokeInverse">@color/black</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 1917ecd..0396659 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1705,9 +1705,10 @@
   <java-symbol type="style" name="VectorPointer" />
   <java-symbol type="style" name="PointerIconVectorStyleFillBlack" />
   <java-symbol type="style" name="PointerIconVectorStyleFillGreen" />
-  <java-symbol type="style" name="PointerIconVectorStyleFillYellow" />
+  <java-symbol type="style" name="PointerIconVectorStyleFillRed" />
   <java-symbol type="style" name="PointerIconVectorStyleFillPink" />
   <java-symbol type="style" name="PointerIconVectorStyleFillBlue" />
+  <java-symbol type="style" name="PointerIconVectorStyleFillPurple" />
   <java-symbol type="attr" name="pointerIconVectorFill" />
   <java-symbol type="style" name="PointerIconVectorStyleStrokeWhite" />
   <java-symbol type="style" name="PointerIconVectorStyleStrokeBlack" />
diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java
index c52f700..90723b2 100644
--- a/graphics/java/android/graphics/BLASTBufferQueue.java
+++ b/graphics/java/android/graphics/BLASTBufferQueue.java
@@ -17,6 +17,7 @@
 package android.graphics;
 
 import android.annotation.NonNull;
+import android.os.IBinder;
 import android.view.Surface;
 import android.view.SurfaceControl;
 
@@ -47,6 +48,7 @@
             long frameNumber);
     private static native void nativeSetTransactionHangCallback(long ptr,
             TransactionHangCallback callback);
+    private static native void nativeSetApplyToken(long ptr, IBinder applyToken);
 
     public interface TransactionHangCallback {
         void onTransactionHang(String reason);
@@ -204,4 +206,8 @@
     public void setTransactionHangCallback(TransactionHangCallback hangCallback) {
         nativeSetTransactionHangCallback(mNativeObject, hangCallback);
     }
+
+    public void setApplyToken(IBinder applyToken) {
+        nativeSetApplyToken(mNativeObject, applyToken);
+    }
 }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
index 9027bf3..88878c6 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
@@ -40,6 +40,7 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
+import android.app.TaskInfo;
 import android.app.WindowConfiguration;
 import android.graphics.Rect;
 import android.util.ArrayMap;
@@ -339,6 +340,52 @@
         return target;
     }
 
+    /**
+     * Creates a new RemoteAnimationTarget from the provided change and leash
+     */
+    public static RemoteAnimationTarget newSyntheticTarget(ActivityManager.RunningTaskInfo taskInfo,
+            SurfaceControl leash, @TransitionInfo.TransitionMode int mode, int order,
+            boolean isTranslucent) {
+        int taskId;
+        boolean isNotInRecents;
+        WindowConfiguration windowConfiguration;
+
+        if (taskInfo != null) {
+            taskId = taskInfo.taskId;
+            isNotInRecents = !taskInfo.isRunning;
+            windowConfiguration = taskInfo.configuration.windowConfiguration;
+        } else {
+            taskId = INVALID_TASK_ID;
+            isNotInRecents = true;
+            windowConfiguration = new WindowConfiguration();
+        }
+
+        Rect localBounds = new Rect();
+        RemoteAnimationTarget target = new RemoteAnimationTarget(
+                taskId,
+                newModeToLegacyMode(mode),
+                // TODO: once we can properly sync transactions across process,
+                // then get rid of this leash.
+                leash,
+                isTranslucent,
+                null,
+                // TODO(shell-transitions): we need to send content insets? evaluate how its used.
+                new Rect(0, 0, 0, 0),
+                order,
+                null,
+                localBounds,
+                new Rect(),
+                windowConfiguration,
+                isNotInRecents,
+                null,
+                new Rect(),
+                taskInfo,
+                false,
+                INVALID_WINDOW_TYPE
+        );
+        return target;
+    }
+
     private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change,
             SurfaceControl leash) {
         return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 452d12a..7e6f434 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -46,7 +46,6 @@
 import android.util.SparseArray;
 import android.view.SurfaceControl;
 import android.window.ITaskOrganizerController;
-import android.window.ScreenCapture;
 import android.window.StartingWindowInfo;
 import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
@@ -55,7 +54,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.ProtoLog;
 import com.android.internal.util.FrameworkStatsLog;
-import com.android.wm.shell.common.ScreenshotUtils;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.compatui.CompatUIController;
 import com.android.wm.shell.compatui.api.CompatUIHandler;
@@ -74,7 +72,6 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.function.Consumer;
 
 /**
  * Unified task organizer for all components in the shell.
@@ -561,19 +558,6 @@
         mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskAdded(info.getTaskInfo()));
     }
 
-    /**
-     * Take a screenshot of a task.
-     */
-    public void screenshotTask(RunningTaskInfo taskInfo, Rect crop,
-            Consumer<ScreenCapture.ScreenshotHardwareBuffer> consumer) {
-        final TaskAppearedInfo info = mTasks.get(taskInfo.taskId);
-        if (info == null) {
-            return;
-        }
-        ScreenshotUtils.captureLayer(info.getLeash(), crop, consumer);
-    }
-
-
     @Override
     public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
         synchronized (mLock) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index b151c8b..05a70d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -487,10 +487,11 @@
     @Provides
     static RecentsTransitionHandler provideRecentsTransitionHandler(
             ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
             Transitions transitions,
             Optional<RecentTasksController> recentTasksController,
             HomeTransitionObserver homeTransitionObserver) {
-        return new RecentsTransitionHandler(shellInit, transitions,
+        return new RecentsTransitionHandler(shellInit, shellTaskOrganizer, transitions,
                 recentTasksController.orElse(null), homeTransitionObserver);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index c660000..8077aee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -20,9 +20,12 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
+import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_SLEEP;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -41,6 +44,7 @@
 import android.content.Intent;
 import android.graphics.Color;
 import android.graphics.Rect;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -64,6 +68,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.IResultReceiver;
 import com.android.internal.protolog.ProtoLog;
+import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -79,10 +84,15 @@
  * Handles the Recents (overview) animation. Only one of these can run at a time. A recents
  * transition must be created via {@link #startRecentsTransition}. Anything else will be ignored.
  */
-public class RecentsTransitionHandler implements Transitions.TransitionHandler {
+public class RecentsTransitionHandler implements Transitions.TransitionHandler,
+        Transitions.TransitionObserver {
     private static final String TAG = "RecentsTransitionHandler";
 
+    // A placeholder for a synthetic transition that isn't backed by a true system transition
+    public static final IBinder SYNTHETIC_TRANSITION = new Binder();
+
     private final Transitions mTransitions;
+    private final ShellTaskOrganizer mShellTaskOrganizer;
     private final ShellExecutor mExecutor;
     @Nullable
     private final RecentTasksController mRecentTasksController;
@@ -99,19 +109,26 @@
     private final HomeTransitionObserver mHomeTransitionObserver;
     private @Nullable Color mBackgroundColor;
 
-    public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions,
+    public RecentsTransitionHandler(
+            @NonNull ShellInit shellInit,
+            @NonNull ShellTaskOrganizer shellTaskOrganizer,
+            @NonNull Transitions transitions,
             @Nullable RecentTasksController recentTasksController,
-            HomeTransitionObserver homeTransitionObserver) {
+            @NonNull HomeTransitionObserver homeTransitionObserver) {
+        mShellTaskOrganizer = shellTaskOrganizer;
         mTransitions = transitions;
         mExecutor = transitions.getMainExecutor();
         mRecentTasksController = recentTasksController;
         mHomeTransitionObserver = homeTransitionObserver;
         if (!Transitions.ENABLE_SHELL_TRANSITIONS) return;
         if (recentTasksController == null) return;
-        shellInit.addInitCallback(() -> {
-            recentTasksController.setTransitionHandler(this);
-            transitions.addHandler(this);
-        }, this);
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        mRecentTasksController.setTransitionHandler(this);
+        mTransitions.addHandler(this);
+        mTransitions.registerObserver(this);
     }
 
     /** Register a mixer handler. {@see RecentsMixedHandler}*/
@@ -138,17 +155,59 @@
         mBackgroundColor = color;
     }
 
+    /**
+     * Starts a new real/synthetic recents transition.
+     */
     @VisibleForTesting
     public IBinder startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
             IApplicationThread appThread, IRecentsAnimationRunner listener) {
+        // only care about latest one.
+        mAnimApp = appThread;
+
+        // TODO(b/366021931): Formalize this later
+        final boolean isSyntheticRequest = options.containsKey("is_synthetic_recents_transition");
+        if (isSyntheticRequest) {
+            return startSyntheticRecentsTransition(listener);
+        } else {
+            return startRealRecentsTransition(intent, fillIn, options, listener);
+        }
+    }
+
+    /**
+     * Starts a synthetic recents transition that is not backed by a real WM transition.
+     */
+    private IBinder startSyntheticRecentsTransition(@NonNull IRecentsAnimationRunner listener) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                "RecentsTransitionHandler.startRecentsTransition(synthetic)");
+        final RecentsController lastController = getLastController();
+        if (lastController != null) {
+            lastController.cancel(lastController.isSyntheticTransition()
+                    ? "existing_running_synthetic_transition"
+                    : "existing_running_transition");
+            return null;
+        }
+
+        // Create a new synthetic transition and start it immediately
+        final RecentsController controller = new RecentsController(listener);
+        controller.startSyntheticTransition();
+        mControllers.add(controller);
+        return SYNTHETIC_TRANSITION;
+    }
+
+    /**
+     * Starts a real WM-backed recents transition.
+     */
+    private IBinder startRealRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
+            IRecentsAnimationRunner listener) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                 "RecentsTransitionHandler.startRecentsTransition");
 
-        // only care about latest one.
-        mAnimApp = appThread;
-        WindowContainerTransaction wct = new WindowContainerTransaction();
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
         wct.sendPendingIntent(intent, fillIn, options);
-        final RecentsController controller = new RecentsController(listener);
+
+        // Find the mixed handler which should handle this request (if we are in a state where a
+        // mixed handler is needed).  This is slightly convoluted because starting the transition
+        // requires the handler, but the mixed handler also needs a reference to the transition.
         RecentsMixedHandler mixer = null;
         Consumer<IBinder> setTransitionForMixer = null;
         for (int i = 0; i < mMixers.size(); ++i) {
@@ -160,12 +219,11 @@
         }
         final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct,
                 mixer == null ? this : mixer);
-        for (int i = 0; i < mStateListeners.size(); i++) {
-            mStateListeners.get(i).onTransitionStarted(transition);
-        }
         if (mixer != null) {
             setTransitionForMixer.accept(transition);
         }
+
+        final RecentsController controller = new RecentsController(listener);
         if (transition != null) {
             controller.setTransition(transition);
             mControllers.add(controller);
@@ -187,11 +245,28 @@
         return null;
     }
 
-    private int findController(IBinder transition) {
+    /**
+     * Returns if there is currently a pending or active recents transition.
+     */
+    @Nullable
+    private RecentsController getLastController() {
+        return !mControllers.isEmpty() ? mControllers.getLast() : null;
+    }
+
+    /**
+     * Finds an existing controller for the provided {@param transition}, or {@code null} if none
+     * exists.
+     */
+    @Nullable
+    @VisibleForTesting
+    RecentsController findController(@NonNull IBinder transition) {
         for (int i = mControllers.size() - 1; i >= 0; --i) {
-            if (mControllers.get(i).mTransition == transition) return i;
+            final RecentsController controller = mControllers.get(i);
+            if (controller.mTransition == transition) {
+                return controller;
+            }
         }
-        return -1;
+        return null;
     }
 
     @Override
@@ -199,13 +274,12 @@
             SurfaceControl.Transaction startTransaction,
             SurfaceControl.Transaction finishTransaction,
             Transitions.TransitionFinishCallback finishCallback) {
-        final int controllerIdx = findController(transition);
-        if (controllerIdx < 0) {
+        final RecentsController controller = findController(transition);
+        if (controller == null) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                     "RecentsTransitionHandler.startAnimation: no controller found");
             return false;
         }
-        final RecentsController controller = mControllers.get(controllerIdx);
         final IApplicationThread animApp = mAnimApp;
         mAnimApp = null;
         if (!controller.start(info, startTransaction, finishTransaction, finishCallback)) {
@@ -221,13 +295,12 @@
     public void mergeAnimation(IBinder transition, TransitionInfo info,
             SurfaceControl.Transaction t, IBinder mergeTarget,
             Transitions.TransitionFinishCallback finishCallback) {
-        final int targetIdx = findController(mergeTarget);
-        if (targetIdx < 0) {
+        final RecentsController controller = findController(mergeTarget);
+        if (controller == null) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                     "RecentsTransitionHandler.mergeAnimation: no controller found");
             return;
         }
-        final RecentsController controller = mControllers.get(targetIdx);
         controller.merge(info, t, finishCallback);
     }
 
@@ -244,8 +317,21 @@
         }
     }
 
+    @Override
+    public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction) {
+        RecentsController controller = findController(SYNTHETIC_TRANSITION);
+        if (controller != null) {
+            // Cancel the existing synthetic transition if there is one
+            controller.cancel("incoming_transition");
+        }
+    }
+
     /** There is only one of these and it gets reset on finish. */
-    private class RecentsController extends IRecentsAnimationController.Stub {
+    @VisibleForTesting
+    class RecentsController extends IRecentsAnimationController.Stub {
+
         private final int mInstanceId;
 
         private IRecentsAnimationRunner mListener;
@@ -307,7 +393,8 @@
             mDeathHandler = () -> {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                         "[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
-                finish(mWillFinishToHome, false /* leaveHint */, null /* finishCb */);
+                finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */,
+                        "deathRecipient");
             };
             try {
                 mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */);
@@ -317,6 +404,9 @@
             }
         }
 
+        /**
+         * Sets the started transition for this instance of the recents transition.
+         */
         void setTransition(IBinder transition) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                     "[%d] RecentsController.setTransition: id=%s", mInstanceId, transition);
@@ -330,6 +420,10 @@
         }
 
         void cancel(boolean toHome, boolean withScreenshots, String reason) {
+            if (cancelSyntheticTransition(reason)) {
+                return;
+            }
+
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                     "[%d] RecentsController.cancel: toHome=%b reason=%s",
                     mInstanceId, toHome, reason);
@@ -341,7 +435,7 @@
                 }
             }
             if (mFinishCB != null) {
-                finishInner(toHome, false /* userLeave */, null /* finishCb */);
+                finishInner(toHome, false /* userLeave */, null /* finishCb */, "cancel");
             } else {
                 cleanUp();
             }
@@ -436,6 +530,91 @@
             }
         }
 
+        /**
+         * Starts a new transition that is not backed by a system transition.
+         */
+        void startSyntheticTransition() {
+            mTransition = SYNTHETIC_TRANSITION;
+
+            // TODO(b/366021931): Update mechanism for pulling the home task, for now add home as
+            //                    both opening and closing since there's some pre-existing
+            //                    dependencies on having a closing task
+            final ActivityManager.RunningTaskInfo homeTask =
+                    mShellTaskOrganizer.getRunningTasks(DEFAULT_DISPLAY).stream()
+                            .filter(task -> task.getActivityType() == ACTIVITY_TYPE_HOME)
+                            .findFirst()
+                            .get();
+            final RemoteAnimationTarget openingTarget = TransitionUtil.newSyntheticTarget(
+                    homeTask, mShellTaskOrganizer.getHomeTaskOverlayContainer(), TRANSIT_OPEN,
+                    0, true /* isTranslucent */);
+            final RemoteAnimationTarget closingTarget = TransitionUtil.newSyntheticTarget(
+                    homeTask, mShellTaskOrganizer.getHomeTaskOverlayContainer(), TRANSIT_CLOSE,
+                    0, true /* isTranslucent */);
+            final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>();
+            apps.add(openingTarget);
+            apps.add(closingTarget);
+            try {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                        "[%d] RecentsController.start: calling onAnimationStart with %d apps",
+                        mInstanceId, apps.size());
+                mListener.onAnimationStart(this,
+                        apps.toArray(new RemoteAnimationTarget[apps.size()]),
+                        new RemoteAnimationTarget[0],
+                        new Rect(0, 0, 0, 0), new Rect(), new Bundle());
+                for (int i = 0; i < mStateListeners.size(); i++) {
+                    mStateListeners.get(i).onAnimationStateChanged(true);
+                }
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error starting recents animation", e);
+                cancel("startSynthetricTransition() failed");
+            }
+        }
+
+        /**
+         * Returns whether this transition is backed by a real system transition or not.
+         */
+        boolean isSyntheticTransition() {
+            return mTransition == SYNTHETIC_TRANSITION;
+        }
+
+        /**
+         * Called when a synthetic transition is canceled.
+         */
+        boolean cancelSyntheticTransition(String reason) {
+            if (!isSyntheticTransition()) {
+                return false;
+            }
+
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                    "[%d] RecentsController.cancelSyntheticTransition reason=%s",
+                    mInstanceId, reason);
+            try {
+                // TODO(b/366021931): Notify the correct tasks once we build actual targets, and
+                //                    clean up leashes accordingly
+                mListener.onAnimationCanceled(new int[0], new TaskSnapshot[0]);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error canceling previous recents animation", e);
+            }
+            cleanUp();
+            return true;
+        }
+
+        /**
+         * Called when a synthetic transition is finished.
+         * @return
+         */
+        boolean finishSyntheticTransition() {
+            if (!isSyntheticTransition()) {
+                return false;
+            }
+
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                    "[%d] RecentsController.finishSyntheticTransition", mInstanceId);
+            // TODO(b/366021931): Clean up leashes accordingly
+            cleanUp();
+            return true;
+        }
+
         boolean start(TransitionInfo info, SurfaceControl.Transaction t,
                 SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCB) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
@@ -662,7 +841,7 @@
                             // Set the callback once again so we can finish correctly.
                             mFinishCB = finishCB;
                             finishInner(true /* toHome */, false /* userLeave */,
-                                    null /* finishCb */);
+                                    null /* finishCb */, "takeOverAnimation");
                         }, updatedStates);
             });
         }
@@ -810,7 +989,7 @@
                 sendCancelWithSnapshots();
                 mExecutor.executeDelayed(
                         () -> finishInner(true /* toHome */, false /* userLeaveHint */,
-                                null /* finishCb */), 0);
+                                null /* finishCb */, "merge"), 0);
                 return;
             }
             if (recentsOpening != null) {
@@ -1005,7 +1184,7 @@
                     return;
                 }
                 final int displayId = mInfo.getRootCount() > 0 ? mInfo.getRoot(0).getDisplayId()
-                        : Display.DEFAULT_DISPLAY;
+                        : DEFAULT_DISPLAY;
                 // transient launches don't receive focus automatically. Since we are taking over
                 // the gesture now, take focus explicitly.
                 // This also moves recents back to top if the user gestured before a switch
@@ -1038,11 +1217,16 @@
         @Override
         @SuppressLint("NewApi")
         public void finish(boolean toHome, boolean sendUserLeaveHint, IResultReceiver finishCb) {
-            mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint, finishCb));
+            mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint, finishCb,
+                    "requested"));
         }
 
         private void finishInner(boolean toHome, boolean sendUserLeaveHint,
-                IResultReceiver runnerFinishCb) {
+                IResultReceiver runnerFinishCb, String reason) {
+            if (finishSyntheticTransition()) {
+                return;
+            }
+
             if (mFinishCB == null) {
                 Slog.e(TAG, "Duplicate call to finish");
                 return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
index e8733eb..95874c8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
@@ -24,7 +24,4 @@
     /** Notifies whether the recents animation is running. */
     default void onAnimationStateChanged(boolean running) {
     }
-
-    /** Notifies that a recents shell transition has started. */
-    default void onTransitionStarted(IBinder transition) {}
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 2c02d4f..d03832d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -1501,16 +1501,16 @@
          *                          transition animation. The Transition system will apply it when
          *                          finishCallback is called by the transition handler.
          */
-        void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
+        default void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
                 @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction);
+                @NonNull SurfaceControl.Transaction finishTransaction) {}
 
         /**
          * Called when the transition is starting to play. It isn't called for merged transitions.
          *
          * @param transition the unique token of this transition
          */
-        void onTransitionStarting(@NonNull IBinder transition);
+        default void onTransitionStarting(@NonNull IBinder transition) {}
 
         /**
          * Called when a transition is merged into another transition. There won't be any following
@@ -1519,7 +1519,7 @@
          * @param merged the unique token of the transition that's merged to another one
          * @param playing the unique token of the transition that accepts the merge
          */
-        void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing);
+        default void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {}
 
         /**
          * Called when the transition is finished. This isn't called for merged transitions.
@@ -1527,7 +1527,7 @@
          * @param transition the unique token of this transition
          * @param aborted {@code true} if this transition is aborted; {@code false} otherwise.
          */
-        void onTransitionFinished(@NonNull IBinder transition, boolean aborted);
+        default void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {}
     }
 
     @BinderThread
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index d43ee44..b1fc55f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -753,9 +753,12 @@
             final ActivityInfo activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */);
             final IconProvider provider = new IconProvider(mContext);
             final Drawable appIconDrawable = provider.getIcon(activityInfo);
+            final Drawable badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable,
+                    UserHandle.of(mTaskInfo.userId));
             final BaseIconFactory headerIconFactory = createIconFactory(mContext,
                     R.dimen.desktop_mode_caption_icon_radius);
-            mAppIconBitmap = headerIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+            mAppIconBitmap = headerIconFactory.createIconBitmap(badgedAppIconDrawable,
+                    1f /* scale */);
 
             final BaseIconFactory resizeVeilIconFactory = createIconFactory(mContext,
                     R.dimen.desktop_mode_resize_veil_icon_size);
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 8258890..b47201e 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
@@ -18,7 +18,9 @@
 
 import android.app.ActivityManager.RecentTaskInfo
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityOptions
 import android.app.KeyguardManager
+import android.app.PendingIntent
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -43,6 +45,7 @@
 import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
 import android.view.Display.DEFAULT_DISPLAY
+import android.view.DragEvent
 import android.view.Gravity
 import android.view.SurfaceControl
 import android.view.WindowManager
@@ -107,6 +110,7 @@
 import com.android.wm.shell.transition.Transitions.TransitionHandler
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import java.util.function.Consumer
 import java.util.Optional
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
@@ -131,8 +135,8 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.atLeastOnce
-import org.mockito.kotlin.eq
 import org.mockito.kotlin.capture
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
 
@@ -3082,6 +3086,95 @@
     assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
   }
 
+
+  @Test
+  fun onUnhandledDrag_newFreeformIntent() {
+    testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
+      PointF(1200f, 700f),
+      Rect(240, 700, 2160, 1900))
+  }
+
+  @Test
+  fun onUnhandledDrag_newFreeformIntentSplitLeft() {
+    testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
+      PointF(50f, 700f),
+      Rect(0, 0, 500, 1000))
+  }
+
+  @Test
+  fun onUnhandledDrag_newFreeformIntentSplitRight() {
+    testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
+      PointF(2500f, 700f),
+      Rect(500, 0, 1000, 1000))
+  }
+
+  @Test
+  fun onUnhandledDrag_newFullscreenIntent() {
+    testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
+      PointF(1200f, 50f),
+      Rect())
+  }
+
+  /**
+   * Assert that an unhandled drag event launches a PendingIntent with the
+   * windowing mode and bounds we are expecting.
+   */
+  private fun testOnUnhandledDrag(
+    indicatorType: DesktopModeVisualIndicator.IndicatorType,
+    inputCoordinate: PointF,
+    expectedBounds: Rect
+  ) {
+    setUpLandscapeDisplay()
+    val task = setUpFreeformTask()
+    markTaskVisible(task)
+    task.isFocused = true
+    val runningTasks = ArrayList<RunningTaskInfo>()
+    runningTasks.add(task)
+    val spyController = spy(controller)
+    val mockPendingIntent = mock(PendingIntent::class.java)
+    val mockDragEvent = mock(DragEvent::class.java)
+    val mockCallback = mock(Consumer::class.java)
+    val b = SurfaceControl.Builder()
+    b.setName("test surface")
+    val dragSurface = b.build()
+    whenever(shellTaskOrganizer.runningTasks).thenReturn(runningTasks)
+    whenever(mockDragEvent.dragSurface).thenReturn(dragSurface)
+    whenever(mockDragEvent.x).thenReturn(inputCoordinate.x)
+    whenever(mockDragEvent.y).thenReturn(inputCoordinate.y)
+    whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true)
+    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+    doReturn(indicatorType)
+      .whenever(spyController).updateVisualIndicator(
+        eq(task),
+        anyOrNull(),
+        anyOrNull(),
+        anyOrNull(),
+        eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
+      )
+
+    spyController.onUnhandledDrag(
+      mockPendingIntent,
+      mockDragEvent,
+      mockCallback as Consumer<Boolean>
+    )
+    val arg: ArgumentCaptor<WindowContainerTransaction> =
+      ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    var expectedWindowingMode: Int
+      if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) {
+        expectedWindowingMode = WINDOWING_MODE_FULLSCREEN
+        // Fullscreen launches currently use default transitions
+        verify(transitions).startTransition(any(), capture(arg), anyOrNull())
+      } else {
+        expectedWindowingMode = WINDOWING_MODE_FREEFORM
+        // All other launches use a special handler.
+        verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg))
+      }
+    assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
+      .launchWindowingMode).isEqualTo(expectedWindowingMode)
+    assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
+      .launchBounds).isEqualTo(expectedBounds)
+  }
+
   private val desktopWallpaperIntent: Intent
     get() = Intent(context, DesktopWallpaperActivity::class.java)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
new file mode 100644
index 0000000..769acf7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2021 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.recents;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityTaskManager;
+import android.app.IApplicationThread;
+import android.app.KeyguardManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.HomeTransitionObserver;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.quality.Strictness;
+
+import java.util.Optional;
+
+/**
+ * Tests for {@link RecentTasksController}
+ *
+ * Usage: atest WMShellUnitTests:RecentsTransitionHandlerTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RecentsTransitionHandlerTest extends ShellTestCase {
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private TaskStackListenerImpl mTaskStackListener;
+    @Mock
+    private ShellCommandHandler mShellCommandHandler;
+    @Mock
+    private DesktopModeTaskRepository mDesktopModeTaskRepository;
+    @Mock
+    private ActivityTaskManager mActivityTaskManager;
+    @Mock
+    private DisplayInsetsController mDisplayInsetsController;
+    @Mock
+    private IRecentTasksListener mRecentTasksListener;
+    @Mock
+    private TaskStackTransitionObserver mTaskStackTransitionObserver;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private ShellTaskOrganizer mShellTaskOrganizer;
+    private RecentTasksController mRecentTasksController;
+    private RecentTasksController mRecentTasksControllerReal;
+    private RecentsTransitionHandler mRecentsTransitionHandler;
+    private ShellInit mShellInit;
+    private ShellController mShellController;
+    private TestShellExecutor mMainExecutor;
+    private static StaticMockitoSession sMockitoSession;
+
+    @Before
+    public void setUp() {
+        sMockitoSession = mockitoSession().initMocks(this).strictness(Strictness.LENIENT)
+                .mockStatic(DesktopModeStatus.class).startMocking();
+        ExtendedMockito.doReturn(true)
+                .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
+        mMainExecutor = new TestShellExecutor();
+        when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
+        when(mContext.getSystemService(KeyguardManager.class))
+                .thenReturn(mock(KeyguardManager.class));
+        mShellInit = spy(new ShellInit(mMainExecutor));
+        mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
+                mDisplayInsetsController, mMainExecutor));
+        mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit,
+                mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
+                Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver,
+                mMainExecutor);
+        mRecentTasksController = spy(mRecentTasksControllerReal);
+        mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
+                null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
+                mMainExecutor);
+
+        final Transitions transitions = mock(Transitions.class);
+        doReturn(mMainExecutor).when(transitions).getMainExecutor();
+        mRecentsTransitionHandler = new RecentsTransitionHandler(mShellInit, mShellTaskOrganizer,
+                transitions, mRecentTasksController, mock(HomeTransitionObserver.class));
+
+        mShellInit.init();
+    }
+
+    @After
+    public void tearDown() {
+        sMockitoSession.finishMocking();
+    }
+
+    @Test
+    public void testStartSyntheticRecentsTransition_callsOnAnimationStart() throws Exception {
+        final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class);
+        doReturn(new Binder()).when(runner).asBinder();
+        Bundle options = new Bundle();
+        options.putBoolean("is_synthetic_recents_transition", true);
+        IBinder transition = mRecentsTransitionHandler.startRecentsTransition(
+                mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class),
+                runner);
+        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+
+        // Finish and verify no transition remains
+        mRecentsTransitionHandler.findController(transition).finish(true /* toHome */,
+                false /* sendUserLeaveHint */, null /* finishCb */);
+        mMainExecutor.flushAll();
+        assertNull(mRecentsTransitionHandler.findController(transition));
+    }
+
+    @Test
+    public void testStartSyntheticRecentsTransition_callsOnAnimationCancel() throws Exception {
+        final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class);
+        doReturn(new Binder()).when(runner).asBinder();
+        Bundle options = new Bundle();
+        options.putBoolean("is_synthetic_recents_transition", true);
+        IBinder transition = mRecentsTransitionHandler.startRecentsTransition(
+                mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class),
+                runner);
+        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+
+        mRecentsTransitionHandler.findController(transition).cancel("test");
+        mMainExecutor.flushAll();
+        verify(runner).onAnimationCanceled(any(), any());
+        assertNull(mRecentsTransitionHandler.findController(transition));
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 61a725f..fec9e3e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -1211,7 +1211,7 @@
                         mTransactionPool, createTestDisplayController(), mMainExecutor,
                         mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
         final RecentsTransitionHandler recentsHandler =
-                new RecentsTransitionHandler(shellInit, transitions,
+                new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
                         mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
         shellInit.init();
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 25c063d6..202535d 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -273,6 +273,7 @@
     ASurfaceTransaction_fromJava; # introduced=34
     ASurfaceTransaction_reparent; # introduced=29
     ASurfaceTransaction_setBuffer; # introduced=29
+    ASurfaceTransaction_setBufferWithRelease; # introduced=36
     ASurfaceTransaction_setBufferAlpha; # introduced=29
     ASurfaceTransaction_setBufferDataSpace; # introduced=29
     ASurfaceTransaction_setBufferTransparency; # introduced=29
diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp
index 6ce83cd..e46db6b 100644
--- a/native/android/surface_control.cpp
+++ b/native/android/surface_control.cpp
@@ -416,6 +416,35 @@
     transaction->setBuffer(surfaceControl, graphic_buffer, fence);
 }
 
+void ASurfaceTransaction_setBufferWithRelease(
+        ASurfaceTransaction* aSurfaceTransaction, ASurfaceControl* aSurfaceControl,
+        AHardwareBuffer* buffer, int acquire_fence_fd, void* _Null_unspecified context,
+        ASurfaceTransaction_OnBufferRelease aReleaseCallback) {
+    CHECK_NOT_NULL(aSurfaceTransaction);
+    CHECK_NOT_NULL(aSurfaceControl);
+    CHECK_NOT_NULL(aReleaseCallback);
+
+    sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl);
+    Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction);
+
+    sp<GraphicBuffer> graphic_buffer(GraphicBuffer::fromAHardwareBuffer(buffer));
+
+    std::optional<sp<Fence>> fence = std::nullopt;
+    if (acquire_fence_fd != -1) {
+        fence = new Fence(acquire_fence_fd);
+    }
+
+    ReleaseBufferCallback releaseBufferCallback =
+            [context,
+             aReleaseCallback](const ReleaseCallbackId&, const sp<Fence>& releaseFence,
+                               std::optional<uint32_t> /* currentMaxAcquiredBufferCount */) {
+                (*aReleaseCallback)(context, (releaseFence) ? releaseFence->dup() : -1);
+            };
+
+    transaction->setBuffer(surfaceControl, graphic_buffer, fence, /* frameNumber */ std::nullopt,
+                           /* producerId */ 0, releaseBufferCallback);
+}
+
 void ASurfaceTransaction_setGeometry(ASurfaceTransaction* aSurfaceTransaction,
                                      ASurfaceControl* aSurfaceControl, const ARect& source,
                                      const ARect& destination, int32_t transform) {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index feee89a..0b094a2 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1409,6 +1409,8 @@
     <string name="media_transfer_this_device_name">This phone</string>
     <!-- Name of the tablet device. [CHAR LIMIT=30] -->
     <string name="media_transfer_this_device_name_tablet">This tablet</string>
+    <!-- Name of the internal speaker. [CHAR LIMIT=30] -->
+    <string name="media_transfer_this_device_name_desktop">This computer (internal)</string>
     <!-- Name of the default media output of the TV. [CHAR LIMIT=30] -->
     <string name="media_transfer_this_device_name_tv">@string/tv_media_transfer_default</string>
     <!-- Name of the internal mic. [CHAR LIMIT=30] -->
@@ -1639,6 +1641,12 @@
     <!-- Name of the 3.5mm and usb audio device. [CHAR LIMIT=50] -->
     <string name="media_transfer_wired_usb_device_name">Wired headphone</string>
 
+    <!-- Name of the 3.5mm headphone, used in desktop devices. [CHAR LIMIT=50] -->
+    <string name="media_transfer_headphone_name">Headphone</string>
+
+    <!-- Name of the usb audio device speaker, used in desktop devices. [CHAR LIMIT=50] -->
+    <string name="media_transfer_usb_speaker_name">USB speaker</string>
+
     <!-- Name of the 3.5mm audio device mic. [CHAR LIMIT=50] -->
     <string name="media_transfer_wired_device_mic_name">Mic jack</string>
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index 548eb3f..874e030 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -57,7 +57,7 @@
                 }
             };
 
-    /* package */ InputRouteManager(@NonNull Context context, @NonNull AudioManager audioManager) {
+    public InputRouteManager(@NonNull Context context, @NonNull AudioManager audioManager) {
         mContext = context;
         mAudioManager = audioManager;
         Handler handler = new Handler(context.getMainLooper());
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index 9eaf8d3..116de56 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -72,6 +72,8 @@
             return context.getString(R.string.media_transfer_this_device_name_tv);
         } else if (isTablet()) {
             return context.getString(R.string.media_transfer_this_device_name_tablet);
+        } else if (inputRoutingEnabledAndIsDesktop()) {
+            return context.getString(R.string.media_transfer_this_device_name_desktop);
         } else {
             return context.getString(R.string.media_transfer_this_device_name);
         }
@@ -85,10 +87,18 @@
         switch (routeInfo.getType()) {
             case TYPE_WIRED_HEADSET:
             case TYPE_WIRED_HEADPHONES:
+                name =
+                        inputRoutingEnabledAndIsDesktop()
+                                ? context.getString(R.string.media_transfer_headphone_name)
+                                : context.getString(R.string.media_transfer_wired_usb_device_name);
+                break;
             case TYPE_USB_DEVICE:
             case TYPE_USB_HEADSET:
             case TYPE_USB_ACCESSORY:
-                name = context.getString(R.string.media_transfer_wired_usb_device_name);
+                name =
+                        inputRoutingEnabledAndIsDesktop()
+                                ? context.getString(R.string.media_transfer_usb_speaker_name)
+                                : context.getString(R.string.media_transfer_wired_usb_device_name);
                 break;
             case TYPE_DOCK:
                 name = context.getString(R.string.media_transfer_dock_speaker_device_name);
@@ -139,6 +149,16 @@
                 .contains("tablet");
     }
 
+    static boolean isDesktop() {
+        return Arrays.asList(SystemProperties.get("ro.build.characteristics").split(","))
+                .contains("desktop");
+    }
+
+    static boolean inputRoutingEnabledAndIsDesktop() {
+        return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl()
+                && isDesktop();
+    }
+
     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
     @SuppressWarnings("NewApi")
     @Override
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
index e2d58d6..23cfc01 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
@@ -47,6 +47,7 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowSystemProperties;
 
 @RunWith(RobolectricTestRunner.class)
 public class PhoneMediaDeviceTest {
@@ -114,6 +115,31 @@
 
         when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
 
+        assertThat(mPhoneMediaDevice.getName()).isEqualTo(getMediaTransferThisDeviceName(mContext));
+    }
+
+    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getName_returnCorrectName_desktop() {
+        ShadowSystemProperties.override("ro.build.characteristics", "desktop");
+
+        when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES);
+
+        assertThat(mPhoneMediaDevice.getName())
+                .isEqualTo(mContext.getString(R.string.media_transfer_headphone_name));
+
+        when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADSET);
+
+        assertThat(mPhoneMediaDevice.getName())
+                .isEqualTo(mContext.getString(R.string.media_transfer_headphone_name));
+
+        when(mInfo.getType()).thenReturn(TYPE_USB_DEVICE);
+
+        assertThat(mPhoneMediaDevice.getName())
+                .isEqualTo(mContext.getString(R.string.media_transfer_usb_speaker_name));
+
+        when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
+
         assertThat(mPhoneMediaDevice.getName())
                 .isEqualTo(getMediaTransferThisDeviceName(mContext));
     }
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index c107ff5..1a99d25 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -36,6 +36,7 @@
         "aconfig_new_storage_flags_lib",
         "aconfigd_java_utils",
         "aconfig_demo_flags_java_lib",
+        "configinfra_framework_flags_java_lib",
         "device_config_service_flags_java",
         "libaconfig_java_proto_lite",
         "SettingsLibDeviceStateRotationLock",
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
index c9ad5a5..fbce6ca 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
@@ -99,13 +99,23 @@
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        pw.print("SyncDisabledForTests: ");
-        MyShellCommand.getSyncDisabledForTests(pw, pw);
+        if (android.provider.flags.Flags.dumpImprovements()) {
+            pw.print("SyncDisabledForTests: ");
+            MyShellCommand.getSyncDisabledForTests(pw, pw);
 
-        pw.print("Is mainline: ");
-        pw.println(UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService());
+            pw.print("UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService(): ");
+            pw.println(UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService());
 
-        final IContentProvider iprovider = mProvider.getIContentProvider();
+            pw.println("DeviceConfig provider: ");
+            try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(fd)) {
+                DeviceConfig.dump(pfd, pw, /* prefix= */ "  ", args);
+            } catch (IOException e) {
+                pw.print("IOException creating ParcelFileDescriptor: ");
+                pw.println(e);
+            }
+        }
+
+        IContentProvider iprovider = mProvider.getIContentProvider();
         pw.println("DeviceConfig flags:");
         for (String line : MyShellCommand.listAll(iprovider)) {
             pw.println(line);
@@ -251,22 +261,13 @@
 
       public static HashMap<String, String> getAllFlags(IContentProvider provider) {
         HashMap<String, String> allFlags = new HashMap<String, String>();
-        try {
-            Bundle args = new Bundle();
-            args.putInt(Settings.CALL_METHOD_USER_KEY,
-                ActivityManager.getService().getCurrentUser().id);
-            Bundle b = provider.call(new AttributionSource(Process.myUid(),
-                    resolveCallingPackage(), null), Settings.AUTHORITY,
-                    Settings.CALL_METHOD_LIST_CONFIG, null, args);
-            if (b != null) {
-                Map<String, String> flagsToValues =
-                    (HashMap) b.getSerializable(Settings.NameValueTable.VALUE);
-                allFlags.putAll(flagsToValues);
+        for (DeviceConfig.Properties properties : DeviceConfig.getAllProperties()) {
+            List<String> keys = new ArrayList<>(properties.getKeyset());
+            for (String flagName : properties.getKeyset()) {
+                String fullName = properties.getNamespace() + "/" + flagName;
+                allFlags.put(fullName, properties.getString(flagName, null));
             }
-        } catch (RemoteException e) {
-            throw new RuntimeException("Failed in IPC", e);
         }
-
         return allFlags;
       }
 
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index f59eab0..cd16af7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -452,7 +452,7 @@
         "tests/src/**/systemui/clipboardoverlay/ClipboardListenerTest.java",
         "tests/src/**/systemui/doze/DozeScreenStateTest.java",
         "tests/src/**/systemui/keyguard/WorkLockActivityControllerTest.java",
-        "tests/src/**/systemui/media/dialog/MediaOutputControllerTest.java",
+        "tests/src/**/systemui/media/dialog/MediaSwitchingControllerTest.java",
         "tests/src/**/systemui/navigationbar/views/NavigationBarTest.java",
         "tests/src/**/systemui/power/PowerNotificationWarningsTest.java",
         "tests/src/**/systemui/power/PowerUITest.java",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7974f92..f3a28ca 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -149,6 +149,16 @@
 }
 
 flag {
+   name: "modes_dialog_single_rows"
+   namespace: "systemui"
+   description: "[Experiment] Display one entry per grid row in the Modes Dialog."
+   bug: "366034002"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
    name: "pss_app_selector_recents_split_screen"
    namespace: "systemui"
    description: "Allows recent apps selected for partial screenshare to be launched in split screen mode"
@@ -538,13 +548,6 @@
 }
 
 flag {
-    name: "haptic_volume_slider"
-    namespace: "systemui"
-    description: "Adds haptic feedback to the volume slider."
-    bug: "316953430"
-}
-
-flag {
     name: "new_volume_panel"
     namespace: "systemui"
     description: "Switches to the new volume panel (without Slices)."
@@ -668,13 +671,6 @@
 }
 
 flag {
-   name: "compose_lockscreen"
-   namespace: "systemui"
-   description: "Enables the compose version of lockscreen that runs standalone, outside of Flexiglass."
-   bug: "301968149"
-}
-
-flag {
    name: "enable_contextual_tip_for_power_off"
    namespace: "systemui"
    description: "Enables on-screen contextual tip about how to power off or restart phone"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
index 296fc27..dcf32b2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
@@ -16,15 +16,10 @@
 
 package com.android.systemui.common.ui.compose.windowinsets
 
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.displayCutout
-import androidx.compose.foundation.layout.systemBars
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -36,9 +31,6 @@
 /** The corner radius in px of the current display. */
 val LocalScreenCornerRadius = staticCompositionLocalOf { 0.dp }
 
-/** The screen height in px without accounting for any screen insets (cutouts, status/nav bars) */
-val LocalRawScreenHeight = staticCompositionLocalOf { 0f }
-
 @Composable
 fun ScreenDecorProvider(
     displayCutout: StateFlow<DisplayCutout>,
@@ -48,22 +40,9 @@
     val cutout by displayCutout.collectAsStateWithLifecycle()
     val screenCornerRadiusDp = with(LocalDensity.current) { screenCornerRadius.toDp() }
 
-    val density = LocalDensity.current
-    val navBarHeight =
-        with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
-    val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
-    val displayCutoutHeight = WindowInsets.displayCutout.asPaddingValues().calculateTopPadding()
-    val screenHeight =
-        with(density) {
-            (LocalConfiguration.current.screenHeightDp.dp +
-                    maxOf(statusBarHeight, displayCutoutHeight))
-                .toPx()
-        } + navBarHeight
-
     CompositionLocalProvider(
         LocalScreenCornerRadius provides screenCornerRadiusDp,
         LocalDisplayCutout provides cutout,
-        LocalRawScreenHeight provides screenHeight,
     ) {
         content()
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
index 897a861..a2ae8bb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
@@ -24,9 +24,11 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
-import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
 import kotlin.math.max
 import kotlin.math.roundToInt
 import kotlin.math.tanh
@@ -36,9 +38,10 @@
 @Composable
 fun Modifier.stackVerticalOverscroll(
     coroutineScope: CoroutineScope,
-    canScrollForward: () -> Boolean
+    canScrollForward: () -> Boolean,
 ): Modifier {
-    val screenHeight = LocalRawScreenHeight.current
+    val screenHeight =
+        with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
     val overscrollOffset = remember { Animatable(0f) }
     val stackNestedScrollConnection = remember {
         NotificationStackNestedScrollConnection(
@@ -60,10 +63,10 @@
                     overscrollOffset.animateTo(
                         targetValue = 0f,
                         initialVelocity = velocityAvailable,
-                        animationSpec = tween()
+                        animationSpec = tween(),
                     )
                 }
-            }
+            },
         )
     }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 91ecfc1..1b99a96 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -19,6 +19,7 @@
 
 import android.util.Log
 import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.background
@@ -29,6 +30,8 @@
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.absoluteOffset
@@ -36,9 +39,11 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imeAnimationTarget
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.windowInsetsBottomHeight
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
@@ -68,6 +73,7 @@
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.Dp
@@ -81,7 +87,6 @@
 import com.android.compose.animation.scene.NestedScrollBehavior
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.thenIf
-import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
 import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
 import com.android.systemui.res.R
 import com.android.systemui.scene.session.ui.composable.SaveableSession
@@ -96,6 +101,7 @@
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
 object Notifications {
@@ -171,7 +177,7 @@
             setCurrent = { scrollOffset = it },
             min = minScrollOffset,
             max = maxScrollOffset,
-            delta
+            delta,
         )
     }
 
@@ -209,8 +215,8 @@
                             calculateHeadsUpPlaceholderYOffset(
                                 scrollOffset.roundToInt(),
                                 minScrollOffset.roundToInt(),
-                                stackScrollView.topHeadsUpHeight
-                            )
+                                stackScrollView.topHeadsUpHeight,
+                            ),
                     )
                 }
                 .thenIf(isHeadsUp) {
@@ -218,11 +224,8 @@
                             bottomBehavior = NestedScrollBehavior.EdgeAlways
                         )
                         .nestedScroll(nestedScrollConnection)
-                        .scrollable(
-                            orientation = Orientation.Vertical,
-                            state = scrollableState,
-                        )
-                }
+                        .scrollable(orientation = Orientation.Vertical, state = scrollableState)
+                },
     )
 }
 
@@ -259,6 +262,7 @@
  * Adds the space where notification stack should appear in the scene, with a scrim and nested
  * scrolling.
  */
+@OptIn(ExperimentalLayoutApi::class)
 @Composable
 fun SceneScope.NotificationScrollingStack(
     shadeSession: SaveableSession,
@@ -291,7 +295,7 @@
     val navBarHeight = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()
     val bottomPadding = if (shouldReserveSpaceForNavBar) navBarHeight else 0.dp
 
-    val screenHeight = LocalRawScreenHeight.current
+    val screenHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
 
     /**
      * The height in px of the contents of notification stack. Depending on the number of
@@ -325,6 +329,14 @@
         screenHeight - maxScrimTop() - with(density) { navBarHeight.toPx() }
     }
 
+    val isRemoteInputActive by viewModel.isRemoteInputActive.collectAsStateWithLifecycle(false)
+
+    // The bottom Y bound of the currently focused remote input notification.
+    val remoteInputRowBottom by viewModel.remoteInputRowBottomBound.collectAsStateWithLifecycle(0f)
+
+    // The top y bound of the IME.
+    val imeTop = remember { mutableFloatStateOf(0f) }
+
     // we are not scrolled to the top unless the scrim is at its maximum offset.
     LaunchedEffect(viewModel, scrimOffset) {
         snapshotFlow { scrimOffset.value >= 0f }
@@ -342,15 +354,34 @@
     LaunchedEffect(syntheticScroll, scrimOffset, scrollState) {
         snapshotFlow { syntheticScroll.value }
             .collect { delta ->
-                val minOffset = minScrimOffset()
-                if (scrimOffset.value > minOffset) {
-                    val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f)
-                    scrimOffset.snapTo((scrimOffset.value - delta).coerceAtLeast(minOffset))
-                    if (remainingDelta > 0f) {
-                        scrollState.scrollBy(remainingDelta)
-                    }
-                } else {
-                    scrollState.scrollTo(delta.roundToInt())
+                scrollNotificationStack(
+                    scope = coroutineScope,
+                    delta = delta,
+                    animate = false,
+                    scrimOffset = scrimOffset,
+                    minScrimOffset = minScrimOffset,
+                    scrollState = scrollState,
+                )
+            }
+    }
+
+    // if remote input state changes, compare the row and IME's overlap and offset the scrim and
+    // placeholder accordingly.
+    LaunchedEffect(isRemoteInputActive, remoteInputRowBottom, imeTop) {
+        imeTop.floatValue = 0f
+        snapshotFlow { imeTop.floatValue }
+            .collect { imeTopValue ->
+                // only scroll the stack if ime value has been populated (ime placeholder has been
+                // composed at least once), and our remote input row overlaps with the ime bounds.
+                if (isRemoteInputActive && imeTopValue > 0f && remoteInputRowBottom > imeTopValue) {
+                    scrollNotificationStack(
+                        scope = coroutineScope,
+                        delta = remoteInputRowBottom - imeTopValue,
+                        animate = true,
+                        scrimOffset = scrimOffset,
+                        minScrimOffset = minScrimOffset,
+                        scrollState = scrollState,
+                    )
                 }
             }
     }
@@ -394,12 +425,12 @@
                         scrimOffset.value < 0 &&
                             layoutState.isTransitioning(
                                 from = Scenes.Shade,
-                                to = Scenes.QuickSettings
+                                to = Scenes.QuickSettings,
                             )
                     ) {
                         IntOffset(
                             x = 0,
-                            y = (scrimOffset.value * (1 - shadeToQsFraction)).roundToInt()
+                            y = (scrimOffset.value * (1 - shadeToQsFraction)).roundToInt(),
                         )
                     } else {
                         IntOffset(x = 0, y = scrimOffset.value.roundToInt())
@@ -458,13 +489,11 @@
                     .thenIf(shouldFillMaxSize) { Modifier.fillMaxSize() }
                     .debugBackground(viewModel, DEBUG_BOX_COLOR)
         ) {
-            NotificationPlaceholder(
-                stackScrollView = stackScrollView,
-                viewModel = viewModel,
+            Column(
                 modifier =
                     Modifier.verticalNestedScrollToScene(
                             topBehavior = NestedScrollBehavior.EdgeWithPreview,
-                            isExternalOverscrollGesture = { isCurrentGestureOverscroll.value }
+                            isExternalOverscrollGesture = { isCurrentGestureOverscroll.value },
                         )
                         .thenIf(shadeMode == ShadeMode.Single) {
                             Modifier.nestedScroll(scrimNestedScrollConnection)
@@ -473,18 +502,31 @@
                         .verticalScroll(scrollState)
                         .padding(top = topPadding)
                         .fillMaxWidth()
-                        .notificationStackHeight(
-                            view = stackScrollView,
-                            totalVerticalPadding = topPadding + bottomPadding,
-                        )
-                        .onSizeChanged { size -> stackHeight.intValue = size.height },
-            )
+            ) {
+                NotificationPlaceholder(
+                    stackScrollView = stackScrollView,
+                    viewModel = viewModel,
+                    modifier =
+                        Modifier.notificationStackHeight(
+                                view = stackScrollView,
+                                totalVerticalPadding = topPadding + bottomPadding,
+                            )
+                            .onSizeChanged { size -> stackHeight.intValue = size.height },
+                )
+                Spacer(
+                    modifier =
+                        Modifier.windowInsetsBottomHeight(WindowInsets.imeAnimationTarget)
+                            .onGloballyPositioned { coordinates: LayoutCoordinates ->
+                                imeTop.floatValue = screenHeight - coordinates.size.height
+                            }
+                )
+            }
         }
         if (shouldIncludeHeadsUpSpace) {
             HeadsUpNotificationSpace(
                 stackScrollView = stackScrollView,
                 viewModel = viewModel,
-                modifier = Modifier.padding(top = topPadding)
+                modifier = Modifier.padding(top = topPadding),
             )
         }
     }
@@ -572,6 +614,42 @@
     )
 }
 
+private suspend fun scrollNotificationStack(
+    scope: CoroutineScope,
+    delta: Float,
+    animate: Boolean,
+    scrimOffset: Animatable<Float, AnimationVector1D>,
+    minScrimOffset: () -> Float,
+    scrollState: ScrollState,
+) {
+    val minOffset = minScrimOffset()
+    if (scrimOffset.value > minOffset) {
+        val remainingDelta =
+            (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f).roundToInt()
+        if (remainingDelta > 0) {
+            if (animate) {
+                // launch a new coroutine for the remainder animation so that it doesn't suspend the
+                // scrim animation, allowing both to play simultaneously.
+                scope.launch { scrollState.animateScrollTo(remainingDelta) }
+            } else {
+                scrollState.scrollTo(remainingDelta)
+            }
+        }
+        val newScrimOffset = (scrimOffset.value - delta).coerceAtLeast(minOffset)
+        if (animate) {
+            scrimOffset.animateTo(newScrimOffset)
+        } else {
+            scrimOffset.snapTo(newScrimOffset)
+        }
+    } else {
+        if (animate) {
+            scrollState.animateScrollBy(delta)
+        } else {
+            scrollState.scrollBy(delta)
+        }
+    }
+}
+
 private fun calculateCornerRadius(
     scrimCornerRadius: Dp,
     screenCornerRadius: Dp,
@@ -618,7 +696,7 @@
     setCurrent: (Float) -> Unit,
     min: Float,
     max: Float,
-    delta: Float
+    delta: Float,
 ): Float {
     return if (delta < 0 && current > min) {
         val remainder = (current + delta - min).coerceAtMost(0f)
@@ -631,10 +709,7 @@
     } else 0f
 }
 
-private inline fun debugLog(
-    viewModel: NotificationsPlaceholderViewModel,
-    msg: () -> Any,
-) {
+private inline fun debugLog(viewModel: NotificationsPlaceholderViewModel, msg: () -> Any) {
     if (viewModel.isDebugLoggingEnabled) {
         Log.d(TAG, msg().toString())
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index fa92bef34..0c1c165 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -61,6 +61,7 @@
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.res.colorResource
@@ -79,7 +80,6 @@
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
 import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
-import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -229,17 +229,16 @@
                 }
                 .thenIf(cutoutLocation != CutoutLocation.CENTER) { Modifier.displayCutoutPadding() }
     ) {
+        val density = LocalDensity.current
         val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsStateWithLifecycle()
         val isCustomizerShowing by
             viewModel.qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle()
         val customizingAnimationDuration by
             viewModel.qsSceneAdapter.customizerAnimationDuration.collectAsStateWithLifecycle()
-        val screenHeight = LocalRawScreenHeight.current
+        val screenHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
 
         BackHandler(enabled = isCustomizing) { viewModel.qsSceneAdapter.requestCloseCustomizer() }
 
-        val collapsedHeaderHeight =
-            with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
         val lifecycleOwner = LocalLifecycleOwner.current
         val footerActionsViewModel =
             remember(lifecycleOwner, viewModel) {
@@ -268,7 +267,6 @@
 
         val navBarBottomHeight =
             WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
-        val density = LocalDensity.current
         val bottomPadding by
             animateDpAsState(
                 targetValue = if (isCustomizing) 0.dp else navBarBottomHeight,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index b85523b..6c4edf4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.shade.ui.composable
 
+import android.view.HapticFeedbackConstants
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
@@ -39,17 +40,20 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.LowestZIndexContentPicker
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
+import com.android.systemui.scene.shared.model.Scenes
 
 /** Renders a lightweight shade UI container, as an overlay. */
 @Composable
@@ -58,6 +62,13 @@
     modifier: Modifier = Modifier,
     content: @Composable () -> Unit,
 ) {
+    val view = LocalView.current
+    LaunchedEffect(Unit) {
+        if (layoutState.currentTransition?.fromContent == Scenes.Gone) {
+            view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
+        }
+    }
+
     Box(modifier) {
         Scrim(onClicked = onScrimClicked)
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index fb9dde3..0bb1d92 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -51,6 +51,7 @@
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.util.fastAll
 import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastSumBy
 import com.android.compose.ui.util.SpaceVectorConverter
@@ -234,8 +235,15 @@
                     pointersDown == 0 -> {
                         startedPosition = null
 
-                        val lastPointerUp = changes.single { it.id == velocityPointerId }
-                        velocityTracker.addPointerInputChange(lastPointerUp)
+                        // In case of multiple events with 0 pointers down (not pressed) we may have
+                        // already removed the velocityPointer
+                        val lastPointerUp = changes.fastFilter { it.id == velocityPointerId }
+                        check(lastPointerUp.isEmpty() || lastPointerUp.size == 1) {
+                            "There are ${lastPointerUp.size} pointers up: $lastPointerUp"
+                        }
+                        if (lastPointerUp.size == 1) {
+                            velocityTracker.addPointerInputChange(lastPointerUp.first())
+                        }
                     }
 
                     // The first pointer down, startedPosition was not set.
diff --git a/packages/SystemUI/docs/scene.md b/packages/SystemUI/docs/scene.md
index 0ac15c5..234c7a0 100644
--- a/packages/SystemUI/docs/scene.md
+++ b/packages/SystemUI/docs/scene.md
@@ -68,15 +68,13 @@
 1.  Set a collection of **aconfig flags** to `true` by running the following
     commands:
     ```console
-    $ adb shell device_config override systemui com.android.systemui.scene_container true
-    $ adb shell device_config override systemui com.android.systemui.compose_lockscreen true
     $ adb shell device_config override systemui com.android.systemui.keyguard_bottom_area_refactor true
     $ adb shell device_config override systemui com.android.systemui.keyguard_wm_state_refactor true
-    $ adb shell device_config override systemui com.android.systemui.media_in_scene_container true
     $ adb shell device_config override systemui com.android.systemui.migrate_clocks_to_blueprint true
-    $ adb shell device_config override systemui com.android.systemui.notifications_heads_up_refactor true
+    $ adb shell device_config override systemui com.android.systemui.notification_avalanche_throttle_hun true
     $ adb shell device_config override systemui com.android.systemui.predictive_back_sysui true
     $ adb shell device_config override systemui com.android.systemui.device_entry_udfps_refactor true
+    $ adb shell device_config override systemui com.android.systemui.scene_container true
     ```
 2.  **Restart** System UI by issuing the following command:
     ```console
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 156e068..312e62d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -196,6 +196,28 @@
     }
 
     @Test
+    public void testUserSwitcherToOneHandedRemovesViews() {
+        // Can happen when a SIM is inserted into a large screen device
+        initMode(MODE_USER_SWITCHER);
+        {
+            View view1 = mKeyguardSecurityContainer.findViewById(
+                    R.id.keyguard_bouncer_user_switcher);
+            View view2 = mKeyguardSecurityContainer.findViewById(R.id.user_switcher_header);
+            assertThat(view1).isNotNull();
+            assertThat(view2).isNotNull();
+        }
+
+        initMode(MODE_ONE_HANDED);
+        {
+            View view1 = mKeyguardSecurityContainer.findViewById(
+                    R.id.keyguard_bouncer_user_switcher);
+            View view2 = mKeyguardSecurityContainer.findViewById(R.id.user_switcher_header);
+            assertThat(view1).isNull();
+            assertThat(view2).isNull();
+        }
+    }
+
+    @Test
     public void updatePosition_movesKeyguard() {
         setupForUpdateKeyguardPosition(/* oneHandedMode= */ true);
         mKeyguardSecurityContainer.updatePositionByTouchX(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index c2acc5f..160865d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -31,8 +31,11 @@
 import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
@@ -211,7 +214,7 @@
             val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
             kosmos.fakeUserRepository.setSelectedUserInfo(
                 primaryUser,
-                SelectionStatus.SELECTION_COMPLETE
+                SelectionStatus.SELECTION_COMPLETE,
             )
 
             kosmos.fakeTrustRepository.setCurrentUserTrusted(true)
@@ -240,6 +243,49 @@
         }
 
     @Test
+    fun deviceUnlockStatus_becomesUnlocked_whenFingerprintUnlocked_whileDeviceAsleepInAod() =
+        testScope.runTest {
+            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = this,
+            )
+            kosmos.powerInteractor.setAsleepForTest()
+            runCurrent()
+
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+        }
+
+    @Test
+    fun deviceUnlockStatus_staysLocked_whenFingerprintUnlocked_whileDeviceAsleep() =
+        testScope.runTest {
+            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+            assertThat(kosmos.keyguardTransitionInteractor.getCurrentState())
+                .isEqualTo(KeyguardState.LOCKSCREEN)
+
+            kosmos.powerInteractor.setAsleepForTest()
+            runCurrent()
+
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+        }
+
+    @Test
     fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_alwaysNull() =
         testScope.runTest {
             kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
@@ -273,7 +319,7 @@
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
                     DeviceEntryRestrictionReason.UserLockdown,
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
-                    DeviceEntryRestrictionReason.PolicyLockdown
+                    DeviceEntryRestrictionReason.PolicyLockdown,
             )
         }
 
@@ -285,7 +331,7 @@
             kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
             kosmos.fakeSystemPropertiesHelper.set(
                 DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
-                "not mainline reboot"
+                "not mainline reboot",
             )
             runCurrent()
 
@@ -321,7 +367,7 @@
             kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
             kosmos.fakeSystemPropertiesHelper.set(
                 DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
-                "not mainline reboot"
+                "not mainline reboot",
             )
             runCurrent()
 
@@ -358,7 +404,7 @@
             kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
             kosmos.fakeSystemPropertiesHelper.set(
                 DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
-                "not mainline reboot"
+                "not mainline reboot",
             )
             runCurrent()
 
@@ -394,12 +440,12 @@
                 collectLastValue(underTest.deviceEntryRestrictionReason)
             kosmos.fakeSystemPropertiesHelper.set(
                 DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
-                DeviceUnlockedInteractor.REBOOT_MAINLINE_UPDATE
+                DeviceUnlockedInteractor.REBOOT_MAINLINE_UPDATE,
             )
             kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
                 AuthenticationFlags(
                     userId = 1,
-                    flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
+                    flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT,
                 )
             )
             runCurrent()
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 50b727c..9cfd328 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
@@ -25,6 +25,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.education.data.model.EduDeviceConnectionTime
 import com.android.systemui.education.data.model.GestureEduModel
+import com.android.systemui.education.domain.interactor.mockEduInputManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
@@ -62,7 +63,13 @@
         // Create TestContext here because TemporaryFolder.create() is called in @Before. It is
         // needed before calling TemporaryFolder.newFolder().
         val testContext = TestContext(context, tmpFolder.newFolder())
-        underTest = UserContextualEducationRepository(testContext, dsScopeProvider)
+        underTest =
+            UserContextualEducationRepository(
+                testContext,
+                dsScopeProvider,
+                kosmos.mockEduInputManager,
+                kosmos.testDispatcher
+            )
         underTest.setUser(testUserId)
     }
 
@@ -99,7 +106,8 @@
                     lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(),
                     lastEducationTime = kosmos.fakeEduClock.instant(),
                     usageSessionStartTime = kosmos.fakeEduClock.instant(),
-                    userId = testUserId
+                    userId = testUserId,
+                    gestureType = BACK
                 )
             underTest.updateGestureEduModel(BACK) { newModel }
             val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
index 64915fb..8201bbe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
@@ -17,15 +17,13 @@
 package com.android.systemui.education.domain.interactor
 
 import android.content.pm.UserInfo
-import android.hardware.input.InputManager
-import android.hardware.input.KeyGestureEvent
-import android.view.KeyEvent
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.contextualeducation.GestureType.ALL_APPS
 import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.contextualeducation.GestureType.HOME
+import com.android.systemui.contextualeducation.GestureType.OVERVIEW
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.education.data.model.GestureEduModel
@@ -40,20 +38,21 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.kotlin.any
-import org.mockito.kotlin.verify
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @kotlinx.coroutines.ExperimentalCoroutinesApi
-class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
+class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val contextualEduInteractor = kosmos.contextualEducationInteractor
@@ -71,21 +70,27 @@
         underTest.start()
         contextualEduInteractor.start()
         userRepository.setUserInfos(USER_INFOS)
+        testScope.launch {
+            contextualEduInteractor.updateKeyboardFirstConnectionTime()
+            contextualEduInteractor.updateTouchpadFirstConnectionTime()
+        }
     }
 
     @Test
     fun newEducationInfoOnMaxSignalCountReached() =
         testScope.runTest {
-            triggerMaxEducationSignals(BACK)
+            triggerMaxEducationSignals(gestureType)
             val model by collectLastValue(underTest.educationTriggered)
-            assertThat(model?.gestureType).isEqualTo(BACK)
+
+            assertThat(model?.gestureType).isEqualTo(gestureType)
         }
 
     @Test
     fun newEducationToastOn1stEducation() =
         testScope.runTest {
             val model by collectLastValue(underTest.educationTriggered)
-            triggerMaxEducationSignals(BACK)
+            triggerMaxEducationSignals(gestureType)
+
             assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast)
         }
 
@@ -93,12 +98,12 @@
     fun newEducationNotificationOn2ndEducation() =
         testScope.runTest {
             val model by collectLastValue(underTest.educationTriggered)
-            triggerMaxEducationSignals(BACK)
+            triggerMaxEducationSignals(gestureType)
             // runCurrent() to trigger 1st education
             runCurrent()
 
             eduClock.offset(minDurationForNextEdu)
-            triggerMaxEducationSignals(BACK)
+            triggerMaxEducationSignals(gestureType)
 
             assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification)
         }
@@ -106,7 +111,7 @@
     @Test
     fun noEducationInfoBeforeMaxSignalCountReached() =
         testScope.runTest {
-            contextualEduInteractor.incrementSignalCount(BACK)
+            contextualEduInteractor.incrementSignalCount(gestureType)
             val model by collectLastValue(underTest.educationTriggered)
             assertThat(model).isNull()
         }
@@ -115,8 +120,8 @@
     fun noEducationInfoWhenShortcutTriggeredPreviously() =
         testScope.runTest {
             val model by collectLastValue(underTest.educationTriggered)
-            contextualEduInteractor.updateShortcutTriggerTime(BACK)
-            triggerMaxEducationSignals(BACK)
+            contextualEduInteractor.updateShortcutTriggerTime(gestureType)
+            triggerMaxEducationSignals(gestureType)
             assertThat(model).isNull()
         }
 
@@ -124,12 +129,12 @@
     fun no2ndEducationBeforeMinEduIntervalReached() =
         testScope.runTest {
             val models by collectValues(underTest.educationTriggered)
-            triggerMaxEducationSignals(BACK)
+            triggerMaxEducationSignals(gestureType)
             runCurrent()
 
             // Offset a duration that is less than the required education interval
             eduClock.offset(1.seconds)
-            triggerMaxEducationSignals(BACK)
+            triggerMaxEducationSignals(gestureType)
             runCurrent()
 
             assertThat(models.filterNotNull().size).isEqualTo(1)
@@ -140,15 +145,15 @@
         testScope.runTest {
             val models by collectValues(underTest.educationTriggered)
             // Trigger 2 educations
-            triggerMaxEducationSignals(BACK)
+            triggerMaxEducationSignals(gestureType)
             runCurrent()
             eduClock.offset(minDurationForNextEdu)
-            triggerMaxEducationSignals(BACK)
+            triggerMaxEducationSignals(gestureType)
             runCurrent()
 
             // Try triggering 3rd education
             eduClock.offset(minDurationForNextEdu)
-            triggerMaxEducationSignals(BACK)
+            triggerMaxEducationSignals(gestureType)
 
             assertThat(models.filterNotNull().size).isEqualTo(2)
         }
@@ -157,18 +162,21 @@
     fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() =
         testScope.runTest {
             val model by
-                collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
-            contextualEduInteractor.incrementSignalCount(BACK)
+                collectLastValue(
+                    kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType)
+                )
+            contextualEduInteractor.incrementSignalCount(gestureType)
             eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds))
             val secondSignalReceivedTime = eduClock.instant()
-            contextualEduInteractor.incrementSignalCount(BACK)
+            contextualEduInteractor.incrementSignalCount(gestureType)
 
             assertThat(model)
                 .isEqualTo(
                     GestureEduModel(
                         signalCount = 1,
                         usageSessionStartTime = secondSignalReceivedTime,
-                        userId = 0
+                        userId = 0,
+                        gestureType = gestureType
                     )
                 )
         }
@@ -252,22 +260,9 @@
     @Test
     fun updateShortcutTimeOnKeyboardShortcutTriggered() =
         testScope.runTest {
-            // runCurrent() to trigger inputManager#registerKeyGestureEventListener in the
-            // interactor
-            runCurrent()
-            val listenerCaptor =
-                ArgumentCaptor.forClass(InputManager.KeyGestureEventListener::class.java)
-            verify(kosmos.mockEduInputManager)
-                .registerKeyGestureEventListener(any(), listenerCaptor.capture())
-
-            val allAppsKeyGestureEvent =
-                KeyGestureEvent.Builder()
-                    .setDeviceId(1)
-                    .setModifierState(KeyEvent.META_META_ON)
-                    .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS)
-                    .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
-                    .build()
-            listenerCaptor.value.onKeyGestureEvent(allAppsKeyGestureEvent)
+            // Only All Apps needs to update the keyboard shortcut
+            assumeTrue(gestureType == ALL_APPS)
+            kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS)
 
             val model by
                 collectLastValue(
@@ -293,10 +288,18 @@
         runCurrent()
     }
 
+    private suspend fun setUpForDeviceConnection() {
+        contextualEduInteractor.updateKeyboardFirstConnectionTime()
+        contextualEduInteractor.updateTouchpadFirstConnectionTime()
+    }
+
     companion object {
-        private val USER_INFOS =
-            listOf(
-                UserInfo(101, "Second User", 0),
-            )
+        private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))
+
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getGestureTypes(): List<GestureType> {
+            return listOf(BACK, HOME, OVERVIEW, ALL_APPS)
+        }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
index c4ac585..ab33269 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -69,6 +70,11 @@
 
     @Before
     fun setUp() {
+        testScope.launch {
+            interactor.updateKeyboardFirstConnectionTime()
+            interactor.updateTouchpadFirstConnectionTime()
+        }
+
         val viewModel =
             ContextualEduViewModel(
                 kosmos.applicationContext.resources,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
index 686b518..366b55d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
@@ -23,6 +23,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.classifier.falsingManager
 import com.android.systemui.haptics.fakeVibratorHelper
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.core.FakeLogBuffer
@@ -68,11 +69,13 @@
         vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_SPIN] = spinDuration
 
         whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(true)
+        kosmos.falsingManager.setFalseLongTap(false)
 
         longPressEffect =
             QSLongPressEffect(
                 vibratorHelper,
                 kosmos.keyguardStateController,
+                kosmos.falsingManager,
                 FakeLogBuffer.Factory.create(),
             )
         longPressEffect.callback = callback
@@ -180,11 +183,7 @@
 
         // THEN the expected texture is played
         val reverseHaptics =
-            LongPressHapticBuilder.createReversedEffect(
-                progress,
-                lowTickDuration,
-                effectDuration,
-            )
+            LongPressHapticBuilder.createReversedEffect(progress, lowTickDuration, effectDuration)
         assertThat(reverseHaptics).isNotNull()
         assertThat(vibratorHelper.hasVibratedWithEffects(reverseHaptics!!)).isTrue()
     }
@@ -224,6 +223,20 @@
         }
 
     @Test
+    fun onAnimationComplete_isFalseLongClick_effectEndsInIdleWithReset() =
+        testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
+            // GIVEN that the long-click is false
+            kosmos.falsingManager.setFalseLongTap(true)
+
+            // GIVEN that the animation completes
+            longPressEffect.handleAnimationComplete()
+
+            // THEN the long-press effect ends in the idle state and the properties are reset
+            assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
+            verify(callback, times(1)).onResetProperties()
+        }
+
+    @Test
     fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversingAndClick() =
         testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP) {
             // GIVEN that the animation completes
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
index 6c3c7ef..fcf4662 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
@@ -16,7 +16,10 @@
  */
 package com.android.systemui.keyguard.data.quickaffordance
 
+import android.app.Flags
 import android.net.Uri
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.provider.Settings
 import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
 import android.provider.Settings.Global.ZEN_MODE_OFF
@@ -25,6 +28,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.notification.modes.EnableZenModeDialog
+import com.android.settingslib.notification.modes.TestModeBuilder
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.ContentDescription
@@ -35,7 +39,11 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
 import com.android.systemui.statusbar.policy.ZenModeController
+import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
+import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
@@ -43,6 +51,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
+import java.time.Duration
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -66,8 +75,13 @@
     private val kosmos = testKosmos()
     private val testDispatcher = kosmos.testDispatcher
     private val testScope = kosmos.testScope
+
     private val settings = kosmos.fakeSettings
 
+    private val zenModeRepository = kosmos.fakeZenModeRepository
+    private val deviceProvisioningRepository = kosmos.fakeDeviceProvisioningRepository
+    private val secureSettingsRepository = kosmos.secureSettingsRepository
+
     @Mock private lateinit var zenModeController: ZenModeController
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var conditionUri: Uri
@@ -85,17 +99,36 @@
             DoNotDisturbQuickAffordanceConfig(
                 context,
                 zenModeController,
+                kosmos.zenModeInteractor,
                 settings,
                 userTracker,
                 testDispatcher,
+                testScope.backgroundScope,
                 conditionUri,
                 enableZenModeDialog,
             )
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_UI)
     fun dndNotAvailable_pickerStateHidden() =
         testScope.runTest {
+            deviceProvisioningRepository.setDeviceProvisioned(false)
+            runCurrent()
+
+            val result = underTest.getPickerScreenState()
+            runCurrent()
+
+            assertEquals(
+                KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice,
+                result,
+            )
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_UI)
+    fun controllerDndNotAvailable_pickerStateHidden() =
+        testScope.runTest {
             // given
             whenever(zenModeController.isZenAvailable).thenReturn(false)
 
@@ -105,13 +138,33 @@
             // then
             assertEquals(
                 KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice,
-                result
+                result,
             )
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_UI)
     fun dndAvailable_pickerStateVisible() =
         testScope.runTest {
+            deviceProvisioningRepository.setDeviceProvisioned(true)
+            runCurrent()
+
+            val result = underTest.getPickerScreenState()
+            runCurrent()
+
+            assertThat(result)
+                .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java)
+            val defaultPickerState =
+                result as KeyguardQuickAffordanceConfig.PickerScreenState.Default
+            assertThat(defaultPickerState.configureIntent).isNotNull()
+            assertThat(defaultPickerState.configureIntent?.action)
+                .isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_UI)
+    fun controllerDndAvailable_pickerStateVisible() =
+        testScope.runTest {
             // given
             whenever(zenModeController.isZenAvailable).thenReturn(true)
 
@@ -129,7 +182,27 @@
         }
 
     @Test
-    fun onTriggered_dndModeIsNotZEN_MODE_OFF_setToZEN_MODE_OFF() =
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    fun onTriggered_dndModeIsNotOff_setToOff() =
+        testScope.runTest {
+            val currentModes by collectLastValue(zenModeRepository.modes)
+
+            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+            secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -2)
+            collectLastValue(underTest.lockScreenState)
+            runCurrent()
+
+            val result = underTest.onTriggered(null)
+            runCurrent()
+
+            val dndMode = currentModes!!.single()
+            assertThat(dndMode.isActive).isFalse()
+            assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_UI)
+    fun onTriggered_controllerDndModeIsNotZEN_MODE_OFF_setToZEN_MODE_OFF() =
         testScope.runTest {
             // given
             whenever(zenModeController.isZenAvailable).thenReturn(true)
@@ -140,11 +213,12 @@
 
             // when
             val result = underTest.onTriggered(null)
+
             verify(zenModeController)
                 .setZen(
                     spyZenMode.capture(),
                     spyConditionId.capture(),
-                    eq(DoNotDisturbQuickAffordanceConfig.TAG)
+                    eq(DoNotDisturbQuickAffordanceConfig.TAG),
                 )
 
             // then
@@ -154,7 +228,28 @@
         }
 
     @Test
-    fun onTriggered_dndModeIsZEN_MODE_OFF_settingFOREVER_setZenWithoutCondition() =
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    fun onTriggered_dndModeIsOff_settingFOREVER_setZenWithoutCondition() =
+        testScope.runTest {
+            val currentModes by collectLastValue(zenModeRepository.modes)
+
+            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+            secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER)
+            collectLastValue(underTest.lockScreenState)
+            runCurrent()
+
+            val result = underTest.onTriggered(null)
+            runCurrent()
+
+            val dndMode = currentModes!!.single()
+            assertThat(dndMode.isActive).isTrue()
+            assertThat(zenModeRepository.getModeActiveDuration(dndMode.id)).isNull()
+            assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_UI)
+    fun onTriggered_controllerDndModeIsZEN_MODE_OFF_settingFOREVER_setZenWithoutCondition() =
         testScope.runTest {
             // given
             whenever(zenModeController.isZenAvailable).thenReturn(true)
@@ -169,7 +264,7 @@
                 .setZen(
                     spyZenMode.capture(),
                     spyConditionId.capture(),
-                    eq(DoNotDisturbQuickAffordanceConfig.TAG)
+                    eq(DoNotDisturbQuickAffordanceConfig.TAG),
                 )
 
             // then
@@ -179,7 +274,27 @@
         }
 
     @Test
-    fun onTriggered_dndZEN_MODE_OFF_settingNotFOREVERorPROMPT_zenWithCondition() =
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    fun onTriggered_dndModeIsOff_settingNotFOREVERorPROMPT_dndWithDuration() =
+        testScope.runTest {
+            val currentModes by collectLastValue(zenModeRepository.modes)
+            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+            secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -900)
+            runCurrent()
+
+            val result = underTest.onTriggered(null)
+            runCurrent()
+
+            assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+            val dndMode = currentModes!!.single()
+            assertThat(dndMode.isActive).isTrue()
+            assertThat(zenModeRepository.getModeActiveDuration(dndMode.id))
+                .isEqualTo(Duration.ofMinutes(-900))
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_UI)
+    fun onTriggered_controllerDndZEN_MODE_OFF_settingNotFOREVERorPROMPT_zenWithCondition() =
         testScope.runTest {
             // given
             whenever(zenModeController.isZenAvailable).thenReturn(true)
@@ -194,7 +309,7 @@
                 .setZen(
                     spyZenMode.capture(),
                     spyConditionId.capture(),
-                    eq(DoNotDisturbQuickAffordanceConfig.TAG)
+                    eq(DoNotDisturbQuickAffordanceConfig.TAG),
                 )
 
             // then
@@ -204,7 +319,28 @@
         }
 
     @Test
-    fun onTriggered_dndModeIsZEN_MODE_OFF_settingIsPROMPT_showDialog() =
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    fun onTriggered_dndModeIsOff_settingIsPROMPT_showDialog() =
+        testScope.runTest {
+            val expandable: Expandable = mock()
+            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+            secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT)
+            whenever(enableZenModeDialog.createDialog()).thenReturn(mock())
+            collectLastValue(underTest.lockScreenState)
+            runCurrent()
+
+            val result = underTest.onTriggered(expandable)
+
+            assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog)
+            assertEquals(
+                expandable,
+                (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable,
+            )
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_UI)
+    fun onTriggered_controllerDndModeIsZEN_MODE_OFF_settingIsPROMPT_showDialog() =
         testScope.runTest {
             // given
             val expandable: Expandable = mock()
@@ -222,13 +358,31 @@
             assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog)
             assertEquals(
                 expandable,
-                (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable
+                (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable,
             )
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_UI)
     fun lockScreenState_dndAvailableStartsAsTrue_changeToFalse_StateIsHidden() =
         testScope.runTest {
+            deviceProvisioningRepository.setDeviceProvisioned(true)
+            val valueSnapshot = collectLastValue(underTest.lockScreenState)
+            val secondLastValue = valueSnapshot()
+            runCurrent()
+
+            deviceProvisioningRepository.setDeviceProvisioned(false)
+            runCurrent()
+            val lastValue = valueSnapshot()
+
+            assertTrue(secondLastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+            assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_UI)
+    fun lockScreenState_controllerDndAvailableStartsAsTrue_changeToFalse_StateIsHidden() =
+        testScope.runTest {
             // given
             whenever(zenModeController.isZenAvailable).thenReturn(true)
             val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
@@ -246,7 +400,44 @@
         }
 
     @Test
-    fun lockScreenState_dndModeStartsAsZEN_MODE_OFF_changeToNotOFF_StateVisible() =
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    fun lockScreenState_dndModeStartsAsOff_changeToOn_StateVisible() =
+        testScope.runTest {
+            val lockScreenState by collectLastValue(underTest.lockScreenState)
+
+            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+            runCurrent()
+
+            assertThat(lockScreenState)
+                .isEqualTo(
+                    KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                        Icon.Resource(
+                            R.drawable.qs_dnd_icon_off,
+                            ContentDescription.Resource(R.string.dnd_is_off),
+                        ),
+                        ActivationState.Inactive,
+                    )
+                )
+
+            zenModeRepository.removeMode(TestModeBuilder.MANUAL_DND_INACTIVE.id)
+            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+            runCurrent()
+
+            assertThat(lockScreenState)
+                .isEqualTo(
+                    KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                        Icon.Resource(
+                            R.drawable.qs_dnd_icon_on,
+                            ContentDescription.Resource(R.string.dnd_is_on),
+                        ),
+                        ActivationState.Active,
+                    )
+                )
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MODES_UI)
+    fun lockScreenState_controllerDndModeStartsAsZEN_MODE_OFF_changeToNotOFF_StateVisible() =
         testScope.runTest {
             // given
             whenever(zenModeController.isZenAvailable).thenReturn(true)
@@ -265,9 +456,9 @@
                 KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                     Icon.Resource(
                         R.drawable.qs_dnd_icon_off,
-                        ContentDescription.Resource(R.string.dnd_is_off)
+                        ContentDescription.Resource(R.string.dnd_is_off),
                     ),
-                    ActivationState.Inactive
+                    ActivationState.Inactive,
                 ),
                 secondLastValue,
             )
@@ -275,9 +466,9 @@
                 KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                     Icon.Resource(
                         R.drawable.qs_dnd_icon_on,
-                        ContentDescription.Resource(R.string.dnd_is_on)
+                        ContentDescription.Resource(R.string.dnd_is_on),
                     ),
-                    ActivationState.Active
+                    ActivationState.Active,
                 ),
                 lastValue,
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index 59f16d7..84b7f5c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -17,17 +17,16 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
 import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
@@ -59,8 +58,8 @@
 class KeyguardBlueprintInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val underTest = kosmos.keyguardBlueprintInteractor
-    private val keyguardBlueprintRepository = kosmos.keyguardBlueprintRepository
+    private val underTest by lazy { kosmos.keyguardBlueprintInteractor }
+    private val keyguardBlueprintRepository by lazy { kosmos.keyguardBlueprintRepository }
     private val clockRepository by lazy { kosmos.fakeKeyguardClockRepository }
     private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
     private val fingerprintPropertyRepository by lazy { kosmos.fakeFingerprintPropertyRepository }
@@ -75,7 +74,7 @@
             sensorId = 1,
             strength = SensorStrength.STRONG,
             sensorType = FingerprintSensorType.POWER_BUTTON,
-            sensorLocations = mapOf()
+            sensorLocations = mapOf(),
         )
     }
 
@@ -93,7 +92,7 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+    @DisableSceneContainer
     fun testAppliesSplitShadeBlueprint() {
         testScope.runTest {
             val blueprintId by collectLastValue(underTest.blueprintId)
@@ -107,7 +106,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+    @EnableSceneContainer
     fun testDoesNotApplySplitShadeBlueprint() {
         testScope.runTest {
             val blueprintId by collectLastValue(underTest.blueprintId)
@@ -122,7 +121,7 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
+    @DisableSceneContainer
     fun fingerprintPropertyInitialized_updatesBlueprint() {
         testScope.runTest {
             underTest.start()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
index 41c5b73..ff6ea3a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -25,6 +25,8 @@
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
@@ -44,6 +46,7 @@
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Answers
@@ -71,10 +74,8 @@
     private val burnInFlow = MutableStateFlow(BurnInModel())
 
     @Before
-    @DisableFlags(
-        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
-        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
-    )
+    @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+    @DisableSceneContainer
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow)
@@ -112,18 +113,13 @@
                     from = KeyguardState.AOD,
                     to = KeyguardState.LOCKSCREEN,
                     value = 1f,
-                    transitionState = TransitionState.FINISHED
+                    transitionState = TransitionState.FINISHED,
                 ),
                 validateStep = false,
             )
 
             // Trigger a change to the burn-in model
-            burnInFlow.value =
-                BurnInModel(
-                    translationX = 20,
-                    translationY = 30,
-                    scale = 0.5f,
-                )
+            burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f)
 
             assertThat(movement?.translationX).isEqualTo(0)
             assertThat(movement?.translationY).isEqualTo(0)
@@ -143,17 +139,12 @@
                     from = KeyguardState.GONE,
                     to = KeyguardState.AOD,
                     value = 1f,
-                    transitionState = TransitionState.FINISHED
+                    transitionState = TransitionState.FINISHED,
                 ),
                 validateStep = false,
             )
             // Trigger a change to the burn-in model
-            burnInFlow.value =
-                BurnInModel(
-                    translationX = 20,
-                    translationY = 30,
-                    scale = 0.5f,
-                )
+            burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f)
 
             assertThat(movement?.translationX).isEqualTo(20)
             assertThat(movement?.translationY).isEqualTo(30)
@@ -166,7 +157,7 @@
                     from = KeyguardState.GONE,
                     to = KeyguardState.AOD,
                     value = 0f,
-                    transitionState = TransitionState.STARTED
+                    transitionState = TransitionState.STARTED,
                 ),
                 validateStep = false,
             )
@@ -180,11 +171,7 @@
     @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_whenFullyDozing_MigrationFlagOff_staysOutOfTopInset() =
         testScope.runTest {
-            burnInParameters =
-                burnInParameters.copy(
-                    minViewY = 100,
-                    topInset = 80,
-                )
+            burnInParameters = burnInParameters.copy(minViewY = 100, topInset = 80)
             val movement by collectLastValue(underTest.movement(burnInParameters))
 
             // Set to dozing (on AOD)
@@ -193,18 +180,13 @@
                     from = KeyguardState.GONE,
                     to = KeyguardState.AOD,
                     value = 1f,
-                    transitionState = TransitionState.FINISHED
+                    transitionState = TransitionState.FINISHED,
                 ),
                 validateStep = false,
             )
 
             // Trigger a change to the burn-in model
-            burnInFlow.value =
-                BurnInModel(
-                    translationX = 20,
-                    translationY = -30,
-                    scale = 0.5f,
-                )
+            burnInFlow.value = BurnInModel(translationX = 20, translationY = -30, scale = 0.5f)
             assertThat(movement?.translationX).isEqualTo(20)
             // -20 instead of -30, due to inset of 80
             assertThat(movement?.translationY).isEqualTo(-20)
@@ -217,7 +199,7 @@
                     from = KeyguardState.GONE,
                     to = KeyguardState.AOD,
                     value = 0f,
-                    transitionState = TransitionState.STARTED
+                    transitionState = TransitionState.STARTED,
                 ),
                 validateStep = false,
             )
@@ -231,11 +213,7 @@
     @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_whenFullyDozing_MigrationFlagOn_staysOutOfTopInset() =
         testScope.runTest {
-            burnInParameters =
-                burnInParameters.copy(
-                    minViewY = 100,
-                    topInset = 80,
-                )
+            burnInParameters = burnInParameters.copy(minViewY = 100, topInset = 80)
             val movement by collectLastValue(underTest.movement(burnInParameters))
 
             // Set to dozing (on AOD)
@@ -244,18 +222,13 @@
                     from = KeyguardState.GONE,
                     to = KeyguardState.AOD,
                     value = 1f,
-                    transitionState = TransitionState.FINISHED
+                    transitionState = TransitionState.FINISHED,
                 ),
                 validateStep = false,
             )
 
             // Trigger a change to the burn-in model
-            burnInFlow.value =
-                BurnInModel(
-                    translationX = 20,
-                    translationY = -30,
-                    scale = 0.5f,
-                )
+            burnInFlow.value = BurnInModel(translationX = 20, translationY = -30, scale = 0.5f)
             assertThat(movement?.translationX).isEqualTo(20)
             // -20 instead of -30, due to inset of 80
             assertThat(movement?.translationY).isEqualTo(-20)
@@ -268,7 +241,7 @@
                     from = KeyguardState.GONE,
                     to = KeyguardState.AOD,
                     value = 0f,
-                    transitionState = TransitionState.STARTED
+                    transitionState = TransitionState.STARTED,
                 ),
                 validateStep = false,
             )
@@ -291,18 +264,13 @@
                     from = KeyguardState.GONE,
                     to = KeyguardState.AOD,
                     value = 1f,
-                    transitionState = TransitionState.FINISHED
+                    transitionState = TransitionState.FINISHED,
                 ),
                 validateStep = false,
             )
 
             // Trigger a change to the burn-in model
-            burnInFlow.value =
-                BurnInModel(
-                    translationX = 20,
-                    translationY = 30,
-                    scale = 0.5f,
-                )
+            burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f)
 
             assertThat(movement?.translationX).isEqualTo(20)
             assertThat(movement?.translationY).isEqualTo(30)
@@ -311,9 +279,9 @@
         }
 
     @Test
-    @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+    @DisableSceneContainer
     @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-    fun translationAndScale_composeFlagOff_weatherLargeClock() =
+    fun translationAndScale_sceneContainerOff_weatherLargeClock() =
         testBurnInViewModelForClocks(
             isSmallClock = false,
             isWeatherClock = true,
@@ -321,9 +289,9 @@
         )
 
     @Test
-    @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+    @DisableSceneContainer
     @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-    fun translationAndScale_composeFlagOff_weatherSmallClock() =
+    fun translationAndScale_sceneContainerOff_weatherSmallClock() =
         testBurnInViewModelForClocks(
             isSmallClock = true,
             isWeatherClock = true,
@@ -331,9 +299,9 @@
         )
 
     @Test
-    @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+    @DisableSceneContainer
     @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-    fun translationAndScale_composeFlagOff_nonWeatherLargeClock() =
+    fun translationAndScale_sceneContainerOff_nonWeatherLargeClock() =
         testBurnInViewModelForClocks(
             isSmallClock = false,
             isWeatherClock = false,
@@ -341,9 +309,9 @@
         )
 
     @Test
-    @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+    @DisableSceneContainer
     @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-    fun translationAndScale_composeFlagOff_nonWeatherSmallClock() =
+    fun translationAndScale_sceneContainerOff_nonWeatherSmallClock() =
         testBurnInViewModelForClocks(
             isSmallClock = true,
             isWeatherClock = false,
@@ -351,11 +319,9 @@
         )
 
     @Test
-    @EnableFlags(
-        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
-        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
-    )
-    fun translationAndScale_composeFlagOn_weatherLargeClock() =
+    @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+    @EnableSceneContainer
+    fun translationAndScale_sceneContainerOn_weatherLargeClock() =
         testBurnInViewModelForClocks(
             isSmallClock = false,
             isWeatherClock = true,
@@ -363,11 +329,9 @@
         )
 
     @Test
-    @EnableFlags(
-        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
-        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
-    )
-    fun translationAndScale_composeFlagOn_weatherSmallClock() =
+    @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+    @EnableSceneContainer
+    fun translationAndScale_sceneContainerOn_weatherSmallClock() =
         testBurnInViewModelForClocks(
             isSmallClock = true,
             isWeatherClock = true,
@@ -375,11 +339,9 @@
         )
 
     @Test
-    @EnableFlags(
-        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
-        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
-    )
-    fun translationAndScale_composeFlagOn_nonWeatherLargeClock() =
+    @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+    @EnableSceneContainer
+    fun translationAndScale_sceneContainerOn_nonWeatherLargeClock() =
         testBurnInViewModelForClocks(
             isSmallClock = false,
             isWeatherClock = false,
@@ -387,11 +349,10 @@
         )
 
     @Test
-    @EnableFlags(
-        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
-        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
-    )
-    fun translationAndScale_composeFlagOn_nonWeatherSmallClock() =
+    @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+    @EnableSceneContainer
+    @Ignore("b/367659687")
+    fun translationAndScale_sceneContainerOn_nonWeatherSmallClock() =
         testBurnInViewModelForClocks(
             isSmallClock = true,
             isWeatherClock = false,
@@ -421,18 +382,13 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     value = 1f,
-                    transitionState = TransitionState.FINISHED
+                    transitionState = TransitionState.FINISHED,
                 ),
                 validateStep = false,
             )
 
             // Trigger a change to the burn-in model
-            burnInFlow.value =
-                BurnInModel(
-                    translationX = 20,
-                    translationY = 30,
-                    scale = 0.5f,
-                )
+            burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f)
 
             assertThat(movement?.translationX).isEqualTo(20)
             assertThat(movement?.translationY).isEqualTo(30)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index 17e1b53..05a6b87 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -16,14 +16,13 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.BrokenWithSceneContainer
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
 import com.android.systemui.keyguard.data.repository.keyguardClockRepository
@@ -229,8 +228,8 @@
         }
 
     @Test
-    @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
-    fun testSmallClockTop_splitShade_composeLockscreenOn() =
+    @EnableSceneContainer
+    fun testSmallClockTop_splitShade_sceneContainerOn() =
         testScope.runTest {
             with(kosmos) {
                 shadeRepository.setShadeLayoutWide(true)
@@ -244,8 +243,8 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
-    fun testSmallClockTop_splitShade_composeLockscreenOff() =
+    @DisableSceneContainer
+    fun testSmallClockTop_splitShade_sceneContainerOff() =
         testScope.runTest {
             with(kosmos) {
                 shadeRepository.setShadeLayoutWide(true)
@@ -257,8 +256,8 @@
         }
 
     @Test
-    @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
-    fun testSmallClockTop_nonSplitShade_composeLockscreenOn() =
+    @EnableSceneContainer
+    fun testSmallClockTop_nonSplitShade_sceneContainerOn() =
         testScope.runTest {
             with(kosmos) {
                 shadeRepository.setShadeLayoutWide(false)
@@ -270,8 +269,8 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
-    fun testSmallClockTop_nonSplitShade_composeLockscreenOff() =
+    @DisableSceneContainer
+    fun testSmallClockTop_nonSplitShade_sceneContainerOff() =
         testScope.runTest {
             with(kosmos) {
                 shadeRepository.setShadeLayoutWide(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
deleted file mode 100644
index 42db96e..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.domain.interactor
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
-import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
-import com.android.systemui.qs.panels.data.repository.gridLayoutTypeRepository
-import com.android.systemui.qs.panels.shared.model.GridLayoutType
-import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType
-import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository
-import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class GridConsistencyInteractorTest : SysuiTestCase() {
-
-    data object NoopGridLayoutType : GridLayoutType
-
-    private val kosmos =
-        testKosmos().apply {
-            defaultLargeTilesRepository =
-                object : DefaultLargeTilesRepository {
-                    override val defaultLargeTiles =
-                        setOf(
-                            TileSpec.create("largeA"),
-                            TileSpec.create("largeB"),
-                            TileSpec.create("largeC"),
-                            TileSpec.create("largeD"),
-                        )
-                }
-            gridConsistencyInteractorsMap =
-                mapOf(
-                    Pair(NoopGridLayoutType, noopGridConsistencyInteractor),
-                    Pair(InfiniteGridLayoutType, infiniteGridConsistencyInteractor)
-                )
-        }
-
-    private val underTest = with(kosmos) { gridConsistencyInteractor }
-
-    @Before
-    fun setUp() {
-        // Mostly testing InfiniteGridConsistencyInteractor because it reorders tiles
-        with(kosmos) { gridLayoutTypeRepository.setLayout(InfiniteGridLayoutType) }
-        underTest.start()
-    }
-
-    @OptIn(ExperimentalCoroutinesApi::class)
-    @Test
-    fun changeLayoutType_usesCorrectGridConsistencyInteractor() =
-        with(kosmos) {
-            testScope.runTest {
-                // Using the no-op grid consistency interactor
-                gridLayoutTypeRepository.setLayout(NoopGridLayoutType)
-
-                // Setting an invalid layout with holes
-                // [ Large A ] [ sa ]
-                // [ Large B ] [ Large C ]
-                // [ sb ] [ Large D ]
-                val newTiles =
-                    listOf(
-                        TileSpec.create("largeA"),
-                        TileSpec.create("smallA"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("largeC"),
-                        TileSpec.create("smallB"),
-                        TileSpec.create("largeD"),
-                    )
-                tileSpecRepository.setTiles(0, newTiles)
-
-                runCurrent()
-
-                val tiles = currentTilesInteractor.currentTiles.value
-                val tileSpecs = tiles.map { it.spec }
-
-                // Saved tiles should be unchanged
-                assertThat(tileSpecs).isEqualTo(newTiles)
-            }
-        }
-
-    @Test
-    fun validTilesWithInfiniteGridConsistencyInteractor_unchangedList() =
-        with(kosmos) {
-            testScope.runTest {
-                // Setting a valid layout with holes
-                // [ Large A ] [ sa ][ sb ]
-                // [ Large B ] [ Large C ]
-                // [ Large D ]
-                val newTiles =
-                    listOf(
-                        TileSpec.create("largeA"),
-                        TileSpec.create("smallA"),
-                        TileSpec.create("smallB"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("largeC"),
-                        TileSpec.create("largeD"),
-                    )
-                tileSpecRepository.setTiles(0, newTiles)
-
-                runCurrent()
-
-                val tiles = currentTilesInteractor.currentTiles.value
-                val tileSpecs = tiles.map { it.spec }
-
-                // Saved tiles should be unchanged
-                assertThat(tileSpecs).isEqualTo(newTiles)
-            }
-        }
-
-    @Test
-    fun invalidTilesWithInfiniteGridConsistencyInteractor_savesNewList() =
-        with(kosmos) {
-            testScope.runTest {
-                // Setting an invalid layout with holes
-                // [ sa ] [ Large A ]
-                // [ Large B ] [ sb ] [ sc ]
-                // [ sd ] [ se ] [ Large C ]
-                val newTiles =
-                    listOf(
-                        TileSpec.create("smallA"),
-                        TileSpec.create("largeA"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("smallB"),
-                        TileSpec.create("smallC"),
-                        TileSpec.create("smallD"),
-                        TileSpec.create("smallE"),
-                        TileSpec.create("largeC"),
-                    )
-                tileSpecRepository.setTiles(0, newTiles)
-
-                runCurrent()
-
-                val tiles = currentTilesInteractor.currentTiles.value
-                val tileSpecs = tiles.map { it.spec }
-
-                // Expected grid
-                // [ sa ] [ Large A ] [ sb ]
-                // [ Large B ] [ sc ] [ sd ]
-                // [ se ] [ Large C ]
-                val expectedTiles =
-                    listOf(
-                        TileSpec.create("smallA"),
-                        TileSpec.create("largeA"),
-                        TileSpec.create("smallB"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("smallC"),
-                        TileSpec.create("smallD"),
-                        TileSpec.create("smallE"),
-                        TileSpec.create("largeC"),
-                    )
-
-                // Saved tiles should be unchanged
-                assertThat(tileSpecs).isEqualTo(expectedTiles)
-            }
-        }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt
deleted file mode 100644
index ea51398..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.domain.interactor
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
-import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class InfiniteGridConsistencyInteractorTest : SysuiTestCase() {
-
-    private val kosmos =
-        testKosmos().apply {
-            defaultLargeTilesRepository =
-                object : DefaultLargeTilesRepository {
-                    override val defaultLargeTiles: Set<TileSpec> =
-                        setOf(
-                            TileSpec.create("largeA"),
-                            TileSpec.create("largeB"),
-                            TileSpec.create("largeC"),
-                            TileSpec.create("largeD"),
-                        )
-                }
-        }
-    private val underTest = with(kosmos) { infiniteGridConsistencyInteractor }
-
-    @Test
-    fun validTiles_returnsUnchangedList() =
-        with(kosmos) {
-            testScope.runTest {
-                // Original grid
-                // [ Large A ] [ sa ][ sb ]
-                // [ Large B ] [ Large C ]
-                // [ Large D ]
-                val tiles =
-                    listOf(
-                        TileSpec.create("largeA"),
-                        TileSpec.create("smallA"),
-                        TileSpec.create("smallB"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("largeC"),
-                        TileSpec.create("largeD"),
-                    )
-
-                val newTiles = underTest.reconcileTiles(tiles)
-
-                assertThat(newTiles).isEqualTo(tiles)
-            }
-        }
-
-    @Test
-    fun invalidTiles_moveIconTileForward() =
-        with(kosmos) {
-            testScope.runTest {
-                // Original grid
-                // [ Large A ] [ sa ]
-                // [ Large B ] [ Large C ]
-                // [ sb ] [ Large D ]
-                val tiles =
-                    listOf(
-                        TileSpec.create("largeA"),
-                        TileSpec.create("smallA"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("largeC"),
-                        TileSpec.create("smallB"),
-                        TileSpec.create("largeD"),
-                    )
-                // Expected grid
-                // [ Large A ] [ sa ][ sb ]
-                // [ Large B ] [ Large C ]
-                // [ Large D ]
-                val expectedTiles =
-                    listOf(
-                        TileSpec.create("largeA"),
-                        TileSpec.create("smallA"),
-                        TileSpec.create("smallB"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("largeC"),
-                        TileSpec.create("largeD"),
-                    )
-
-                val newTiles = underTest.reconcileTiles(tiles)
-
-                assertThat(newTiles).isEqualTo(expectedTiles)
-            }
-        }
-
-    @Test
-    fun invalidTiles_moveIconTileBack() =
-        with(kosmos) {
-            testScope.runTest {
-                // Original grid
-                // [ sa ] [ Large A ]
-                // [ Large B ] [ Large C ]
-                // [ Large D ]
-                val tiles =
-                    listOf(
-                        TileSpec.create("smallA"),
-                        TileSpec.create("largeA"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("largeC"),
-                        TileSpec.create("largeD"),
-                    )
-                // Expected grid
-                // [ Large A ] [ Large B ]
-                // [ Large C ] [ Large D ]
-                // [ sa ]
-                val expectedTiles =
-                    listOf(
-                        TileSpec.create("largeA"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("largeC"),
-                        TileSpec.create("largeD"),
-                        TileSpec.create("smallA"),
-                    )
-
-                val newTiles = underTest.reconcileTiles(tiles)
-
-                assertThat(newTiles).isEqualTo(expectedTiles)
-            }
-        }
-
-    @Test
-    fun invalidTiles_multipleCorrections() =
-        with(kosmos) {
-            testScope.runTest {
-                // Original grid
-                // [ sa ] [ Large A ]
-                // [ Large B ] [ sb ] [ sc ]
-                // [ sd ] [ se ] [ Large C ]
-                val tiles =
-                    listOf(
-                        TileSpec.create("smallA"),
-                        TileSpec.create("largeA"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("smallB"),
-                        TileSpec.create("smallC"),
-                        TileSpec.create("smallD"),
-                        TileSpec.create("smallE"),
-                        TileSpec.create("largeC"),
-                    )
-                // Expected grid
-                // [ sa ] [ Large A ] [ sb ]
-                // [ Large B ] [ sc ] [ sd ]
-                // [ se ] [ Large C ]
-                val expectedTiles =
-                    listOf(
-                        TileSpec.create("smallA"),
-                        TileSpec.create("largeA"),
-                        TileSpec.create("smallB"),
-                        TileSpec.create("largeB"),
-                        TileSpec.create("smallC"),
-                        TileSpec.create("smallD"),
-                        TileSpec.create("smallE"),
-                        TileSpec.create("largeC"),
-                    )
-
-                val newTiles = underTest.reconcileTiles(tiles)
-
-                assertThat(newTiles).isEqualTo(expectedTiles)
-            }
-        }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
index 53384af..9e90090 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
 import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
 import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
@@ -44,12 +45,7 @@
         }
 
     private val underTest =
-        with(kosmos) {
-            InfiniteGridLayout(
-                iconTilesViewModel,
-                fixedColumnsSizeViewModel,
-            )
-        }
+        with(kosmos) { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) }
 
     @Test
     fun correctPagination_underOnePage_sameOrder() =
@@ -65,7 +61,7 @@
                         smallTile(),
                         largeTile(),
                         largeTile(),
-                        smallTile()
+                        smallTile(),
                     )
 
                 val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 763a1a9..3850891 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -27,6 +27,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.OverlayKey
 import com.android.compose.animation.scene.SceneKey
 import com.android.internal.logging.uiEventLoggerFake
 import com.android.internal.policy.IKeyguardDismissCallback
@@ -88,9 +89,11 @@
 import com.android.systemui.scene.data.repository.Transition
 import com.android.systemui.scene.domain.interactor.sceneBackInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.shared.system.QuickStepContract
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
@@ -161,6 +164,7 @@
     }
 
     @Test
+    @DisableFlags(DualShade.FLAG_NAME)
     fun hydrateVisibility() =
         testScope.runTest {
             val currentDesiredSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -221,6 +225,87 @@
         }
 
     @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun hydrateVisibility_dualShade() =
+        testScope.runTest {
+            val currentDesiredSceneKey by collectLastValue(sceneInteractor.currentScene)
+            val currentDesiredOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            val isVisible by collectLastValue(sceneInteractor.isVisible)
+            val transitionStateFlow =
+                prepareState(
+                    authenticationMethod = AuthenticationMethodModel.Pin,
+                    isDeviceUnlocked = true,
+                    initialSceneKey = Scenes.Gone,
+                )
+            assertThat(currentDesiredSceneKey).isEqualTo(Scenes.Gone)
+            assertThat(currentDesiredOverlays).isEmpty()
+            assertThat(isVisible).isTrue()
+
+            underTest.start()
+            assertThat(isVisible).isFalse()
+
+            // Expand the notifications shade.
+            fakeSceneDataSource.pause()
+            sceneInteractor.showOverlay(Overlays.NotificationsShade, "reason")
+            transitionStateFlow.value =
+                ObservableTransitionState.Transition.ShowOrHideOverlay(
+                    overlay = Overlays.NotificationsShade,
+                    fromContent = Scenes.Gone,
+                    toContent = Overlays.NotificationsShade,
+                    currentScene = Scenes.Gone,
+                    currentOverlays = flowOf(emptySet()),
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            assertThat(isVisible).isTrue()
+            fakeSceneDataSource.unpause(expectedScene = Scenes.Gone)
+            transitionStateFlow.value =
+                ObservableTransitionState.Idle(
+                    currentScene = Scenes.Gone,
+                    currentOverlays = setOf(Overlays.NotificationsShade),
+                )
+            assertThat(isVisible).isTrue()
+
+            // Collapse the notifications shade.
+            fakeSceneDataSource.pause()
+            sceneInteractor.hideOverlay(Overlays.NotificationsShade, "reason")
+            transitionStateFlow.value =
+                ObservableTransitionState.Transition.ShowOrHideOverlay(
+                    overlay = Overlays.NotificationsShade,
+                    fromContent = Overlays.NotificationsShade,
+                    toContent = Scenes.Gone,
+                    currentScene = Scenes.Gone,
+                    currentOverlays = flowOf(setOf(Overlays.NotificationsShade)),
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            assertThat(isVisible).isTrue()
+            fakeSceneDataSource.unpause(expectedScene = Scenes.Gone)
+            transitionStateFlow.value =
+                ObservableTransitionState.Idle(
+                    currentScene = Scenes.Gone,
+                    currentOverlays = emptySet(),
+                )
+            assertThat(isVisible).isFalse()
+
+            kosmos.headsUpNotificationRepository.setNotifications(
+                buildNotificationRows(isPinned = true)
+            )
+            assertThat(isVisible).isTrue()
+
+            kosmos.headsUpNotificationRepository.setNotifications(
+                buildNotificationRows(isPinned = false)
+            )
+            assertThat(isVisible).isFalse()
+        }
+
+    @Test
     fun hydrateVisibility_basedOnDeviceProvisioning() =
         testScope.runTest {
             val isVisible by collectLastValue(sceneInteractor.isVisible)
@@ -1621,6 +1706,7 @@
         }
 
     @Test
+    @DisableFlags(DualShade.FLAG_NAME)
     fun hydrateInteractionState_whileLocked() =
         testScope.runTest {
             val transitionStateFlow = prepareState(initialSceneKey = Scenes.Lockscreen)
@@ -1707,6 +1793,7 @@
         }
 
     @Test
+    @DisableFlags(DualShade.FLAG_NAME)
     fun hydrateInteractionState_whileUnlocked() =
         testScope.runTest {
             val transitionStateFlow =
@@ -1795,6 +1882,186 @@
         }
 
     @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun hydrateInteractionState_dualShade_whileLocked() =
+        testScope.runTest {
+            val currentDesiredOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            val transitionStateFlow = prepareState(initialSceneKey = Scenes.Lockscreen)
+            underTest.start()
+            runCurrent()
+            verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true)
+            assertThat(currentDesiredOverlays).isEmpty()
+
+            clearInvocations(centralSurfaces)
+            emulateSceneTransition(
+                transitionStateFlow = transitionStateFlow,
+                toScene = Scenes.Bouncer,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces)
+                        .setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false)
+                },
+            )
+
+            clearInvocations(centralSurfaces)
+            emulateSceneTransition(
+                transitionStateFlow = transitionStateFlow,
+                toScene = Scenes.Lockscreen,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true)
+                },
+            )
+
+            clearInvocations(centralSurfaces)
+            emulateOverlayTransition(
+                transitionStateFlow = transitionStateFlow,
+                toOverlay = Overlays.NotificationsShade,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces)
+                        .setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false)
+                },
+            )
+
+            clearInvocations(centralSurfaces)
+            emulateSceneTransition(
+                transitionStateFlow = transitionStateFlow,
+                toScene = Scenes.Lockscreen,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true)
+                },
+            )
+
+            clearInvocations(centralSurfaces)
+            emulateOverlayTransition(
+                transitionStateFlow = transitionStateFlow,
+                toOverlay = Overlays.QuickSettingsShade,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+            )
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun hydrateInteractionState_dualShade_whileUnlocked() =
+        testScope.runTest {
+            val currentDesiredOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            val transitionStateFlow =
+                prepareState(
+                    authenticationMethod = AuthenticationMethodModel.Pin,
+                    isDeviceUnlocked = true,
+                    initialSceneKey = Scenes.Gone,
+                )
+            underTest.start()
+            verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+            assertThat(currentDesiredOverlays).isEmpty()
+
+            clearInvocations(centralSurfaces)
+            emulateSceneTransition(
+                transitionStateFlow = transitionStateFlow,
+                toScene = Scenes.Bouncer,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+            )
+
+            clearInvocations(centralSurfaces)
+            emulateSceneTransition(
+                transitionStateFlow = transitionStateFlow,
+                toScene = Scenes.Lockscreen,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+            )
+
+            clearInvocations(centralSurfaces)
+            emulateSceneTransition(
+                transitionStateFlow = transitionStateFlow,
+                toScene = Scenes.Shade,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+            )
+
+            clearInvocations(centralSurfaces)
+            emulateSceneTransition(
+                transitionStateFlow = transitionStateFlow,
+                toScene = Scenes.Lockscreen,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+            )
+
+            clearInvocations(centralSurfaces)
+            emulateSceneTransition(
+                transitionStateFlow = transitionStateFlow,
+                toScene = Scenes.QuickSettings,
+                verifyBeforeTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyDuringTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+                verifyAfterTransition = {
+                    verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
+                },
+            )
+        }
+
+    @Test
     fun respondToFalsingDetections() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
@@ -2131,19 +2398,40 @@
         verifyAfterTransition: (() -> Unit)? = null,
     ) {
         val fromScene = sceneInteractor.currentScene.value
+        val fromOverlays = sceneInteractor.currentOverlays.value
         sceneInteractor.changeScene(toScene, "reason")
         runCurrent()
         verifyBeforeTransition?.invoke()
 
         transitionStateFlow.value =
-            ObservableTransitionState.Transition(
-                fromScene = fromScene,
-                toScene = toScene,
-                currentScene = flowOf(fromScene),
-                progress = flowOf(0.5f),
-                isInitiatedByUserInput = true,
-                isUserInputOngoing = flowOf(true),
-            )
+            if (fromOverlays.isEmpty()) {
+                // Regular scene-to-scene transition.
+                ObservableTransitionState.Transition.ChangeScene(
+                    fromScene = fromScene,
+                    toScene = toScene,
+                    currentScene = flowOf(fromScene),
+                    currentOverlays = fromOverlays,
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            } else {
+                // An overlay is present; hide it.
+                ObservableTransitionState.Transition.ShowOrHideOverlay(
+                    overlay = fromOverlays.first(),
+                    fromContent = fromOverlays.first(),
+                    toContent = toScene,
+                    currentScene = fromScene,
+                    currentOverlays = sceneInteractor.currentOverlays,
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            }
         runCurrent()
         verifyDuringTransition?.invoke()
 
@@ -2152,6 +2440,60 @@
         verifyAfterTransition?.invoke()
     }
 
+    private fun TestScope.emulateOverlayTransition(
+        transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
+        toOverlay: OverlayKey,
+        verifyBeforeTransition: (() -> Unit)? = null,
+        verifyDuringTransition: (() -> Unit)? = null,
+        verifyAfterTransition: (() -> Unit)? = null,
+    ) {
+        val fromScene = sceneInteractor.currentScene.value
+        val fromOverlays = sceneInteractor.currentOverlays.value
+        sceneInteractor.showOverlay(toOverlay, "reason")
+        runCurrent()
+        verifyBeforeTransition?.invoke()
+
+        transitionStateFlow.value =
+            if (fromOverlays.isEmpty()) {
+                // Show a new overlay.
+                ObservableTransitionState.Transition.ShowOrHideOverlay(
+                    overlay = toOverlay,
+                    fromContent = fromScene,
+                    toContent = toOverlay,
+                    currentScene = fromScene,
+                    currentOverlays = sceneInteractor.currentOverlays,
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            } else {
+                // Overlay-to-overlay transition.
+                ObservableTransitionState.Transition.ReplaceOverlay(
+                    fromOverlay = fromOverlays.first(),
+                    toOverlay = toOverlay,
+                    currentScene = fromScene,
+                    currentOverlays = sceneInteractor.currentOverlays,
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            }
+        runCurrent()
+        verifyDuringTransition?.invoke()
+
+        transitionStateFlow.value =
+            ObservableTransitionState.Idle(
+                currentScene = fromScene,
+                currentOverlays = setOf(toOverlay),
+            )
+        runCurrent()
+        verifyAfterTransition?.invoke()
+    }
+
     private fun TestScope.prepareState(
         isDeviceUnlocked: Boolean = false,
         isBypassEnabled: Boolean = false,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
index 4d69f0d..f86337e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
@@ -19,8 +19,8 @@
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_COMPOSE_LOCKSCREEN
 import com.android.systemui.Flags.FLAG_EXAMPLE_FLAG
+import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
 import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.andSceneContainer
@@ -66,7 +66,7 @@
 
     @Test
     fun oneDependencyAndSceneContainer() {
-        val dependentFlag = FLAG_COMPOSE_LOCKSCREEN
+        val dependentFlag = FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
         val result = FlagsParameterization.allCombinationsOf(dependentFlag).andSceneContainer()
         Truth.assertThat(result).hasSize(3)
         Truth.assertThat(result[0].mOverrides[dependentFlag]).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index fb32855..0f6dc07 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -17,7 +17,9 @@
 package com.android.systemui.statusbar.policy.domain.interactor
 
 import android.app.AutomaticZenRule
+import android.app.Flags
 import android.app.NotificationManager.Policy
+import android.platform.test.annotations.EnableFlags
 import android.provider.Settings
 import android.provider.Settings.Secure.ZEN_DURATION
 import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
@@ -32,6 +34,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
 import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -50,10 +53,31 @@
     private val testScope = kosmos.testScope
     private val zenModeRepository = kosmos.fakeZenModeRepository
     private val settingsRepository = kosmos.secureSettingsRepository
+    private val deviceProvisioningRepository = kosmos.fakeDeviceProvisioningRepository
 
     private val underTest = kosmos.zenModeInteractor
 
     @Test
+    fun isZenAvailable_off() =
+        testScope.runTest {
+            val isZenAvailable by collectLastValue(underTest.isZenAvailable)
+            deviceProvisioningRepository.setDeviceProvisioned(false)
+            runCurrent()
+
+            assertThat(isZenAvailable).isFalse()
+        }
+
+    @Test
+    fun isZenAvailable_on() =
+        testScope.runTest {
+            val isZenAvailable by collectLastValue(underTest.isZenAvailable)
+            deviceProvisioningRepository.setDeviceProvisioned(true)
+            runCurrent()
+
+            assertThat(isZenAvailable).isTrue()
+        }
+
+    @Test
     fun isZenModeEnabled_off() =
         testScope.runTest {
             val enabled by collectLastValue(underTest.isZenModeEnabled)
@@ -337,4 +361,22 @@
             runCurrent()
             assertThat(mainActiveMode).isNull()
         }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    fun dndMode_flows() =
+        testScope.runTest {
+            val dndMode by collectLastValue(underTest.dndMode)
+
+            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+            runCurrent()
+
+            assertThat(dndMode!!.isActive).isFalse()
+
+            zenModeRepository.removeMode(TestModeBuilder.MANUAL_DND_INACTIVE.id)
+            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+            runCurrent()
+
+            assertThat(dndMode!!.isActive).isTrue()
+        }
 }
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f9c2aef..ba3822b 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3114,6 +3114,10 @@
     <string name="media_output_group_title_speakers_and_displays">Speakers &amp; Displays</string>
     <!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] -->
     <string name="media_output_group_title_suggested_device">Suggested Devices</string>
+    <!-- Title for input device group. [CHAR LIMIT=NONE] -->
+    <string name="media_input_group_title">Input</string>
+    <!-- Title for output device group. [CHAR LIMIT=NONE] -->
+    <string name="media_output_group_title">Output</string>
     <!-- Summary for end session dialog. [CHAR LIMIT=NONE] -->
     <string name="media_output_end_session_dialog_summary">Stop your shared session to move media to another device</string>
     <!-- Button text for stopping session [CHAR LIMIT=60] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 7efe2dd..ffbc85c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -968,6 +968,15 @@
             constraintSet.constrainWidth(mViewFlipper.getId(), MATCH_CONSTRAINT);
             constraintSet.applyTo(mView);
         }
+
+        @Override
+        public void onDestroy() {
+            if (mView == null) return;
+            ConstraintSet constraintSet = new ConstraintSet();
+            constraintSet.clone(mView);
+            constraintSet.clear(mViewFlipper.getId());
+            constraintSet.applyTo(mView);
+        }
     }
 
     /**
@@ -1043,12 +1052,20 @@
         @Override
         public void onDensityOrFontScaleChanged() {
             mView.removeView(mUserSwitcherViewGroup);
+            mView.removeView(mUserSwitcher);
             inflateUserSwitcher();
         }
 
         @Override
         public void onDestroy() {
-            mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback);
+            ConstraintSet constraintSet = new ConstraintSet();
+            constraintSet.clone(mView);
+            constraintSet.clear(mUserSwitcherViewGroup.getId());
+            constraintSet.clear(mViewFlipper.getId());
+            constraintSet.applyTo(mView);
+
+            mView.removeView(mUserSwitcherViewGroup);
+            mView.removeView(mUserSwitcher);
         }
 
         private Drawable findLargeUserIcon(int userId) {
@@ -1344,5 +1361,13 @@
             constraintSet.constrainPercentWidth(mViewFlipper.getId(), 0.5f);
             constraintSet.applyTo(mView);
         }
+
+        @Override
+        public void onDestroy() {
+            ConstraintSet constraintSet = new ConstraintSet();
+            constraintSet.clone(mView);
+            constraintSet.clear(mViewFlipper.getId());
+            constraintSet.applyTo(mView);
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index c7a47b1..1ada56d 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -30,6 +30,7 @@
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.os.Build;
+import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.Log;
 
@@ -37,6 +38,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.user.utils.UserScopedService;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -67,13 +69,13 @@
     public ClipboardListener(Context context,
             Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
             ClipboardToast clipboardToast,
-            ClipboardManager clipboardManager,
+            UserScopedService<ClipboardManager> clipboardManager,
             KeyguardManager keyguardManager,
             UiEventLogger uiEventLogger) {
         mContext = context;
         mOverlayProvider = clipboardOverlayControllerProvider;
         mClipboardToast = clipboardToast;
-        mClipboardManager = clipboardManager;
+        mClipboardManager = clipboardManager.forUser(UserHandle.CURRENT);
         mKeyguardManager = keyguardManager;
         mUiEventLogger = uiEventLogger;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 8818c3a..8f913ff 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -733,9 +733,8 @@
     }
 
     @Provides
-    @Singleton
-    static ClipboardManager provideClipboardManager(Context context) {
-        return context.getSystemService(ClipboardManager.class);
+    static UserScopedService<ClipboardManager> provideClipboardManager(Context context) {
+        return new UserScopedServiceImpl<>(context, ClipboardManager.class);
     }
 
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
index e17e530..5259c5d 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
@@ -26,7 +26,9 @@
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus
 import com.android.systemui.flags.SystemPropertiesHelper
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.TrustInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
@@ -36,6 +38,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
@@ -57,6 +60,7 @@
     private val powerInteractor: PowerInteractor,
     private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor,
     private val systemPropertiesHelper: SystemPropertiesHelper,
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) {
 
     private val deviceUnlockSource =
@@ -74,7 +78,7 @@
             trustInteractor.isTrusted.filter { it }.map { DeviceUnlockSource.TrustAgent },
             authenticationInteractor.onAuthenticationResult
                 .filter { it }
-                .map { DeviceUnlockSource.BouncerInput }
+                .map { DeviceUnlockSource.BouncerInput },
         )
 
     private val faceEnrolledAndEnabled = biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled
@@ -170,10 +174,20 @@
                     combine(
                             powerInteractor.isAsleep,
                             isInLockdown,
-                            ::Pair,
+                            keyguardTransitionInteractor
+                                .transitionValue(KeyguardState.AOD)
+                                .map { it == 1f }
+                                .distinctUntilChanged(),
+                            ::Triple,
                         )
-                        .flatMapLatestConflated { (isAsleep, isInLockdown) ->
-                            if (isAsleep || isInLockdown) {
+                        .flatMapLatestConflated { (isAsleep, isInLockdown, isAod) ->
+                            val isForceLocked =
+                                when {
+                                    isAsleep && !isAod -> true
+                                    isInLockdown -> true
+                                    else -> false
+                                }
+                            if (isForceLocked) {
                                 flowOf(DeviceUnlockStatus(false, null))
                             } else {
                                 deviceUnlockSource.map { DeviceUnlockStatus(true, it) }
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 1daaa11..500c5b3 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,6 +16,7 @@
 
 package com.android.systemui.education.data.model
 
+import com.android.systemui.contextualeducation.GestureType
 import java.time.Instant
 
 /**
@@ -23,6 +24,7 @@
  * gesture stores its own model separately.
  */
 data class GestureEduModel(
+    val gestureType: GestureType,
     val signalCount: Int = 0,
     val educationShownCount: Int = 0,
     val lastShortcutTriggeredTime: Instant? = null,
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 01f838f..2978595 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
@@ -17,6 +17,8 @@
 package com.android.systemui.education.data.repository
 
 import android.content.Context
+import android.hardware.input.InputManager
+import android.hardware.input.KeyGestureEvent
 import androidx.datastore.core.DataStore
 import androidx.datastore.preferences.core.MutablePreferences
 import androidx.datastore.preferences.core.PreferenceDataStoreFactory
@@ -25,23 +27,31 @@
 import androidx.datastore.preferences.core.intPreferencesKey
 import androidx.datastore.preferences.core.longPreferencesKey
 import androidx.datastore.preferences.preferencesDataStoreFile
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.ALL_APPS
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope
 import com.android.systemui.education.data.model.EduDeviceConnectionTime
 import com.android.systemui.education.data.model.GestureEduModel
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import java.time.Instant
+import java.util.concurrent.Executor
 import javax.inject.Inject
 import javax.inject.Provider
 import kotlin.properties.Delegates.notNull
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 
 /**
@@ -64,6 +74,8 @@
     suspend fun updateEduDeviceConnectionTime(
         transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime
     )
+
+    val keyboardShortcutTriggered: Flow<GestureType>
 }
 
 /**
@@ -75,9 +87,13 @@
 @Inject
 constructor(
     @Application private val applicationContext: Context,
-    @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>
+    @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>,
+    private val inputManager: InputManager,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) : ContextualEducationRepository {
     companion object {
+        const val TAG = "UserContextualEducationRepository"
+
         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"
@@ -98,6 +114,30 @@
     @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
     private val prefData: Flow<Preferences> = datastore.filterNotNull().flatMapLatest { it.data }
 
+    override val keyboardShortcutTriggered: Flow<GestureType> =
+        conflatedCallbackFlow {
+                val listener =
+                    InputManager.KeyGestureEventListener { event ->
+                        // Only store keyboard shortcut time for gestures providing keyboard
+                        // education
+                        val shortcutType =
+                            when (event.keyGestureType) {
+                                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS,
+                                KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS
+
+                                else -> null
+                            }
+
+                        if (shortcutType != null) {
+                            trySendWithFailureLogging(shortcutType, TAG)
+                        }
+                    }
+
+                inputManager.registerKeyGestureEventListener(Executor(Runnable::run), listener)
+                awaitClose { inputManager.unregisterKeyGestureEventListener(listener) }
+            }
+            .flowOn(backgroundDispatcher)
+
     override fun setUser(userId: Int) {
         dataStoreScope?.cancel()
         val newDsScope = dataStoreScopeProvider.get()
@@ -136,7 +176,8 @@
                 preferences[getLastEducationTimeKey(gestureType)]?.let {
                     Instant.ofEpochSecond(it)
                 },
-            userId = userId
+            userId = userId,
+            gestureType = gestureType,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
index 10be26e..c88b364 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
@@ -18,7 +18,10 @@
 
 import com.android.systemui.CoreStartable
 import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.ALL_APPS
 import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.contextualeducation.GestureType.HOME
+import com.android.systemui.contextualeducation.GestureType.OVERVIEW
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
@@ -53,6 +56,13 @@
 ) : CoreStartable {
 
     val backGestureModelFlow = readEduModelsOnSignalCountChanged(BACK)
+    val homeGestureModelFlow = readEduModelsOnSignalCountChanged(HOME)
+    val overviewGestureModelFlow = readEduModelsOnSignalCountChanged(OVERVIEW)
+    val allAppsGestureModelFlow = readEduModelsOnSignalCountChanged(ALL_APPS)
+    val eduDeviceConnectionTimeFlow =
+        repository.readEduDeviceConnectionTime().distinctUntilChanged()
+
+    val keyboardShortcutTriggered = repository.keyboardShortcutTriggered
 
     override fun start() {
         backgroundScope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index 43855d9..faee326 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -16,15 +16,8 @@
 
 package com.android.systemui.education.domain.interactor
 
-import android.hardware.input.InputManager
-import android.hardware.input.InputManager.KeyGestureEventListener
-import android.hardware.input.KeyGestureEvent
 import android.os.SystemProperties
 import com.android.systemui.CoreStartable
-import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.contextualeducation.GestureType
-import com.android.systemui.contextualeducation.GestureType.ALL_APPS
-import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
@@ -32,19 +25,19 @@
 import com.android.systemui.education.shared.model.EducationInfo
 import com.android.systemui.education.shared.model.EducationUiType
 import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import java.time.Clock
-import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.days
 import kotlin.time.DurationUnit
 import kotlin.time.toDuration
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.launch
 
 /** Allow listening to new contextual education triggered */
@@ -55,7 +48,6 @@
     @Background private val backgroundScope: CoroutineScope,
     private val contextualEducationInteractor: ContextualEducationInteractor,
     private val userInputDeviceRepository: UserInputDeviceRepository,
-    private val inputManager: InputManager,
     @EduClock private val clock: Clock,
 ) : CoreStartable {
 
@@ -82,34 +74,32 @@
     private val _educationTriggered = MutableStateFlow<EducationInfo?>(null)
     val educationTriggered = _educationTriggered.asStateFlow()
 
-    private val keyboardShortcutTriggered: Flow<GestureType> = conflatedCallbackFlow {
-        val listener = KeyGestureEventListener { event ->
-            // Only store keyboard shortcut time for gestures providing keyboard education
-            val shortcutType =
-                when (event.keyGestureType) {
-                    KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS
-                    else -> null
-                }
-
-            if (shortcutType != null) {
-                trySendWithFailureLogging(shortcutType, TAG)
-            }
-        }
-
-        inputManager.registerKeyGestureEventListener(Executor(Runnable::run), listener)
-        awaitClose { inputManager.unregisterKeyGestureEventListener(listener) }
-    }
-
+    @OptIn(ExperimentalCoroutinesApi::class)
     override fun start() {
         backgroundScope.launch {
-            contextualEducationInteractor.backGestureModelFlow.collect {
-                if (isUsageSessionExpired(it)) {
-                    contextualEducationInteractor.startNewUsageSession(BACK)
-                } else if (isEducationNeeded(it)) {
-                    _educationTriggered.value = EducationInfo(BACK, getEduType(it), it.userId)
-                    contextualEducationInteractor.updateOnEduTriggered(BACK)
+            contextualEducationInteractor.eduDeviceConnectionTimeFlow
+                .flatMapLatest {
+                    val gestureFlows = mutableListOf<Flow<GestureEduModel>>()
+                    if (it.touchpadFirstConnectionTime != null) {
+                        gestureFlows.add(contextualEducationInteractor.backGestureModelFlow)
+                        gestureFlows.add(contextualEducationInteractor.homeGestureModelFlow)
+                        gestureFlows.add(contextualEducationInteractor.overviewGestureModelFlow)
+                    }
+
+                    if (it.keyboardFirstConnectionTime != null) {
+                        gestureFlows.add(contextualEducationInteractor.allAppsGestureModelFlow)
+                    }
+                    gestureFlows.merge()
                 }
-            }
+                .collect {
+                    if (isUsageSessionExpired(it)) {
+                        contextualEducationInteractor.startNewUsageSession(it.gestureType)
+                    } else if (isEducationNeeded(it)) {
+                        _educationTriggered.value =
+                            EducationInfo(it.gestureType, getEduType(it), it.userId)
+                        contextualEducationInteractor.updateOnEduTriggered(it.gestureType)
+                    }
+                }
         }
 
         backgroundScope.launch {
@@ -139,7 +129,7 @@
         }
 
         backgroundScope.launch {
-            keyboardShortcutTriggered.collect {
+            contextualEducationInteractor.keyboardShortcutTriggered.collect {
                 contextualEducationInteractor.updateShortcutTriggerTime(it)
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 6318dc0..0b775ab 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -31,9 +31,7 @@
 import com.android.systemui.Flags.statusBarScreenSharingChips
 import com.android.systemui.Flags.statusBarUseReposForCallChip
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
-import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
@@ -62,10 +60,6 @@
         // SceneContainer dependencies
         SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
 
-        // ComposeLockscreen dependencies
-        ComposeLockscreen.token dependsOn KeyguardBottomAreaRefactor.token
-        ComposeLockscreen.token dependsOn MigrateClocksToBlueprint.token
-
         // CommunalHub dependencies
         communalHub dependsOn MigrateClocksToBlueprint.token
 
@@ -99,7 +93,7 @@
         get() =
             FlagToken(
                 FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON,
-                statusBarCallChipNotificationIcon()
+                statusBarCallChipNotificationIcon(),
             )
 
     private inline val statusBarScreenSharingChipsToken
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
index e50c05c..8966209 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.QSLog
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -50,6 +51,7 @@
 constructor(
     private val vibratorHelper: VibratorHelper?,
     private val keyguardStateController: KeyguardStateController,
+    private val falsingManager: FalsingManager,
     @QSLog private val logBuffer: LogBuffer,
 ) {
 
@@ -72,7 +74,7 @@
     private val durations =
         vibratorHelper?.getPrimitiveDurations(
             VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
-            VibrationEffect.Composition.PRIMITIVE_SPIN
+            VibrationEffect.Composition.PRIMITIVE_SPIN,
         )
 
     private var longPressHint: VibrationEffect? = null
@@ -152,15 +154,27 @@
         logEvent(qsTile?.tileSpec, state, "animation completed")
         when (state) {
             State.RUNNING_FORWARD -> {
-                vibrate(snapEffect)
-                if (keyguardStateController.isUnlocked) {
-                    setState(State.LONG_CLICKED)
-                } else {
+                val wasFalseLongTap = falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
+                if (wasFalseLongTap) {
                     callback?.onResetProperties()
                     setState(State.IDLE)
+                    logEvent(qsTile?.tileSpec, state, "false long click. No action triggered")
+                } else if (keyguardStateController.isUnlocked) {
+                    vibrate(snapEffect)
+                    setState(State.LONG_CLICKED)
+                    qsTile?.longClick(expandable)
+                    logEvent(qsTile?.tileSpec, state, "long click action triggered")
+                } else {
+                    vibrate(snapEffect)
+                    callback?.onResetProperties()
+                    setState(State.IDLE)
+                    qsTile?.longClick(expandable)
+                    logEvent(
+                        qsTile?.tileSpec,
+                        state,
+                        "properties reset and long click action triggered",
+                    )
                 }
-                logEvent(qsTile?.tileSpec, state, "long click action triggered")
-                qsTile?.longClick(expandable)
             }
             State.RUNNING_BACKWARDS_FROM_UP -> {
                 callback?.onEffectFinishedReversing()
@@ -236,7 +250,7 @@
             LongPressHapticBuilder.createLongPressHint(
                 durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION,
                 durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION,
-                effectDuration
+                effectDuration,
             )
         setState(State.IDLE)
         return true
@@ -265,7 +279,7 @@
                 }
 
                 override fun dialogTransitionController(
-                    cuj: DialogCuj?,
+                    cuj: DialogCuj?
                 ): DialogTransitionAnimator.Controller? =
                     DialogTransitionAnimator.Controller.fromView(view, cuj)
             }
@@ -298,7 +312,7 @@
                 str2 = event
                 str3 = state.name
             },
-            { "[long-press effect on $str1 tile] $str2 on state: $str3" }
+            { "[long-press effect on $str1 tile] $str2 on state: $str3" },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index df0f10a..416eaba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -24,12 +24,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.ComposeView
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.END
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import androidx.constraintlayout.widget.ConstraintSet.START
-import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayout
@@ -47,7 +41,6 @@
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
 import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
@@ -128,7 +121,7 @@
                     keyguardStatusViewComponentFactory.build(
                         LayoutInflater.from(context).inflate(R.layout.keyguard_status_view, null)
                             as KeyguardStatusView,
-                        context.display
+                        context.display,
                     )
                 val controller = statusViewComponent.keyguardStatusViewController
                 controller.init()
@@ -143,29 +136,12 @@
         initializeViews()
 
         if (!SceneContainerFlag.isEnabled) {
-            if (ComposeLockscreen.isEnabled) {
-                val composeView =
-                    createLockscreen(
-                        context = context,
-                        viewModelFactory = lockscreenContentViewModelFactory,
-                        blueprints = lockscreenSceneBlueprintsLazy.get(),
-                    )
-                composeView.id = View.generateViewId()
-                val cs = ConstraintSet()
-                cs.clone(keyguardRootView)
-                cs.connect(composeView.id, START, PARENT_ID, START)
-                cs.connect(composeView.id, END, PARENT_ID, END)
-                cs.connect(composeView.id, TOP, PARENT_ID, TOP)
-                cs.connect(composeView.id, BOTTOM, PARENT_ID, BOTTOM)
-                keyguardRootView.addView(composeView)
-            } else {
-                KeyguardBlueprintViewBinder.bind(
-                    keyguardRootView,
-                    keyguardBlueprintViewModel,
-                    keyguardClockViewModel,
-                    smartspaceViewModel,
-                )
-            }
+            KeyguardBlueprintViewBinder.bind(
+                keyguardRootView,
+                keyguardBlueprintViewModel,
+                keyguardClockViewModel,
+                smartspaceViewModel,
+            )
         }
         if (deviceEntryUnlockTrackerViewBinder.isPresent) {
             deviceEntryUnlockTrackerViewBinder.get().bind(keyguardRootView)
@@ -247,7 +223,7 @@
                             LockscreenContent(
                                 viewModelFactory = viewModelFactory,
                                 blueprints = sceneBlueprints,
-                                clockInteractor = clockInteractor
+                                clockInteractor = clockInteractor,
                             )
                         ) {
                             Content(modifier = Modifier.fillMaxSize())
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
index 406b9f6..be87334 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
@@ -25,7 +25,9 @@
 import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
 import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
 import android.service.notification.ZenModeConfig
+import android.util.Log
 import com.android.settingslib.notification.modes.EnableZenModeDialog
+import com.android.settingslib.notification.modes.ZenMode
 import com.android.settingslib.notification.modes.ZenModeDialogMetricsLogger
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -35,30 +37,38 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.modes.shared.ModesUi
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.ZenModeController
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
 import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 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.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
 
 @SysUISingleton
 class DoNotDisturbQuickAffordanceConfig
 constructor(
     private val context: Context,
     private val controller: ZenModeController,
+    private val interactor: ZenModeInteractor,
     private val secureSettings: SecureSettings,
     private val userTracker: UserTracker,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Background private val backgroundScope: CoroutineScope,
     private val testConditionId: Uri?,
     testDialog: EnableZenModeDialog?,
 ) : KeyguardQuickAffordanceConfig {
@@ -67,15 +77,45 @@
     constructor(
         context: Context,
         controller: ZenModeController,
+        interactor: ZenModeInteractor,
         secureSettings: SecureSettings,
         userTracker: UserTracker,
         @Background backgroundDispatcher: CoroutineDispatcher,
-    ) : this(context, controller, secureSettings, userTracker, backgroundDispatcher, null, null)
+        @Background backgroundScope: CoroutineScope,
+    ) : this(
+        context,
+        controller,
+        interactor,
+        secureSettings,
+        userTracker,
+        backgroundDispatcher,
+        backgroundScope,
+        null,
+        null,
+    )
 
-    private var dndMode: Int = 0
-    private var isAvailable = false
+    private var zenMode: Int = 0
+    private var oldIsAvailable = false
     private var settingsValue: Int = 0
 
+    private val dndMode: StateFlow<ZenMode?> by lazy {
+        ModesUi.assertInNewMode()
+        interactor.dndMode.stateIn(
+            scope = backgroundScope,
+            started = SharingStarted.Eagerly,
+            initialValue = null,
+        )
+    }
+
+    private val isAvailable: StateFlow<Boolean> by lazy {
+        ModesUi.assertInNewMode()
+        interactor.isZenAvailable.stateIn(
+            scope = backgroundScope,
+            started = SharingStarted.Eagerly,
+            initialValue = false,
+        )
+    }
+
     private val conditionUri: Uri
         get() =
             testConditionId
@@ -104,42 +144,68 @@
     override val pickerIconResourceId: Int = R.drawable.ic_do_not_disturb
 
     override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
-        combine(
-            conflatedCallbackFlow {
-                val callback =
-                    object : ZenModeController.Callback {
-                        override fun onZenChanged(zen: Int) {
-                            dndMode = zen
-                            trySendWithFailureLogging(updateState(), TAG)
+        if (ModesUi.isEnabled) {
+            combine(isAvailable, dndMode) { isAvailable, dndMode ->
+                if (!isAvailable) {
+                    KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+                } else if (dndMode?.isActive == true) {
+                    KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                        Icon.Resource(
+                            R.drawable.qs_dnd_icon_on,
+                            ContentDescription.Resource(R.string.dnd_is_on),
+                        ),
+                        ActivationState.Active,
+                    )
+                } else {
+                    KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                        Icon.Resource(
+                            R.drawable.qs_dnd_icon_off,
+                            ContentDescription.Resource(R.string.dnd_is_off),
+                        ),
+                        ActivationState.Inactive,
+                    )
+                }
+            }
+        } else {
+            combine(
+                conflatedCallbackFlow {
+                    val callback =
+                        object : ZenModeController.Callback {
+                            override fun onZenChanged(zen: Int) {
+                                zenMode = zen
+                                trySendWithFailureLogging(updateState(), TAG)
+                            }
+
+                            override fun onZenAvailableChanged(available: Boolean) {
+                                oldIsAvailable = available
+                                trySendWithFailureLogging(updateState(), TAG)
+                            }
                         }
 
-                        override fun onZenAvailableChanged(available: Boolean) {
-                            isAvailable = available
-                            trySendWithFailureLogging(updateState(), TAG)
-                        }
-                    }
+                    zenMode = controller.zen
+                    oldIsAvailable = controller.isZenAvailable
+                    trySendWithFailureLogging(updateState(), TAG)
 
-                dndMode = controller.zen
-                isAvailable = controller.isZenAvailable
-                trySendWithFailureLogging(updateState(), TAG)
+                    controller.addCallback(callback)
 
-                controller.addCallback(callback)
-
-                awaitClose { controller.removeCallback(callback) }
-            },
-            secureSettings
-                .observerFlow(userTracker.userId, Settings.Secure.ZEN_DURATION)
-                .onStart { emit(Unit) }
-                .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) }
-                .flowOn(backgroundDispatcher)
-                .distinctUntilChanged()
-                .onEach { settingsValue = it }
-        ) { callbackFlowValue, _ ->
-            callbackFlowValue
+                    awaitClose { controller.removeCallback(callback) }
+                },
+                secureSettings
+                    .observerFlow(userTracker.userId, Settings.Secure.ZEN_DURATION)
+                    .onStart { emit(Unit) }
+                    .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) }
+                    .flowOn(backgroundDispatcher)
+                    .distinctUntilChanged()
+                    .onEach { settingsValue = it },
+            ) { callbackFlowValue, _ ->
+                callbackFlowValue
+            }
         }
 
     override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
-        return if (controller.isZenAvailable) {
+        val isZenAvailable = if (ModesUi.isEnabled) isAvailable.value else controller.isZenAvailable
+
+        return if (isZenAvailable) {
             KeyguardQuickAffordanceConfig.PickerScreenState.Default(
                 configureIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
             )
@@ -151,32 +217,63 @@
     override fun onTriggered(
         expandable: Expandable?
     ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
-        return when {
-            !isAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
-            dndMode != ZEN_MODE_OFF -> {
-                controller.setZen(ZEN_MODE_OFF, null, TAG)
+        return if (ModesUi.isEnabled) {
+            if (!isAvailable.value) {
                 KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+            } else {
+                val dnd = dndMode.value
+                if (dnd == null) {
+                    Log.wtf(TAG, "Triggered DND but it's null!?")
+                    return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+                }
+                if (dnd.isActive) {
+                    interactor.deactivateMode(dnd)
+                    return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+                } else {
+                    if (interactor.shouldAskForZenDuration(dnd)) {
+                        // NOTE: The dialog handles turning on the mode itself.
+                        return KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog(
+                            dialog.createDialog(),
+                            expandable,
+                        )
+                    } else {
+                        interactor.activateMode(dnd)
+                        return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+                    }
+                }
             }
-            settingsValue == ZEN_DURATION_PROMPT ->
-                KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog(
-                    dialog.createDialog(),
-                    expandable
-                )
-            settingsValue == ZEN_DURATION_FOREVER -> {
-                controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG)
-                KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
-            }
-            else -> {
-                controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, conditionUri, TAG)
-                KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+        } else {
+            when {
+                !oldIsAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+                zenMode != ZEN_MODE_OFF -> {
+                    controller.setZen(ZEN_MODE_OFF, null, TAG)
+                    KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+                }
+
+                settingsValue == ZEN_DURATION_PROMPT ->
+                    KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog(
+                        dialog.createDialog(),
+                        expandable,
+                    )
+
+                settingsValue == ZEN_DURATION_FOREVER -> {
+                    controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG)
+                    KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+                }
+
+                else -> {
+                    controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, conditionUri, TAG)
+                    KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+                }
             }
         }
     }
 
     private fun updateState(): KeyguardQuickAffordanceConfig.LockScreenState {
-        return if (!isAvailable) {
+        ModesUi.assertInLegacyMode()
+        return if (!oldIsAvailable) {
             KeyguardQuickAffordanceConfig.LockScreenState.Hidden
-        } else if (dndMode == ZEN_MODE_OFF) {
+        } else if (zenMode == ZEN_MODE_OFF) {
             KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                 Icon.Resource(
                     R.drawable.qs_dnd_icon_off,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index 7afc759..6932eb5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -25,13 +25,13 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
-import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -64,7 +64,7 @@
     /** Current BlueprintId */
     val blueprintId =
         shadeInteractor.isShadeLayoutWide.map { isShadeLayoutWide ->
-            val useSplitShade = isShadeLayoutWide && !ComposeLockscreen.isEnabled
+            val useSplitShade = isShadeLayoutWide && !SceneContainerFlag.isEnabled
             when {
                 useSplitShade -> SplitShadeKeyguardBlueprint.ID
                 else -> DefaultKeyguardBlueprint.DEFAULT
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt
deleted file mode 100644
index 601fbfa..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.shared
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the compose lockscreen flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object ComposeLockscreen {
-    /** The aconfig flag name */
-    const val FLAG_NAME = Flags.FLAG_COMPOSE_LOCKSCREEN
-
-    /** A token used for dependency declaration */
-    val token: FlagToken
-        get() = FlagToken(FLAG_NAME, isEnabled)
-
-    /** Is the refactor enabled */
-    @JvmStatic
-    inline val isEnabled
-        get() = Flags.composeLockscreen()
-
-    /**
-     * Called to ensure code is only run when the flag is enabled. This protects users from the
-     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
-     * build to ensure that the refactor author catches issues in testing.
-     */
-    @JvmStatic
-    inline fun isUnexpectedlyInLegacyMode() =
-        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
-    /**
-     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
-     * the flag is enabled to ensure that the refactor author catches issues in testing.
-     */
-    @JvmStatic
-    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index ed82159..deb0b2d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -59,7 +59,6 @@
 import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
@@ -72,6 +71,7 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.CrossFadeHelper
 import com.android.systemui.statusbar.VibratorHelper
@@ -241,7 +241,7 @@
         disposables +=
             view.repeatWhenAttached {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
-                    if (ComposeLockscreen.isEnabled) {
+                    if (SceneContainerFlag.isEnabled) {
                         view.setViewTreeOnBackPressedDispatcherOwner(
                             object : OnBackPressedDispatcherOwner {
                                 override val onBackPressedDispatcher =
@@ -261,10 +261,7 @@
                             ->
                             if (biometricMessage?.message != null) {
                                 chipbarCoordinator!!.displayView(
-                                    createChipbarInfo(
-                                        biometricMessage.message,
-                                        R.drawable.ic_lock,
-                                    )
+                                    createChipbarInfo(biometricMessage.message, R.drawable.ic_lock)
                                 )
                             } else {
                                 chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull")
@@ -327,12 +324,16 @@
                                     .getDimensionPixelSize(R.dimen.shelf_appear_translation)
                                     .stateIn(this)
                             viewModel.isNotifIconContainerVisible.collect { isVisible ->
-                                childViews[aodNotificationIconContainerId]
-                                    ?.setAodNotifIconContainerIsVisible(
-                                        isVisible,
-                                        iconsAppearTranslationPx.value,
-                                        screenOffAnimationController,
-                                    )
+                                if (isVisible.value) {
+                                    blueprintViewModel.refreshBlueprint()
+                                } else {
+                                    childViews[aodNotificationIconContainerId]
+                                        ?.setAodNotifIconContainerIsVisible(
+                                            isVisible,
+                                            iconsAppearTranslationPx.value,
+                                            screenOffAnimationController,
+                                        )
+                                }
                             }
                         }
 
@@ -382,7 +383,7 @@
                                 if (msdlFeedback()) {
                                     msdlPlayer?.playToken(
                                         MSDLToken.UNLOCK,
-                                        authInteractionProperties
+                                        authInteractionProperties,
                                     )
                                 } else {
                                     vibratorHelper.performHapticFeedback(
@@ -398,7 +399,7 @@
                                 if (msdlFeedback()) {
                                     msdlPlayer?.playToken(
                                         MSDLToken.FAILURE,
-                                        authInteractionProperties
+                                        authInteractionProperties,
                                     )
                                 } else {
                                     vibratorHelper.performHapticFeedback(
@@ -425,7 +426,7 @@
                     blueprintViewModel,
                     clockViewModel,
                     childViews,
-                    burnInParams
+                    burnInParams,
                 )
             )
 
@@ -464,11 +465,7 @@
      */
     private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo {
         return ChipbarInfo(
-            startIcon =
-                TintedIcon(
-                    Icon.Resource(icon, null),
-                    ChipbarInfo.DEFAULT_ICON_TINT,
-                ),
+            startIcon = TintedIcon(Icon.Resource(icon, null), ChipbarInfo.DEFAULT_ICON_TINT),
             text = Text.Loaded(message),
             endItem = null,
             vibrationEffect = null,
@@ -499,7 +496,7 @@
             oldLeft: Int,
             oldTop: Int,
             oldRight: Int,
-            oldBottom: Int
+            oldBottom: Int,
         ) {
             // After layout, ensure the notifications are positioned correctly
             childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
@@ -515,7 +512,7 @@
                 viewModel.onNotificationContainerBoundsChanged(
                     notificationListPlaceholder.top.toFloat(),
                     notificationListPlaceholder.bottom.toFloat(),
-                    animate = shouldAnimate
+                    animate = shouldAnimate,
                 )
             }
 
@@ -531,7 +528,7 @@
                                         Int.MAX_VALUE
                                     } else {
                                         view.getTop()
-                                    }
+                                    },
                                 )
                             }
                         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index c6efcfa..4cf3c4e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -25,20 +25,18 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-data class TransitionData(
-    val config: Config,
-    val start: Long = System.currentTimeMillis(),
-)
+data class TransitionData(val config: Config, val start: Long = System.currentTimeMillis())
 
 class KeyguardBlueprintViewModel
 @Inject
 constructor(
     @Main private val handler: Handler,
-    keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+    private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
 ) {
     val blueprint = keyguardBlueprintInteractor.blueprint
     val blueprintId = keyguardBlueprintInteractor.blueprintId
@@ -76,6 +74,9 @@
             }
         }
 
+    fun refreshBlueprint(type: Type = Type.NoTransition) =
+        keyguardBlueprintInteractor.refreshBlueprint(type)
+
     fun updateTransitions(data: TransitionData?, mutate: MutableSet<Transition>.() -> Unit) {
         runningTransitions.mutate()
 
@@ -95,7 +96,7 @@
                 Log.w(
                     TAG,
                     "runTransition: skipping ${transition::class.simpleName}: " +
-                        "currentPriority=$currentPriority; config=$config"
+                        "currentPriority=$currentPriority; config=$config",
                 )
             }
             apply()
@@ -106,7 +107,7 @@
             Log.i(
                 TAG,
                 "runTransition: running ${transition::class.simpleName}: " +
-                    "currentPriority=$currentPriority; config=$config"
+                    "currentPriority=$currentPriority; config=$config",
             )
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 73028c5..36f684e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -25,10 +25,10 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.keyguard.shared.model.ClockSize
 import com.android.systemui.keyguard.shared.model.ClockSizeSetting
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
 import com.android.systemui.statusbar.ui.SystemBarUtilsProxy
@@ -56,10 +56,9 @@
     var burnInLayer: Layer? = null
 
     val clockSize: StateFlow<ClockSize> =
-        combine(
-                keyguardClockInteractor.selectedClockSize,
-                keyguardClockInteractor.clockSize,
-            ) { selectedSize, clockSize ->
+        combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) {
+                selectedSize,
+                clockSize ->
                 if (selectedSize == ClockSizeSetting.SMALL) ClockSize.SMALL else clockSize
             }
             .stateIn(
@@ -80,10 +79,7 @@
     val currentClock = keyguardClockInteractor.currentClock
 
     val hasCustomWeatherDataDisplay =
-        combine(
-                isLargeClockVisible,
-                currentClock,
-            ) { isLargeClock, currentClock ->
+        combine(isLargeClockVisible, currentClock) { isLargeClock, currentClock ->
                 currentClock?.let { clock ->
                     val face = if (isLargeClock) clock.largeClock else clock.smallClock
                     face.config.hasCustomWeatherDataDisplay
@@ -93,14 +89,14 @@
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
                 initialValue =
-                    currentClock.value?.largeClock?.config?.hasCustomWeatherDataDisplay ?: false
+                    currentClock.value?.largeClock?.config?.hasCustomWeatherDataDisplay ?: false,
             )
 
     val clockShouldBeCentered: StateFlow<Boolean> =
         keyguardClockInteractor.clockShouldBeCentered.stateIn(
             scope = applicationScope,
             started = SharingStarted.WhileSubscribed(),
-            initialValue = false
+            initialValue = false,
         )
 
     // To translate elements below smartspace in weather clock to avoid overlapping between date
@@ -111,7 +107,7 @@
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = false
+                initialValue = false,
             )
 
     val currentClockLayout: StateFlow<ClockLayout> =
@@ -145,7 +141,7 @@
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = ClockLayout.SMALL_CLOCK
+                initialValue = ClockLayout.SMALL_CLOCK,
             )
 
     val hasCustomPositionUpdatedAnimation: StateFlow<Boolean> =
@@ -156,7 +152,7 @@
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = false
+                initialValue = false,
             )
 
     /** Calculates the top margin for the small clock. */
@@ -164,10 +160,10 @@
         val statusBarHeight = systemBarUtils.getStatusBarHeaderHeightKeyguard()
         return if (shadeInteractor.isShadeLayoutWide.value) {
             resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin) -
-                if (ComposeLockscreen.isEnabled) statusBarHeight else 0
+                if (SceneContainerFlag.isEnabled) statusBarHeight else 0
         } else {
             resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
-                if (!ComposeLockscreen.isEnabled) statusBarHeight else 0
+                if (!SceneContainerFlag.isEnabled) statusBarHeight else 0
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index ff9495d..2961d05 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -57,7 +57,7 @@
     private static final float DEVICE_CONNECTED_ALPHA = 1f;
     protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
 
-    public MediaOutputAdapter(MediaOutputController controller) {
+    public MediaOutputAdapter(MediaSwitchingController controller) {
         super(controller);
         setHasStableIds(true);
     }
@@ -531,8 +531,10 @@
     @RequiresApi(34)
     private static class Api34Impl {
         @DoNotInline
-        static View.OnClickListener getClickListenerBasedOnSelectionBehavior(MediaDevice device,
-                MediaOutputController controller, View.OnClickListener defaultTransferListener) {
+        static View.OnClickListener getClickListenerBasedOnSelectionBehavior(
+                MediaDevice device,
+                MediaSwitchingController controller,
+                View.OnClickListener defaultTransferListener) {
             switch (device.getSelectionBehavior()) {
                 case SELECTION_BEHAVIOR_NONE:
                     return null;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 5958b0a..63a7e01 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -63,7 +63,7 @@
     static final int CUSTOMIZED_ITEM_GROUP = 2;
     static final int CUSTOMIZED_ITEM_DYNAMIC_GROUP = 3;
 
-    protected final MediaOutputController mController;
+    protected final MediaSwitchingController mController;
 
     private static final int UNMUTE_DEFAULT_VOLUME = 2;
 
@@ -73,7 +73,7 @@
     int mCurrentActivePosition;
     private boolean mIsInitVolumeFirstTime;
 
-    public MediaOutputBaseAdapter(MediaOutputController controller) {
+    public MediaOutputBaseAdapter(MediaSwitchingController controller) {
         mController = controller;
         mIsDragging = false;
         mCurrentActivePosition = -1;
@@ -127,7 +127,7 @@
         return mCurrentActivePosition;
     }
 
-    public MediaOutputController getController() {
+    public MediaSwitchingController getController() {
         return mController;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 6cc4dcb..6bc995f3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -65,11 +65,9 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
-/**
- * Base dialog for media output UI
- */
-public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
-        MediaOutputController.Callback, Window.Callback {
+/** Base dialog for media output UI */
+public abstract class MediaOutputBaseDialog extends SystemUIDialog
+        implements MediaSwitchingController.Callback, Window.Callback {
 
     private static final String TAG = "MediaOutputDialog";
     private static final String EMPTY_TITLE = " ";
@@ -82,7 +80,7 @@
     private final RecyclerView.LayoutManager mLayoutManager;
 
     final Context mContext;
-    final MediaOutputController mMediaOutputController;
+    final MediaSwitchingController mMediaSwitchingController;
     final BroadcastSender mBroadcastSender;
 
     /**
@@ -212,22 +210,22 @@
         @Override
         public void onLayoutCompleted(RecyclerView.State state) {
             super.onLayoutCompleted(state);
-            mMediaOutputController.setRefreshing(false);
-            mMediaOutputController.refreshDataSetIfNeeded();
+            mMediaSwitchingController.setRefreshing(false);
+            mMediaSwitchingController.refreshDataSetIfNeeded();
         }
     }
 
     public MediaOutputBaseDialog(
             Context context,
             BroadcastSender broadcastSender,
-            MediaOutputController mediaOutputController,
+            MediaSwitchingController mediaSwitchingController,
             boolean includePlaybackAndAppMetadata) {
         super(context, R.style.Theme_SystemUI_Dialog_Media);
 
         // Save the context that is wrapped with our theme.
         mContext = getContext();
         mBroadcastSender = broadcastSender;
-        mMediaOutputController = mediaOutputController;
+        mMediaSwitchingController = mediaSwitchingController;
         mLayoutManager = new LayoutManagerWrapper(mContext);
         mListMaxHeight = context.getResources().getDimensionPixelSize(
                 R.dimen.media_output_dialog_list_max_height);
@@ -279,9 +277,9 @@
         // Init bottom buttons
         mDoneButton.setOnClickListener(v -> dismiss());
         mStopButton.setOnClickListener(v -> onStopButtonClick());
-        mAppButton.setOnClickListener(mMediaOutputController::tryToLaunchMediaApplication);
+        mAppButton.setOnClickListener(mMediaSwitchingController::tryToLaunchMediaApplication);
         mMediaMetadataSectionLayout.setOnClickListener(
-                mMediaOutputController::tryToLaunchMediaApplication);
+                mMediaSwitchingController::tryToLaunchMediaApplication);
 
         mDismissing = false;
     }
@@ -298,10 +296,10 @@
 
     @Override
     public void start() {
-        mMediaOutputController.start(this);
+        mMediaSwitchingController.start(this);
         if (isBroadcastSupported() && !mIsLeBroadcastCallbackRegistered) {
-            mMediaOutputController.registerLeBroadcastServiceCallback(mExecutor,
-                    mBroadcastCallback);
+            mMediaSwitchingController.registerLeBroadcastServiceCallback(
+                    mExecutor, mBroadcastCallback);
             mIsLeBroadcastCallbackRegistered = true;
         }
     }
@@ -311,11 +309,11 @@
         // unregister broadcast callback should only depend on profile and registered flag
         // rather than remote device or broadcast state
         // otherwise it might have risks of leaking registered callback handle
-        if (mMediaOutputController.isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) {
-            mMediaOutputController.unregisterLeBroadcastServiceCallback(mBroadcastCallback);
+        if (mMediaSwitchingController.isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) {
+            mMediaSwitchingController.unregisterLeBroadcastServiceCallback(mBroadcastCallback);
             mIsLeBroadcastCallbackRegistered = false;
         }
-        mMediaOutputController.stop();
+        mMediaSwitchingController.stop();
     }
 
     @VisibleForTesting
@@ -326,18 +324,17 @@
     void refresh(boolean deviceSetChanged) {
         // TODO(287191450): remove binder calls in this method from the UI thread.
         // If the dialog is going away or is already refreshing, do nothing.
-        if (mDismissing || mMediaOutputController.isRefreshing()) {
+        if (mDismissing || mMediaSwitchingController.isRefreshing()) {
             return;
         }
-        mMediaOutputController.setRefreshing(true);
+        mMediaSwitchingController.setRefreshing(true);
         // Update header icon
         final int iconRes = getHeaderIconRes();
         final IconCompat headerIcon = getHeaderIcon();
         final IconCompat appSourceIcon = getAppSourceIcon();
         boolean colorSetUpdated = false;
         mCastAppLayout.setVisibility(
-                mMediaOutputController.shouldShowLaunchSection()
-                        ? View.VISIBLE : View.GONE);
+                mMediaSwitchingController.shouldShowLaunchSection() ? View.VISIBLE : View.GONE);
         if (iconRes != 0) {
             mHeaderIcon.setVisibility(View.VISIBLE);
             mHeaderIcon.setImageResource(iconRes);
@@ -371,10 +368,10 @@
             mAppResourceIcon.setVisibility(View.GONE);
         } else if (appSourceIcon != null) {
             Icon appIcon = appSourceIcon.toIcon(mContext);
-            mAppResourceIcon.setColorFilter(mMediaOutputController.getColorItemContent());
+            mAppResourceIcon.setColorFilter(mMediaSwitchingController.getColorItemContent());
             mAppResourceIcon.setImageIcon(appIcon);
         } else {
-            Drawable appIconDrawable = mMediaOutputController.getAppSourceIconFromPackage();
+            Drawable appIconDrawable = mMediaSwitchingController.getAppSourceIconFromPackage();
             if (appIconDrawable != null) {
                 mAppResourceIcon.setImageDrawable(appIconDrawable);
             } else {
@@ -387,7 +384,7 @@
                     R.dimen.media_output_dialog_header_icon_padding);
             mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size));
         }
-        mAppButton.setText(mMediaOutputController.getAppSourceName());
+        mAppButton.setText(mMediaSwitchingController.getAppSourceName());
 
         if (!mIncludePlaybackAndAppMetadata) {
             mHeaderTitle.setVisibility(View.GONE);
@@ -424,23 +421,26 @@
                 mAdapter.updateItems();
             }
         } else {
-            mMediaOutputController.setRefreshing(false);
-            mMediaOutputController.refreshDataSetIfNeeded();
+            mMediaSwitchingController.setRefreshing(false);
+            mMediaSwitchingController.refreshDataSetIfNeeded();
         }
     }
 
     private void updateButtonBackgroundColorFilter() {
-        ColorFilter buttonColorFilter = new PorterDuffColorFilter(
-                mMediaOutputController.getColorButtonBackground(),
-                PorterDuff.Mode.SRC_IN);
+        ColorFilter buttonColorFilter =
+                new PorterDuffColorFilter(
+                        mMediaSwitchingController.getColorButtonBackground(),
+                        PorterDuff.Mode.SRC_IN);
         mDoneButton.getBackground().setColorFilter(buttonColorFilter);
         mStopButton.getBackground().setColorFilter(buttonColorFilter);
-        mDoneButton.setTextColor(mMediaOutputController.getColorPositiveButtonText());
+        mDoneButton.setTextColor(mMediaSwitchingController.getColorPositiveButtonText());
     }
 
     private void updateDialogBackgroundColor() {
-        getDialogView().getBackground().setTint(mMediaOutputController.getColorDialogBackground());
-        mDeviceListLayout.setBackgroundColor(mMediaOutputController.getColorDialogBackground());
+        getDialogView()
+                .getBackground()
+                .setTint(mMediaSwitchingController.getColorDialogBackground());
+        mDeviceListLayout.setBackgroundColor(mMediaSwitchingController.getColorDialogBackground());
     }
 
     private Drawable resizeDrawable(Drawable drawable, int size) {
@@ -499,7 +499,7 @@
     protected void startLeBroadcast() {
         mStopButton.setText(R.string.media_output_broadcast_starting);
         mStopButton.setEnabled(false);
-        if (!mMediaOutputController.startBluetoothLeBroadcast()) {
+        if (!mMediaSwitchingController.startBluetoothLeBroadcast()) {
             // If the system can't execute "broadcast start", then UI shows the error.
             handleLeBroadcastStartFailed();
         }
@@ -512,9 +512,10 @@
                 && sharedPref.getBoolean(PREF_IS_LE_BROADCAST_FIRST_LAUNCH, true)) {
             Log.d(TAG, "PREF_IS_LE_BROADCAST_FIRST_LAUNCH: true");
 
-            mMediaOutputController.launchLeBroadcastNotifyDialog(mDialogView,
+            mMediaSwitchingController.launchLeBroadcastNotifyDialog(
+                    mDialogView,
                     mBroadcastSender,
-                    MediaOutputController.BroadcastNotifyDialog.ACTION_FIRST_LAUNCH,
+                    MediaSwitchingController.BroadcastNotifyDialog.ACTION_FIRST_LAUNCH,
                     (d, w) -> {
                         startLeBroadcast();
                     });
@@ -527,14 +528,13 @@
     }
 
     protected void startLeBroadcastDialog() {
-        mMediaOutputController.launchMediaOutputBroadcastDialog(mDialogView,
-                mBroadcastSender);
+        mMediaSwitchingController.launchMediaOutputBroadcastDialog(mDialogView, mBroadcastSender);
         refresh();
     }
 
     protected void stopLeBroadcast() {
         mStopButton.setEnabled(false);
-        if (!mMediaOutputController.stopBluetoothLeBroadcast()) {
+        if (!mMediaSwitchingController.stopBluetoothLeBroadcast()) {
             // If the system can't execute "broadcast stop", then UI does refresh.
             mMainThreadHandler.post(() -> refresh());
         }
@@ -559,7 +559,7 @@
     }
 
     public void onStopButtonClick() {
-        mMediaOutputController.releaseSession();
+        mMediaSwitchingController.releaseSession();
         dismiss();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index 1e31755..9b5b872a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -235,14 +235,17 @@
                 }
             };
 
-    MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar,
-            BroadcastSender broadcastSender, MediaOutputController mediaOutputController) {
+    MediaOutputBroadcastDialog(
+            Context context,
+            boolean aboveStatusbar,
+            BroadcastSender broadcastSender,
+            MediaSwitchingController mediaSwitchingController) {
         super(
                 context,
                 broadcastSender,
-                mediaOutputController, /* includePlaybackAndAppMetadata */
+                mediaSwitchingController, /* includePlaybackAndAppMetadata */
                 true);
-        mAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
         //  that extends MediaOutputBaseDialog
         if (!aboveStatusbar) {
@@ -262,8 +265,8 @@
         super.start();
         if (!mIsLeBroadcastAssistantCallbackRegistered) {
             mIsLeBroadcastAssistantCallbackRegistered = true;
-            mMediaOutputController.registerLeBroadcastAssistantServiceCallback(mExecutor,
-                    mBroadcastAssistantCallback);
+            mMediaSwitchingController.registerLeBroadcastAssistantServiceCallback(
+                    mExecutor, mBroadcastAssistantCallback);
         }
         /* Add local source broadcast to connected capable devices that may be possible receivers
          * of stream.
@@ -276,7 +279,7 @@
         super.stop();
         if (mIsLeBroadcastAssistantCallbackRegistered) {
             mIsLeBroadcastAssistantCallbackRegistered = false;
-            mMediaOutputController.unregisterLeBroadcastAssistantServiceCallback(
+            mMediaSwitchingController.unregisterLeBroadcastAssistantServiceCallback(
                     mBroadcastAssistantCallback);
         }
     }
@@ -288,7 +291,7 @@
 
     @Override
     IconCompat getHeaderIcon() {
-        return mMediaOutputController.getHeaderIcon();
+        return mMediaSwitchingController.getHeaderIcon();
     }
 
     @Override
@@ -299,17 +302,17 @@
 
     @Override
     CharSequence getHeaderText() {
-        return mMediaOutputController.getHeaderTitle();
+        return mMediaSwitchingController.getHeaderTitle();
     }
 
     @Override
     CharSequence getHeaderSubtitle() {
-        return mMediaOutputController.getHeaderSubTitle();
+        return mMediaSwitchingController.getHeaderSubTitle();
     }
 
     @Override
     IconCompat getAppSourceIcon() {
-        return mMediaOutputController.getNotificationSmallIcon();
+        return mMediaSwitchingController.getNotificationSmallIcon();
     }
 
     @Override
@@ -319,16 +322,16 @@
 
     @Override
     public void onStopButtonClick() {
-        mMediaOutputController.stopBluetoothLeBroadcast();
+        mMediaSwitchingController.stopBluetoothLeBroadcast();
         dismiss();
     }
 
     private String getBroadcastMetadataInfo(int metadata) {
         switch (metadata) {
             case METADATA_BROADCAST_NAME:
-                return mMediaOutputController.getBroadcastName();
+                return mMediaSwitchingController.getBroadcastName();
             case METADATA_BROADCAST_CODE:
-                return mMediaOutputController.getBroadcastCode();
+                return mMediaSwitchingController.getBroadcastCode();
             default:
                 return "";
         }
@@ -342,13 +345,15 @@
         mBroadcastQrCodeView = getDialogView().requireViewById(R.id.qrcode_view);
 
         mBroadcastNotify = getDialogView().requireViewById(R.id.broadcast_info);
-        mBroadcastNotify.setOnClickListener(v -> {
-            mMediaOutputController.launchLeBroadcastNotifyDialog(
-                    /* view= */ null,
-                    /* broadcastSender= */ null,
-                    MediaOutputController.BroadcastNotifyDialog.ACTION_BROADCAST_INFO_ICON,
-                    /* onClickListener= */ null);
-        });
+        mBroadcastNotify.setOnClickListener(
+                v -> {
+                    mMediaSwitchingController.launchLeBroadcastNotifyDialog(
+                            /* mediaOutputDialog= */ null,
+                            /* broadcastSender= */ null,
+                            MediaSwitchingController.BroadcastNotifyDialog
+                                    .ACTION_BROADCAST_INFO_ICON,
+                            /* listener= */ null);
+                });
         mBroadcastName = getDialogView().requireViewById(R.id.broadcast_name_summary);
         mBroadcastNameEdit = getDialogView().requireViewById(R.id.broadcast_name_edit);
         mBroadcastNameEdit.setOnClickListener(v -> {
@@ -409,16 +414,16 @@
             return;
         }
 
-        for (BluetoothDevice sink : mMediaOutputController.getConnectedBroadcastSinkDevices()) {
+        for (BluetoothDevice sink : mMediaSwitchingController.getConnectedBroadcastSinkDevices()) {
             Log.d(TAG, "The broadcastMetadata broadcastId: " + broadcastMetadata.getBroadcastId()
                     + ", the device: " + sink.getAnonymizedAddress());
 
-            if (mMediaOutputController.isThereAnyBroadcastSourceIntoSinkDevice(sink)) {
+            if (mMediaSwitchingController.isThereAnyBroadcastSourceIntoSinkDevice(sink)) {
                 Log.d(TAG, "The sink device has the broadcast source now.");
                 return;
             }
-            if (!mMediaOutputController.addSourceIntoSinkDeviceWithBluetoothLeAssistant(sink,
-                    broadcastMetadata, /*isGroupOp=*/ false)) {
+            if (!mMediaSwitchingController.addSourceIntoSinkDeviceWithBluetoothLeAssistant(
+                    sink, broadcastMetadata, /* isGroupOp= */ false)) {
                 Log.e(TAG, "Error: Source add failed");
             }
         }
@@ -457,11 +462,11 @@
     }
 
     private String getLocalBroadcastMetadataQrCodeString() {
-        return mMediaOutputController.getLocalBroadcastMetadataQrCodeString();
+        return mMediaSwitchingController.getLocalBroadcastMetadataQrCodeString();
     }
 
     private BluetoothLeBroadcastMetadata getBroadcastMetadata() {
-        return mMediaOutputController.getBroadcastMetadata();
+        return mMediaSwitchingController.getBroadcastMetadata();
     }
 
     @VisibleForTesting
@@ -476,8 +481,8 @@
              * stopped then used the new Broadcast code to start the Broadcast.
              */
             mIsStopbyUpdateBroadcastCode = true;
-            mMediaOutputController.setBroadcastCode(updatedString);
-            if (!mMediaOutputController.stopBluetoothLeBroadcast()) {
+            mMediaSwitchingController.setBroadcastCode(updatedString);
+            if (!mMediaSwitchingController.stopBluetoothLeBroadcast()) {
                 handleLeBroadcastStopFailed();
                 return;
             }
@@ -485,8 +490,8 @@
             /* If the user wants to update the Broadcast Name, we don't need to stop the Broadcast
              * session. Only use the new Broadcast name to update the broadcast session.
              */
-            mMediaOutputController.setBroadcastName(updatedString);
-            if (!mMediaOutputController.updateBluetoothLeBroadcast()) {
+            mMediaSwitchingController.setBroadcastName(updatedString);
+            if (!mMediaSwitchingController.updateBluetoothLeBroadcast()) {
                 handleLeBroadcastUpdateFailed();
             }
         }
@@ -496,12 +501,13 @@
     public boolean isBroadcastSupported() {
         if (!legacyLeAudioSharing()) return false;
         boolean isBluetoothLeDevice = false;
-        if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) {
-            isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice(
-                    mMediaOutputController.getCurrentConnectedMediaDevice());
+        if (mMediaSwitchingController.getCurrentConnectedMediaDevice() != null) {
+            isBluetoothLeDevice =
+                    mMediaSwitchingController.isBluetoothLeDevice(
+                            mMediaSwitchingController.getCurrentConnectedMediaDevice());
         }
 
-        return mMediaOutputController.isBroadcastSupported() && isBluetoothLeDevice;
+        return mMediaSwitchingController.isBroadcastSupported() && isBluetoothLeDevice;
     }
 
     @Override
@@ -515,7 +521,7 @@
 
     @Override
     public void handleLeBroadcastStartFailed() {
-        mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode);
+        mMediaSwitchingController.setBroadcastCode(mCurrentBroadcastCode);
         mRetryCount++;
 
         handleUpdateFailedUi();
@@ -538,8 +544,8 @@
 
     @Override
     public void handleLeBroadcastUpdateFailed() {
-        //Change the value in shared preferences back to it original value
-        mMediaOutputController.setBroadcastName(mCurrentBroadcastName);
+        // Change the value in shared preferences back to it original value
+        mMediaSwitchingController.setBroadcastName(mCurrentBroadcastName);
         mRetryCount++;
 
         handleUpdateFailedUi();
@@ -550,7 +556,7 @@
         if (mIsStopbyUpdateBroadcastCode) {
             mIsStopbyUpdateBroadcastCode = false;
             mRetryCount = 0;
-            if (!mMediaOutputController.startBluetoothLeBroadcast()) {
+            if (!mMediaSwitchingController.startBluetoothLeBroadcast()) {
                 handleLeBroadcastStartFailed();
                 return;
             }
@@ -561,8 +567,8 @@
 
     @Override
     public void handleLeBroadcastStopFailed() {
-        //Change the value in shared preferences back to it original value
-        mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode);
+        // Change the value in shared preferences back to it original value
+        mMediaSwitchingController.setBroadcastCode(mCurrentBroadcastCode);
         mRetryCount++;
 
         handleUpdateFailedUi();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
index 6ef9ea3..2e7e66f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
@@ -29,7 +29,7 @@
     private val context: Context,
     private val broadcastSender: BroadcastSender,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
-    private val mediaOutputControllerFactory: MediaOutputController.Factory
+    private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory
 ) {
     var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
 
@@ -41,7 +41,7 @@
         // TODO: b/321969740 - Populate the userHandle parameter. The user handle is necessary to
         //  disambiguate the same package running on different users.
         val controller =
-            mediaOutputControllerFactory.create(
+            mediaSwitchingControllerFactory.create(
                 packageName,
                 /* userHandle= */ null,
                 /* token */ null,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index eb6a320..c9af7b3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -46,14 +46,14 @@
             Context context,
             boolean aboveStatusbar,
             BroadcastSender broadcastSender,
-            MediaOutputController mediaOutputController,
+            MediaSwitchingController mediaSwitchingController,
             DialogTransitionAnimator dialogTransitionAnimator,
             UiEventLogger uiEventLogger,
             boolean includePlaybackAndAppMetadata) {
-        super(context, broadcastSender, mediaOutputController, includePlaybackAndAppMetadata);
+        super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata);
         mDialogTransitionAnimator = dialogTransitionAnimator;
         mUiEventLogger = uiEventLogger;
-        mAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         if (!aboveStatusbar) {
             getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
         }
@@ -72,7 +72,7 @@
 
     @Override
     IconCompat getHeaderIcon() {
-        return mMediaOutputController.getHeaderIcon();
+        return mMediaSwitchingController.getHeaderIcon();
     }
 
     @Override
@@ -83,27 +83,29 @@
 
     @Override
     CharSequence getHeaderText() {
-        return mMediaOutputController.getHeaderTitle();
+        return mMediaSwitchingController.getHeaderTitle();
     }
 
     @Override
     CharSequence getHeaderSubtitle() {
-        return mMediaOutputController.getHeaderSubTitle();
+        return mMediaSwitchingController.getHeaderSubTitle();
     }
 
     @Override
     IconCompat getAppSourceIcon() {
-        return mMediaOutputController.getNotificationSmallIcon();
+        return mMediaSwitchingController.getNotificationSmallIcon();
     }
 
     @Override
     int getStopButtonVisibility() {
         boolean isActiveRemoteDevice = false;
-        if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) {
-            isActiveRemoteDevice = mMediaOutputController.isActiveRemoteDevice(
-                    mMediaOutputController.getCurrentConnectedMediaDevice());
+        if (mMediaSwitchingController.getCurrentConnectedMediaDevice() != null) {
+            isActiveRemoteDevice =
+                    mMediaSwitchingController.isActiveRemoteDevice(
+                            mMediaSwitchingController.getCurrentConnectedMediaDevice());
         }
-        boolean showBroadcastButton = isBroadcastSupported() && mMediaOutputController.isPlaying();
+        boolean showBroadcastButton =
+                isBroadcastSupported() && mMediaSwitchingController.isPlaying();
 
         return (isActiveRemoteDevice || showBroadcastButton) ? View.VISIBLE : View.GONE;
     }
@@ -115,13 +117,14 @@
         boolean isBroadcastEnabled = false;
         if (FeatureFlagUtils.isEnabled(mContext,
                 FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST)) {
-            if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) {
-                isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice(
-                    mMediaOutputController.getCurrentConnectedMediaDevice());
+            if (mMediaSwitchingController.getCurrentConnectedMediaDevice() != null) {
+                isBluetoothLeDevice =
+                        mMediaSwitchingController.isBluetoothLeDevice(
+                                mMediaSwitchingController.getCurrentConnectedMediaDevice());
                 // if broadcast is active, broadcast should be considered as supported
                 // there could be a valid case that broadcast is ongoing
                 // without active LEA device connected
-                isBroadcastEnabled = mMediaOutputController.isBluetoothLeBroadcastEnabled();
+                isBroadcastEnabled = mMediaSwitchingController.isBluetoothLeBroadcastEnabled();
             }
         } else {
             // To decouple LE Audio Broadcast and Unicast, it always displays the button when there
@@ -129,15 +132,16 @@
             isBluetoothLeDevice = true;
         }
 
-        return mMediaOutputController.isBroadcastSupported()
+        return mMediaSwitchingController.isBroadcastSupported()
                 && (isBluetoothLeDevice || isBroadcastEnabled);
     }
 
     @Override
     public CharSequence getStopButtonText() {
         int resId = R.string.media_output_dialog_button_stop_casting;
-        if (isBroadcastSupported() && mMediaOutputController.isPlaying()
-                && !mMediaOutputController.isBluetoothLeBroadcastEnabled()) {
+        if (isBroadcastSupported()
+                && mMediaSwitchingController.isPlaying()
+                && !mMediaSwitchingController.isBluetoothLeBroadcastEnabled()) {
             resId = R.string.media_output_broadcast;
         }
         return mContext.getText(resId);
@@ -145,8 +149,8 @@
 
     @Override
     public void onStopButtonClick() {
-        if (isBroadcastSupported() && mMediaOutputController.isPlaying()) {
-            if (!mMediaOutputController.isBluetoothLeBroadcastEnabled()) {
+        if (isBroadcastSupported() && mMediaSwitchingController.isPlaying()) {
+            if (!mMediaSwitchingController.isBluetoothLeBroadcastEnabled()) {
                 if (startLeBroadcastDialogForFirstTime()) {
                     return;
                 }
@@ -155,7 +159,7 @@
                 stopLeBroadcast();
             }
         } else {
-            mMediaOutputController.releaseSession();
+            mMediaSwitchingController.releaseSession();
             mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations();
             dismiss();
         }
@@ -163,8 +167,9 @@
 
     @Override
     public int getBroadcastIconVisibility() {
-        return (isBroadcastSupported() && mMediaOutputController.isBluetoothLeBroadcastEnabled())
-                ? View.VISIBLE : View.GONE;
+        return (isBroadcastSupported() && mMediaSwitchingController.isBluetoothLeBroadcastEnabled())
+                ? View.VISIBLE
+                : View.GONE;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
index 47e0691..4e9451a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
@@ -35,7 +35,7 @@
     private val broadcastSender: BroadcastSender,
     private val uiEventLogger: UiEventLogger,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
-    private val mediaOutputControllerFactory: MediaOutputController.Factory,
+    private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory,
 ) {
     companion object {
         const val INTERACTION_JANK_TAG = "media_output"
@@ -118,7 +118,7 @@
         // Dismiss the previous dialog, if any.
         mediaOutputDialog?.dismiss()
 
-        val controller = mediaOutputControllerFactory.create(packageName, userHandle, token)
+        val controller = mediaSwitchingControllerFactory.create(packageName, userHandle, token)
 
         val mediaOutputDialog =
             MediaOutputDialog(
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
rename to packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 875e505..2cbc7575 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -77,6 +77,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.media.InfoMediaManager;
+import com.android.settingslib.media.InputRouteManager;
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
 import com.android.settingslib.media.flags.Flags;
@@ -116,12 +117,13 @@
 import java.util.stream.Collectors;
 
 /**
- * Controller for media output dialog
+ * Controller for a dialog that allows users to switch media output and input devices, control
+ * volume, connect to new devices, etc.
  */
-public class MediaOutputController implements LocalMediaManager.DeviceCallback,
-        INearbyMediaDevicesUpdateCallback {
+public class MediaSwitchingController
+        implements LocalMediaManager.DeviceCallback, INearbyMediaDevicesUpdateCallback {
 
-    private static final String TAG = "MediaOutputController";
+    private static final String TAG = "MediaSwitchingController";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private static final String PAGE_CONNECTED_DEVICES_KEY =
             "top_level_connected_devices";
@@ -137,10 +139,12 @@
     private final DialogTransitionAnimator mDialogTransitionAnimator;
     private final CommonNotifCollection mNotifCollection;
     protected final Object mMediaDevicesLock = new Object();
+    protected final Object mInputMediaDevicesLock = new Object();
     @VisibleForTesting
     final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
     final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
-    private final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
+    private final List<MediaItem> mOutputMediaItemList = new CopyOnWriteArrayList<>();
+    private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>();
     private final AudioManager mAudioManager;
     private final PowerExemptionManager mPowerExemptionManager;
     private final KeyguardManager mKeyGuardManager;
@@ -153,6 +157,7 @@
     @VisibleForTesting
     boolean mNeedRefresh = false;
     private MediaController mMediaController;
+    private InputRouteManager mInputRouteManager;
     @VisibleForTesting
     Callback mCallback;
     @VisibleForTesting
@@ -181,8 +186,20 @@
         ACTION_BROADCAST_INFO_ICON
     }
 
+    @VisibleForTesting
+    final InputRouteManager.InputDeviceCallback mInputDeviceCallback =
+            new InputRouteManager.InputDeviceCallback() {
+                @Override
+                public void onInputDeviceListUpdated(@NonNull List<MediaDevice> devices) {
+                    synchronized (mInputMediaDevicesLock) {
+                        buildInputMediaItems(devices);
+                        mCallback.onDeviceListChanged();
+                    }
+                }
+            };
+
     @AssistedInject
-    public MediaOutputController(
+    public MediaSwitchingController(
             Context context,
             @Assisted String packageName,
             @Assisted @Nullable UserHandle userHandle,
@@ -241,19 +258,23 @@
                 R.dimen.media_output_dialog_default_margin_end);
         mItemMarginEndSelectable = (int) mContext.getResources().getDimension(
                 R.dimen.media_output_dialog_selectable_margin_end);
+
+        if (enableInputRouting()) {
+            mInputRouteManager = new InputRouteManager(mContext, audioManager);
+        }
     }
 
     @AssistedFactory
     public interface Factory {
-        /** Construct a MediaOutputController */
-        MediaOutputController create(
+        /** Construct a MediaSwitchingController */
+        MediaSwitchingController create(
                 String packageName, UserHandle userHandle, MediaSession.Token token);
     }
 
     protected void start(@NonNull Callback cb) {
         synchronized (mMediaDevicesLock) {
             mCachedMediaDevices.clear();
-            mMediaItemList.clear();
+            mOutputMediaItemList.clear();
         }
         mNearbyDeviceInfoMap.clear();
         if (mNearbyMediaDevicesManager != null) {
@@ -277,6 +298,10 @@
         mCallback = cb;
         mLocalMediaManager.registerCallback(this);
         mLocalMediaManager.startScan();
+
+        if (enableInputRouting()) {
+            mInputRouteManager.registerCallback(mInputDeviceCallback);
+        }
     }
 
     boolean shouldShowLaunchSection() {
@@ -300,12 +325,19 @@
         mLocalMediaManager.stopScan();
         synchronized (mMediaDevicesLock) {
             mCachedMediaDevices.clear();
-            mMediaItemList.clear();
+            mOutputMediaItemList.clear();
         }
         if (mNearbyMediaDevicesManager != null) {
             mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
         }
         mNearbyDeviceInfoMap.clear();
+
+        if (enableInputRouting()) {
+            mInputRouteManager.unregisterCallback(mInputDeviceCallback);
+            synchronized (mInputMediaDevicesLock) {
+                mInputMediaItemList.clear();
+            }
+        }
     }
 
     private MediaController getMediaController() {
@@ -335,7 +367,7 @@
 
     @Override
     public void onDeviceListUpdate(List<MediaDevice> devices) {
-        boolean isListEmpty = mMediaItemList.isEmpty();
+        boolean isListEmpty = mOutputMediaItemList.isEmpty();
         if (isListEmpty || !mIsRefreshing) {
             buildMediaItems(devices);
             mCallback.onDeviceListChanged();
@@ -352,7 +384,8 @@
     public void onSelectedDeviceStateChanged(MediaDevice device,
             @LocalMediaManager.MediaDeviceState int state) {
         mCallback.onRouteChanged();
-        mMetricLogger.logOutputItemSuccess(device.toString(), new ArrayList<>(mMediaItemList));
+        mMetricLogger.logOutputItemSuccess(
+                device.toString(), new ArrayList<>(mOutputMediaItemList));
     }
 
     @Override
@@ -363,7 +396,7 @@
     @Override
     public void onRequestFailed(int reason) {
         mCallback.onRouteChanged();
-        mMetricLogger.logOutputItemFailure(new ArrayList<>(mMediaItemList), reason);
+        mMetricLogger.logOutputItemFailure(new ArrayList<>(mOutputMediaItemList), reason);
     }
 
     /**
@@ -382,7 +415,7 @@
         }
         try {
             synchronized (mMediaDevicesLock) {
-                mMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+                mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
             }
             mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice());
         } catch (Exception e) {
@@ -638,9 +671,9 @@
 
     private void buildMediaItems(List<MediaDevice> devices) {
         synchronized (mMediaDevicesLock) {
-            List<MediaItem> updatedMediaItems = buildMediaItems(mMediaItemList, devices);
-            mMediaItemList.clear();
-            mMediaItemList.addAll(updatedMediaItems);
+            List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices);
+            mOutputMediaItemList.clear();
+            mOutputMediaItemList.addAll(updatedMediaItems);
         }
     }
 
@@ -714,6 +747,19 @@
         }
     }
 
+    private boolean enableInputRouting() {
+        return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl();
+    }
+
+    private void buildInputMediaItems(List<MediaDevice> devices) {
+        synchronized (mInputMediaDevicesLock) {
+            List<MediaItem> updatedInputMediaItems =
+                    devices.stream().map(MediaItem::createDeviceMediaItem).toList();
+            mInputMediaItemList.clear();
+            mInputMediaItemList.addAll(updatedInputMediaItems);
+        }
+    }
+
     /**
      * Initial categorization of current devices, will not be called for updates to the devices
      * list.
@@ -778,7 +824,6 @@
                 mediaDevice.setRangeZone(mNearbyDeviceInfoMap.get(mediaDevice.getId()));
             }
         }
-
     }
 
     boolean isCurrentConnectedDeviceRemote() {
@@ -837,8 +882,31 @@
         });
     }
 
+    private void addInputDevices(List<MediaItem> mediaItems) {
+        mediaItems.add(
+                MediaItem.createGroupDividerMediaItem(
+                        mContext.getString(R.string.media_input_group_title)));
+        mediaItems.addAll(mInputMediaItemList);
+    }
+
+    private void addOutputDevices(List<MediaItem> mediaItems) {
+        mediaItems.add(
+                MediaItem.createGroupDividerMediaItem(
+                        mContext.getString(R.string.media_output_group_title)));
+        mediaItems.addAll(mOutputMediaItemList);
+    }
+
     public List<MediaItem> getMediaItemList() {
-        return mMediaItemList;
+        // If input routing is not enabled, only return output media items.
+        if (!enableInputRouting()) {
+            return mOutputMediaItemList;
+        }
+
+        // If input routing is enabled, return both output and input media items.
+        List<MediaItem> mediaItems = new ArrayList<>();
+        addOutputDevices(mediaItems);
+        addInputDevices(mediaItems);
+        return mediaItems;
     }
 
     public MediaDevice getCurrentConnectedMediaDevice() {
@@ -921,7 +989,7 @@
 
     public boolean isAnyDeviceTransferring() {
         synchronized (mMediaDevicesLock) {
-            for (MediaItem mediaItem : mMediaItemList) {
+            for (MediaItem mediaItem : mOutputMediaItemList) {
                 if (mediaItem.getMediaDevice().isPresent()
                         && mediaItem.getMediaDevice().get().getState()
                         == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
@@ -986,8 +1054,8 @@
     }
 
     void launchMediaOutputBroadcastDialog(View mediaOutputDialog, BroadcastSender broadcastSender) {
-        MediaOutputController controller =
-                new MediaOutputController(
+        MediaSwitchingController controller =
+                new MediaSwitchingController(
                         mContext,
                         mPackageName,
                         mUserHandle,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
index 072d322..1fe54e4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
@@ -23,17 +23,14 @@
 import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepositoryImpl
 import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepository
 import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepositoryImpl
-import com.android.systemui.qs.panels.domain.interactor.GridTypeConsistencyInteractor
-import com.android.systemui.qs.panels.domain.interactor.InfiniteGridConsistencyInteractor
-import com.android.systemui.qs.panels.domain.interactor.NoopGridConsistencyInteractor
 import com.android.systemui.qs.panels.shared.model.GridLayoutType
 import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType
 import com.android.systemui.qs.panels.shared.model.PaginatedGridLayoutType
 import com.android.systemui.qs.panels.shared.model.PanelsLog
 import com.android.systemui.qs.panels.ui.compose.GridLayout
-import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
 import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
 import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
 import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModelImpl
 import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModel
@@ -56,11 +53,6 @@
     @Binds
     fun bindGridLayoutTypeRepository(impl: GridLayoutTypeRepositoryImpl): GridLayoutTypeRepository
 
-    @Binds
-    fun bindDefaultGridConsistencyInteractor(
-        impl: NoopGridConsistencyInteractor
-    ): GridTypeConsistencyInteractor
-
     @Binds fun bindIconTilesViewModel(impl: IconTilesViewModelImpl): IconTilesViewModel
 
     @Binds fun bindGridSizeViewModel(impl: FixedColumnsSizeViewModelImpl): FixedColumnsSizeViewModel
@@ -74,12 +66,6 @@
     @PaginatedBaseLayoutType
     fun bindPaginatedBaseGridLayout(impl: InfiniteGridLayout): PaginatableGridLayout
 
-    @Binds
-    @PaginatedBaseLayoutType
-    fun bindPaginatedBaseConsistencyInteractor(
-        impl: NoopGridConsistencyInteractor
-    ): GridTypeConsistencyInteractor
-
     @Binds @Named("Default") fun bindDefaultGridLayout(impl: PaginatedGridLayout): GridLayout
 
     companion object {
@@ -117,28 +103,5 @@
         ): Set<GridLayoutType> {
             return entries.map { it.first }.toSet()
         }
-
-        @Provides
-        @IntoSet
-        fun provideGridConsistencyInteractor(
-            consistencyInteractor: InfiniteGridConsistencyInteractor
-        ): Pair<GridLayoutType, GridTypeConsistencyInteractor> {
-            return Pair(InfiniteGridLayoutType, consistencyInteractor)
-        }
-
-        @Provides
-        @IntoSet
-        fun providePaginatedGridConsistencyInteractor(
-            @PaginatedBaseLayoutType consistencyInteractor: GridTypeConsistencyInteractor,
-        ): Pair<GridLayoutType, GridTypeConsistencyInteractor> {
-            return Pair(PaginatedGridLayoutType, consistencyInteractor)
-        }
-
-        @Provides
-        fun provideGridConsistencyInteractorMap(
-            entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridTypeConsistencyInteractor>>
-        ): Map<GridLayoutType, GridTypeConsistencyInteractor> {
-            return entries.toMap()
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractor.kt
deleted file mode 100644
index a2e7ea6..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractor.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.core.LogLevel
-import com.android.systemui.qs.panels.shared.model.GridLayoutType
-import com.android.systemui.qs.panels.shared.model.PanelsLog
-import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class GridConsistencyInteractor
-@Inject
-constructor(
-    private val gridLayoutTypeInteractor: GridLayoutTypeInteractor,
-    private val currentTilesInteractor: CurrentTilesInteractor,
-    private val consistencyInteractors:
-        Map<GridLayoutType, @JvmSuppressWildcards GridTypeConsistencyInteractor>,
-    private val defaultConsistencyInteractor: GridTypeConsistencyInteractor,
-    @PanelsLog private val logBuffer: LogBuffer,
-    @Application private val applicationScope: CoroutineScope,
-) {
-    fun start() {
-        applicationScope.launch {
-            gridLayoutTypeInteractor.layout.collectLatest { type ->
-                val consistencyInteractor =
-                    consistencyInteractors[type] ?: defaultConsistencyInteractor
-                currentTilesInteractor.currentTiles
-                    .map { tiles -> tiles.map { it.spec } }
-                    .collectLatest { tiles ->
-                        val newTiles = consistencyInteractor.reconcileTiles(tiles)
-                        if (newTiles != tiles) {
-                            currentTilesInteractor.setTiles(newTiles)
-                            logChange(newTiles)
-                        }
-                    }
-            }
-        }
-    }
-
-    private fun logChange(tiles: List<TileSpec>) {
-        logBuffer.log(
-            LOG_BUFFER_CURRENT_TILES_CHANGE_TAG,
-            LogLevel.DEBUG,
-            { str1 = tiles.toString() },
-            { "Tiles reordered: $str1" }
-        )
-    }
-
-    private companion object {
-        const val LOG_BUFFER_CURRENT_TILES_CHANGE_TAG = "GridConsistencyTilesChange"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridTypeConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridTypeConsistencyInteractor.kt
deleted file mode 100644
index 4cdabae..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridTypeConsistencyInteractor.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.qs.pipeline.shared.TileSpec
-
-interface GridTypeConsistencyInteractor {
-    /**
-     * Given a list of tiles, return the best list of the same tiles (preserving as much order as
-     * possible, such that it's consistent with the current layout.
-     */
-    fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec>
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
deleted file mode 100644
index 874b3b0..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.domain.interactor
-
-import android.util.Log
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.shared.model.SizedTile
-import com.android.systemui.qs.panels.shared.model.SizedTileImpl
-import com.android.systemui.qs.panels.shared.model.TileRow
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import javax.inject.Inject
-
-@SysUISingleton
-class InfiniteGridConsistencyInteractor
-@Inject
-constructor(
-    private val iconTilesInteractor: IconTilesInteractor,
-    private val gridSizeInteractor: FixedColumnsSizeInteractor
-) : GridTypeConsistencyInteractor {
-
-    /**
-     * Tries to fill in every columns of all rows (except the last row), potentially reordering
-     * tiles.
-     */
-    override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> {
-        val newTiles: MutableList<TileSpec> = mutableListOf()
-        val row = TileRow<TileSpec>(columns = gridSizeInteractor.columns.value)
-        val tilesQueue: ArrayDeque<SizedTile<TileSpec>> =
-            ArrayDeque(
-                tiles.map {
-                    SizedTileImpl(
-                        it,
-                        if (iconTilesInteractor.isIconTile(it)) 1 else 2,
-                    )
-                }
-            )
-
-        while (tilesQueue.isNotEmpty()) {
-            if (row.isFull()) {
-                newTiles.addAll(row.tiles.map { it.tile })
-                row.clear()
-            }
-
-            val tile = tilesQueue.removeFirst()
-
-            // If the tile fits in the row, add it.
-            if (!row.maybeAddTile(tile)) {
-                // If the tile does not fit the row, find an icon tile to move.
-                // We'll try to either add an icon tile from the queue to complete the row, or
-                // remove an icon tile from the current row to free up space.
-
-                val iconTile: SizedTile<TileSpec>? = tilesQueue.firstOrNull { it.width == 1 }
-                if (iconTile != null) {
-                    tilesQueue.remove(iconTile)
-                    tilesQueue.addFirst(tile)
-                    row.maybeAddTile(iconTile)
-                } else {
-                    val tileToRemove: SizedTile<TileSpec>? = row.findLastIconTile()
-                    if (tileToRemove != null) {
-                        row.removeTile(tileToRemove)
-                        row.maybeAddTile(tile)
-
-                        // Moving the icon tile to the end because there's no other
-                        // icon tiles in the queue.
-                        tilesQueue.addLast(tileToRemove)
-                    } else {
-                        // If the row does not have an icon tile, add the incomplete row.
-                        // Note: this shouldn't happen because an icon tile is guaranteed to be in a
-                        // row that doesn't have enough space for a large tile.
-                        val tileSpecs = row.tiles.map { it.tile }
-                        Log.wtf(TAG, "Uneven row does not have an icon tile to remove: $tileSpecs")
-                        newTiles.addAll(tileSpecs)
-                        row.clear()
-                        tilesQueue.addFirst(tile)
-                    }
-                }
-            }
-        }
-
-        // Add last row that might be incomplete
-        newTiles.addAll(row.tiles.map { it.tile })
-
-        return newTiles.toList()
-    }
-
-    private companion object {
-        const val TAG = "InfiniteGridConsistencyInteractor"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractor.kt
deleted file mode 100644
index 0386a6a..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractor.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import javax.inject.Inject
-
-/** [GridTypeConsistencyInteractor] implementation that doesn't do any changes to tiles. */
-@SysUISingleton
-class NoopGridConsistencyInteractor @Inject constructor() : GridTypeConsistencyInteractor {
-    override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> = tiles
-}
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 9a2315b..1f8a24a1 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
@@ -161,9 +161,9 @@
 @Composable
 fun Modifier.dragAndDropTileSource(
     sizedTile: SizedTile<EditTileViewModel>,
+    dragAndDropState: DragAndDropState,
     onTap: (TileSpec) -> Unit,
-    onDoubleTap: (TileSpec) -> Unit,
-    dragAndDropState: DragAndDropState
+    onDoubleTap: (TileSpec) -> Unit = {},
 ): Modifier {
     val state by rememberUpdatedState(dragAndDropState)
     return dragAndDropSource {
@@ -181,11 +181,11 @@
                         ClipData(
                             QsDragAndDrop.CLIPDATA_LABEL,
                             arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE),
-                            ClipData.Item(sizedTile.tile.tileSpec.spec)
+                            ClipData.Item(sizedTile.tile.tileSpec.spec),
                         )
                     )
                 )
-            }
+            },
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index fde40da..f4acbec 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -25,6 +25,8 @@
 import androidx.compose.ui.util.fastMap
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.TileLazyGrid
 import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel
 
 @Composable
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
deleted file mode 100644
index afd47a7..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ /dev/null
@@ -1,926 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:OptIn(ExperimentalFoundationApi::class)
-
-package com.android.systemui.qs.panels.ui.compose
-
-import android.content.res.Resources
-import android.graphics.drawable.Animatable
-import android.service.quicksettings.Tile.STATE_ACTIVE
-import android.service.quicksettings.Tile.STATE_INACTIVE
-import android.text.TextUtils
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.animateDpAsState
-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.background
-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
-import androidx.compose.foundation.layout.Box
-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.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.LazyGridItemScope
-import androidx.compose.foundation.lazy.grid.LazyGridScope
-import androidx.compose.foundation.lazy.grid.LazyGridState
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.rememberLazyGridState
-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.ReadOnlyComposable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.clearAndSetSemantics
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.onClick
-import androidx.compose.ui.semantics.role
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
-import androidx.compose.ui.semantics.toggleableState
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.util.fastMap
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.compose.animation.Expandable
-import com.android.compose.modifiers.background
-import com.android.compose.modifiers.thenIf
-import com.android.systemui.animation.Expandable
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.ui.compose.Icon
-import com.android.systemui.common.ui.compose.load
-import com.android.systemui.compose.modifiers.sysuiResTag
-import com.android.systemui.plugins.qs.QSTile
-import com.android.systemui.qs.panels.shared.model.SizedTile
-import com.android.systemui.qs.panels.shared.model.SizedTileImpl
-import com.android.systemui.qs.panels.ui.compose.TileDefaults.longPressLabel
-import com.android.systemui.qs.panels.ui.model.GridCell
-import com.android.systemui.qs.panels.ui.model.SpacerGridCell
-import com.android.systemui.qs.panels.ui.model.TileGridCell
-import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState
-import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
-import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.toUiState
-import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.qs.shared.model.groupAndSort
-import com.android.systemui.qs.tileimpl.QSTileImpl
-import com.android.systemui.res.R
-import java.util.function.Supplier
-import kotlinx.coroutines.delay
-
-object TileType
-
-private const val TEST_TAG_SMALL = "qs_tile_small"
-private const val TEST_TAG_LARGE = "qs_tile_large"
-private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target"
-
-@Composable
-fun Tile(tile: TileViewModel, iconOnly: Boolean, showLabels: Boolean = false, modifier: Modifier) {
-    val state by tile.state.collectAsStateWithLifecycle(tile.currentState)
-    val resources = resources()
-    val uiState = remember(state, resources) { state.toUiState(resources) }
-    val colors = TileDefaults.getColorForState(uiState)
-
-    // TODO(b/361789146): Draw the shapes instead of clipping
-    val tileShape = TileDefaults.animateTileShape(uiState.state)
-
-    TileContainer(
-        colors = colors,
-        showLabels = showLabels,
-        label = uiState.label,
-        iconOnly = iconOnly,
-        shape = tileShape,
-        clickEnabled = true,
-        onClick = tile::onClick,
-        onLongClick = tile::onLongClick,
-        modifier = modifier.height(tileHeight()),
-        uiState = uiState,
-    ) {
-        val icon = getTileIcon(icon = uiState.icon)
-        if (iconOnly) {
-            TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center))
-        } else {
-            val iconShape = TileDefaults.animateIconShape(uiState.state)
-            LargeTileContent(
-                label = uiState.label,
-                secondaryLabel = uiState.secondaryLabel,
-                icon = icon,
-                colors = colors,
-                iconShape = iconShape,
-                toggleClickSupported = state.handlesSecondaryClick,
-                onClick = {
-                    if (state.handlesSecondaryClick) {
-                        tile.onSecondaryClick()
-                    }
-                },
-                onLongClick = { tile.onLongClick(it) },
-                accessibilityUiState = uiState.accessibilityUiState,
-            )
-        }
-    }
-}
-
-@Composable
-private fun TileContainer(
-    colors: TileColors,
-    showLabels: Boolean,
-    label: String,
-    iconOnly: Boolean,
-    shape: Shape,
-    clickEnabled: Boolean = false,
-    onClick: (Expandable) -> Unit = {},
-    onLongClick: (Expandable) -> Unit = {},
-    modifier: Modifier = Modifier,
-    uiState: TileUiState? = null,
-    content: @Composable BoxScope.(Expandable) -> Unit,
-) {
-    Column(
-        horizontalAlignment = Alignment.CenterHorizontally,
-        verticalArrangement =
-            spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin), Alignment.Top),
-        modifier = modifier,
-    ) {
-        val backgroundColor =
-            if (iconOnly || uiState?.handlesSecondaryClick != true) {
-                colors.iconBackground
-            } else {
-                colors.background
-            }
-        Expandable(
-            color = backgroundColor,
-            shape = shape,
-            modifier = Modifier.height(tileHeight()).clip(shape),
-        ) {
-            val longPressLabel = longPressLabel()
-            Box(
-                modifier =
-                    Modifier.fillMaxSize()
-                        .thenIf(clickEnabled) {
-                            Modifier.combinedClickable(
-                                onClick = { onClick(it) },
-                                onLongClick = { onLongClick(it) },
-                                onClickLabel = uiState?.accessibilityUiState?.clickLabel,
-                                onLongClickLabel = longPressLabel,
-                            )
-                        }
-                        .thenIf(uiState != null) {
-                            uiState as TileUiState
-                            Modifier.semantics {
-                                    role = uiState.accessibilityUiState.accessibilityRole
-                                    if (
-                                        uiState.accessibilityUiState.accessibilityRole ==
-                                            Role.Switch
-                                    ) {
-                                        uiState.accessibilityUiState.toggleableState?.let {
-                                            toggleableState = it
-                                        }
-                                    }
-                                    stateDescription = uiState.accessibilityUiState.stateDescription
-                                }
-                                .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE)
-                                .thenIf(iconOnly) {
-                                    Modifier.semantics {
-                                        contentDescription =
-                                            uiState.accessibilityUiState.contentDescription
-                                    }
-                                }
-                        }
-                        .tilePadding()
-            ) {
-                content(it)
-            }
-        }
-
-        if (showLabels && iconOnly) {
-            Text(
-                label,
-                maxLines = 2,
-                color = colors.label,
-                overflow = TextOverflow.Ellipsis,
-                textAlign = TextAlign.Center,
-            )
-        }
-    }
-}
-
-@Composable
-private fun LargeTileContent(
-    label: String,
-    secondaryLabel: String?,
-    icon: Icon,
-    colors: TileColors,
-    iconShape: Shape,
-    accessibilityUiState: AccessibilityUiState? = null,
-    toggleClickSupported: Boolean = false,
-    onClick: () -> Unit = {},
-    onLongClick: () -> Unit = {},
-) {
-    Row(
-        verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = tileHorizontalArrangement(),
-    ) {
-        // Icon
-        val longPressLabel = longPressLabel()
-        Box(
-            modifier =
-                Modifier.size(TileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) {
-                    Modifier.clip(iconShape)
-                        .background(colors.iconBackground, { 1f })
-                        .combinedClickable(
-                            onClick = onClick,
-                            onLongClick = onLongClick,
-                            onLongClickLabel = longPressLabel,
-                        )
-                        .thenIf(accessibilityUiState != null) {
-                            accessibilityUiState as AccessibilityUiState
-                            Modifier.semantics {
-                                    contentDescription = accessibilityUiState.contentDescription
-                                    stateDescription = accessibilityUiState.stateDescription
-                                    accessibilityUiState.toggleableState?.let {
-                                        toggleableState = it
-                                    }
-                                    role = Role.Switch
-                                }
-                                .sysuiResTag(TEST_TAG_TOGGLE)
-                        }
-                }
-        ) {
-            TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center))
-        }
-
-        // Labels
-        Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
-            Text(label, color = colors.label, modifier = Modifier.tileMarquee())
-            if (!TextUtils.isEmpty(secondaryLabel)) {
-                Text(
-                    secondaryLabel ?: "",
-                    color = colors.secondaryLabel,
-                    modifier =
-                        Modifier.tileMarquee().thenIf(
-                            accessibilityUiState
-                                ?.stateDescription
-                                ?.contains(secondaryLabel ?: "") == true
-                        ) {
-                            Modifier.clearAndSetSemantics {}
-                        },
-                )
-            }
-        }
-    }
-}
-
-private fun Modifier.tileMarquee(): Modifier {
-    return basicMarquee(iterations = 1, initialDelayMillis = 200)
-}
-
-@Composable
-fun TileLazyGrid(
-    modifier: Modifier = Modifier,
-    state: LazyGridState = rememberLazyGridState(),
-    columns: GridCells,
-    content: LazyGridScope.() -> Unit,
-) {
-    LazyVerticalGrid(
-        state = state,
-        columns = columns,
-        verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
-        horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
-        modifier = modifier,
-        content = content,
-    )
-}
-
-@Composable
-fun DefaultEditTileGrid(
-    currentListState: EditTileListState,
-    otherTiles: List<SizedTile<EditTileViewModel>>,
-    columns: Int,
-    modifier: Modifier,
-    onAddTile: (TileSpec, Int) -> Unit,
-    onRemoveTile: (TileSpec) -> Unit,
-    onSetTiles: (List<TileSpec>) -> Unit,
-    onResize: (TileSpec) -> Unit,
-) {
-    val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
-        onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
-    }
-    val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical)
-
-    CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
-        Column(
-            verticalArrangement =
-                spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
-            modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()),
-        ) {
-            AnimatedContent(
-                targetState = currentListState.dragInProgress,
-                modifier = Modifier.wrapContentSize(),
-            ) { dragIsInProgress ->
-                EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) {
-                    if (dragIsInProgress) {
-                        RemoveTileTarget()
-                    } else {
-                        Text(text = "Hold and drag to rearrange tiles.")
-                    }
-                }
-            }
-
-            CurrentTilesGrid(
-                currentListState,
-                columns,
-                tilePadding,
-                onRemoveTile,
-                onResize,
-                onSetTiles,
-            )
-
-            // Hide available tiles when dragging
-            AnimatedVisibility(
-                visible = !currentListState.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,
-                        currentListState,
-                    )
-                }
-            }
-
-            // Drop zone to remove tiles dragged out of the tile grid
-            Spacer(
-                modifier =
-                    Modifier.fillMaxWidth()
-                        .weight(1f)
-                        .dragAndDropRemoveZone(currentListState, onRemoveTile)
-            )
-        }
-    }
-}
-
-@Composable
-private fun EditGridHeader(
-    modifier: Modifier = Modifier,
-    content: @Composable BoxScope.() -> Unit,
-) {
-    CompositionLocalProvider(
-        LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f)
-    ) {
-        Box(
-            contentAlignment = Alignment.Center,
-            modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight),
-        ) {
-            content()
-        }
-    }
-}
-
-@Composable
-private fun RemoveTileTarget() {
-    Row(
-        verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = tileHorizontalArrangement(),
-        modifier =
-            Modifier.fillMaxHeight()
-                .border(1.dp, LocalContentColor.current, shape = CircleShape)
-                .padding(10.dp),
-    ) {
-        Icon(imageVector = Icons.Default.Clear, contentDescription = null)
-        Text(text = "Remove")
-    }
-}
-
-@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(
-    listState: EditTileListState,
-    columns: Int,
-    tilePadding: Dp,
-    onClick: (TileSpec) -> Unit,
-    onResize: (TileSpec) -> Unit,
-    onSetTiles: (List<TileSpec>) -> Unit,
-) {
-    val currentListState by rememberUpdatedState(listState)
-
-    CurrentTilesContainer {
-        val tileHeight = tileHeight()
-        val totalRows = listState.tiles.lastOrNull()?.row ?: 0
-        val totalHeight = gridHeight(totalRows + 1, tileHeight, tilePadding)
-        val gridState = rememberLazyGridState()
-        var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
-
-        TileLazyGrid(
-            state = gridState,
-            modifier =
-                Modifier.height(totalHeight)
-                    .dragAndDropTileList(gridState, gridContentOffset, listState) {
-                        onSetTiles(currentListState.tileSpecs())
-                    }
-                    .onGloballyPositioned { coordinates ->
-                        gridContentOffset = coordinates.positionInRoot()
-                    }
-                    .testTag(CURRENT_TILES_GRID_TEST_TAG),
-            columns = GridCells.Fixed(columns),
-        ) {
-            editTiles(
-                listState.tiles,
-                ClickAction.REMOVE,
-                onClick,
-                listState,
-                onResize = onResize,
-                indicatePosition = true,
-            )
-        }
-    }
-}
-
-@Composable
-private fun AvailableTileGrid(
-    tiles: List<SizedTile<EditTileViewModel>>,
-    columns: Int,
-    tilePadding: Dp,
-    onClick: (TileSpec) -> Unit,
-    dragAndDropState: DragAndDropState,
-) {
-    val availableTileHeight = tileHeight(true)
-    val availableGridHeight = gridHeight(tiles.size, availableTileHeight, columns, tilePadding)
-
-    // Available tiles aren't visible during drag and drop, so the row isn't needed
-    val groupedTiles =
-        remember(tiles.fastMap { it.tile.category }, tiles.fastMap { it.tile.label }) {
-            groupAndSort(tiles.fastMap { TileGridCell(it, 0) })
-        }
-    val labelColors = TileDefaults.inactiveTileColors()
-    // Available tiles
-    TileLazyGrid(
-        modifier = Modifier.height(availableGridHeight).testTag(AVAILABLE_TILES_GRID_TEST_TAG),
-        columns = GridCells.Fixed(columns),
-    ) {
-        groupedTiles.forEach { category, tiles ->
-            stickyHeader {
-                Text(
-                    text = category.label.load() ?: "",
-                    fontSize = 20.sp,
-                    color = labelColors.label,
-                    modifier =
-                        Modifier.background(Color.Black)
-                            .padding(start = 16.dp, bottom = 8.dp, top = 8.dp),
-                )
-            }
-            editTiles(
-                tiles,
-                ClickAction.ADD,
-                onClick,
-                dragAndDropState = dragAndDropState,
-                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 GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any {
-    return if (this is TileGridCell && !dragAndDropState.isMoving(tile.tileSpec)) {
-        key
-    } else {
-        index
-    }
-}
-
-fun LazyGridScope.editTiles(
-    cells: List<GridCell>,
-    clickAction: ClickAction,
-    onClick: (TileSpec) -> Unit,
-    dragAndDropState: DragAndDropState,
-    onResize: (TileSpec) -> Unit = {},
-    showLabels: Boolean = false,
-    indicatePosition: Boolean = false,
-) {
-    items(
-        count = cells.size,
-        key = { cells[it].key(it, dragAndDropState) },
-        span = { cells[it].span },
-        contentType = { TileType },
-    ) { index ->
-        when (val cell = cells[index]) {
-            is TileGridCell ->
-                if (dragAndDropState.isMoving(cell.tile.tileSpec)) {
-                    // If the tile is being moved, replace it with a visible spacer
-                    SpacerGridCell(
-                        Modifier.background(
-                                color = MaterialTheme.colorScheme.secondary,
-                                alpha = { EditModeTileDefaults.PLACEHOLDER_ALPHA },
-                                shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
-                            )
-                            .animateItem()
-                    )
-                } else {
-                    TileGridCell(
-                        cell = cell,
-                        index = index,
-                        dragAndDropState = dragAndDropState,
-                        clickAction = clickAction,
-                        onClick = onClick,
-                        onResize = onResize,
-                        showLabels = showLabels,
-                        indicatePosition = indicatePosition,
-                    )
-                }
-            is SpacerGridCell -> SpacerGridCell()
-        }
-    }
-}
-
-@Composable
-private fun LazyGridItemScope.TileGridCell(
-    cell: TileGridCell,
-    index: Int,
-    dragAndDropState: DragAndDropState,
-    clickAction: ClickAction,
-    onClick: (TileSpec) -> Unit,
-    onResize: (TileSpec) -> Unit = {},
-    showLabels: Boolean = false,
-    indicatePosition: Boolean = false,
-) {
-    val tileHeight = tileHeight(cell.isIcon && showLabels)
-    val onClickActionName =
-        when (clickAction) {
-            ClickAction.ADD -> stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
-            ClickAction.REMOVE ->
-                stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
-        }
-    val stateDescription =
-        if (indicatePosition) {
-            stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
-        } else {
-            ""
-        }
-    EditTile(
-        tileViewModel = cell.tile,
-        iconOnly = cell.isIcon,
-        showLabels = showLabels,
-        modifier =
-            Modifier.height(tileHeight)
-                .animateItem()
-                .semantics(mergeDescendants = true) {
-                    onClick(onClickActionName) { false }
-                    this.stateDescription = stateDescription
-                }
-                .dragAndDropTileSource(
-                    SizedTileImpl(cell.tile, cell.width),
-                    onClick,
-                    onResize,
-                    dragAndDropState,
-                ),
-    )
-}
-
-@Composable
-private fun SpacerGridCell(modifier: Modifier = Modifier) {
-    // By default, spacers are invisible and exist purely to catch drag movements
-    Box(modifier.height(tileHeight()).fillMaxWidth().tilePadding())
-}
-
-@Composable
-fun EditTile(
-    tileViewModel: EditTileViewModel,
-    iconOnly: Boolean,
-    showLabels: Boolean,
-    modifier: Modifier = Modifier,
-) {
-    val label = tileViewModel.label.text
-    val colors = TileDefaults.inactiveTileColors()
-
-    TileContainer(
-        colors = colors,
-        showLabels = showLabels,
-        label = label,
-        iconOnly = iconOnly,
-        shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
-        modifier = modifier,
-    ) {
-        if (iconOnly) {
-            TileIcon(
-                icon = tileViewModel.icon,
-                color = colors.icon,
-                modifier = Modifier.align(Alignment.Center),
-            )
-        } else {
-            LargeTileContent(
-                label = label,
-                secondaryLabel = tileViewModel.appName?.text,
-                icon = tileViewModel.icon,
-                colors = colors,
-                iconShape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
-            )
-        }
-    }
-}
-
-enum class ClickAction {
-    ADD,
-    REMOVE,
-}
-
-@Composable
-private fun getTileIcon(icon: Supplier<QSTile.Icon?>): Icon {
-    val context = LocalContext.current
-    return icon.get()?.let {
-        if (it is QSTileImpl.ResourceIcon) {
-            Icon.Resource(it.resId, null)
-        } else {
-            Icon.Loaded(it.getDrawable(context), null)
-        }
-    } ?: Icon.Resource(R.drawable.ic_error_outline, null)
-}
-
-@OptIn(ExperimentalAnimationGraphicsApi::class)
-@Composable
-private fun TileIcon(
-    icon: Icon,
-    color: Color,
-    animateToEnd: Boolean = false,
-    modifier: Modifier = Modifier,
-) {
-    val iconModifier = modifier.size(TileDefaults.IconSize)
-    val context = LocalContext.current
-    val loadedDrawable =
-        remember(icon, context) {
-            when (icon) {
-                is Icon.Loaded -> icon.drawable
-                is Icon.Resource -> context.getDrawable(icon.res)
-            }
-        }
-    if (loadedDrawable !is Animatable) {
-        Icon(icon = icon, tint = color, modifier = iconModifier)
-    } else if (icon is Icon.Resource) {
-        val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
-        val painter =
-            if (animateToEnd) {
-                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
-            } else {
-                var atEnd by remember(icon.res) { mutableStateOf(false) }
-                LaunchedEffect(key1 = icon.res) {
-                    delay(350)
-                    atEnd = true
-                }
-                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
-            }
-        Image(
-            painter = painter,
-            contentDescription = icon.contentDescription?.load(),
-            colorFilter = ColorFilter.tint(color = color),
-            modifier = iconModifier,
-        )
-    }
-}
-
-private fun Modifier.tilePadding(): Modifier {
-    return padding(TileDefaults.TilePadding)
-}
-
-private fun tileHorizontalArrangement(): Arrangement.Horizontal {
-    return spacedBy(space = TileDefaults.TileArrangementPadding, alignment = Alignment.Start)
-}
-
-@Composable
-fun tileHeight(iconWithLabel: Boolean = false): Dp {
-    return if (iconWithLabel) {
-        TileDefaults.IconTileWithLabelHeight
-    } else {
-        TileDefaults.TileHeight
-    }
-}
-
-private data class TileColors(
-    val background: Color,
-    val iconBackground: Color,
-    val label: Color,
-    val secondaryLabel: Color,
-    val icon: Color,
-)
-
-private object EditModeTileDefaults {
-    const val PLACEHOLDER_ALPHA = .3f
-    val EditGridHeaderHeight = 60.dp
-}
-
-private object TileDefaults {
-    val InactiveCornerRadius = 50.dp
-    val ActiveIconCornerRadius = 16.dp
-    val ActiveTileCornerRadius = 24.dp
-
-    val ToggleTargetSize = 56.dp
-    val IconSize = 24.dp
-
-    val TilePadding = 8.dp
-    val TileArrangementPadding = 6.dp
-
-    val TileHeight = 72.dp
-    val IconTileWithLabelHeight = 140.dp
-
-    @Composable fun longPressLabel() = stringResource(id = R.string.accessibility_long_click_tile)
-
-    /** An active tile without dual target uses the active color as background */
-    @Composable
-    fun activeTileColors(): TileColors =
-        TileColors(
-            background = MaterialTheme.colorScheme.primary,
-            iconBackground = MaterialTheme.colorScheme.primary,
-            label = MaterialTheme.colorScheme.onPrimary,
-            secondaryLabel = MaterialTheme.colorScheme.onPrimary,
-            icon = MaterialTheme.colorScheme.onPrimary,
-        )
-
-    /** An active tile with dual target only show the active color on the icon */
-    @Composable
-    fun activeDualTargetTileColors(): TileColors =
-        TileColors(
-            background = MaterialTheme.colorScheme.surfaceVariant,
-            iconBackground = MaterialTheme.colorScheme.primary,
-            label = MaterialTheme.colorScheme.onSurfaceVariant,
-            secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
-            icon = MaterialTheme.colorScheme.onPrimary,
-        )
-
-    @Composable
-    fun inactiveTileColors(): TileColors =
-        TileColors(
-            background = MaterialTheme.colorScheme.surfaceVariant,
-            iconBackground = MaterialTheme.colorScheme.surfaceVariant,
-            label = MaterialTheme.colorScheme.onSurfaceVariant,
-            secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
-            icon = MaterialTheme.colorScheme.onSurfaceVariant,
-        )
-
-    @Composable
-    fun unavailableTileColors(): TileColors =
-        TileColors(
-            background = MaterialTheme.colorScheme.surface,
-            iconBackground = MaterialTheme.colorScheme.surface,
-            label = MaterialTheme.colorScheme.onSurface,
-            secondaryLabel = MaterialTheme.colorScheme.onSurface,
-            icon = MaterialTheme.colorScheme.onSurface,
-        )
-
-    @Composable
-    fun getColorForState(uiState: TileUiState): TileColors {
-        return when (uiState.state) {
-            STATE_ACTIVE -> {
-                if (uiState.handlesSecondaryClick) {
-                    activeDualTargetTileColors()
-                } else {
-                    activeTileColors()
-                }
-            }
-            STATE_INACTIVE -> inactiveTileColors()
-            else -> unavailableTileColors()
-        }
-    }
-
-    @Composable
-    fun animateIconShape(state: Int): Shape {
-        return animateShape(
-            state = state,
-            activeCornerRadius = ActiveIconCornerRadius,
-            label = "QSTileCornerRadius",
-        )
-    }
-
-    @Composable
-    fun animateTileShape(state: Int): Shape {
-        return animateShape(
-            state = state,
-            activeCornerRadius = ActiveTileCornerRadius,
-            label = "QSTileIconCornerRadius",
-        )
-    }
-
-    @Composable
-    fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape {
-        val animatedCornerRadius by
-            animateDpAsState(
-                targetValue =
-                    if (state == STATE_ACTIVE) {
-                        activeCornerRadius
-                    } else {
-                        InactiveCornerRadius
-                    },
-                label = label,
-            )
-        return RoundedCornerShape(animatedCornerRadius)
-    }
-}
-
-private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
-private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
-
-/**
- * A composable function that returns the [Resources]. It will be recomposed when [Configuration]
- * gets updated.
- */
-@Composable
-@ReadOnlyComposable
-private fun resources(): Resources {
-    LocalConfiguration.current
-    return LocalContext.current.resources
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
new file mode 100644
index 0000000..aeb6031
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose.infinitegrid
+
+import android.graphics.drawable.Animatable
+import android.text.TextUtils
+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.Image
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.toggleableState
+import androidx.compose.ui.unit.dp
+import com.android.compose.modifiers.background
+import com.android.compose.modifiers.thenIf
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.common.ui.compose.load
+import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel
+import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState
+import com.android.systemui.res.R
+import kotlinx.coroutines.delay
+
+private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target"
+
+@Composable
+fun LargeTileContent(
+    label: String,
+    secondaryLabel: String?,
+    icon: Icon,
+    colors: TileColors,
+    accessibilityUiState: AccessibilityUiState? = null,
+    toggleClickSupported: Boolean = false,
+    iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius),
+    onClick: () -> Unit = {},
+    onLongClick: () -> Unit = {},
+) {
+    Row(
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = tileHorizontalArrangement(),
+    ) {
+        // Icon
+        val longPressLabel = longPressLabel()
+        Box(
+            modifier =
+                Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) {
+                    Modifier.clip(iconShape)
+                        .background(colors.iconBackground, { 1f })
+                        .combinedClickable(
+                            onClick = onClick,
+                            onLongClick = onLongClick,
+                            onLongClickLabel = longPressLabel,
+                        )
+                        .thenIf(accessibilityUiState != null) {
+                            Modifier.semantics {
+                                    accessibilityUiState as AccessibilityUiState
+                                    contentDescription = accessibilityUiState.contentDescription
+                                    stateDescription = accessibilityUiState.stateDescription
+                                    accessibilityUiState.toggleableState?.let {
+                                        toggleableState = it
+                                    }
+                                    role = Role.Switch
+                                }
+                                .sysuiResTag(TEST_TAG_TOGGLE)
+                        }
+                }
+        ) {
+            SmallTileContent(
+                icon = icon,
+                color = colors.icon,
+                modifier = Modifier.align(Alignment.Center),
+            )
+        }
+
+        // Labels
+        LargeTileLabels(
+            label = label,
+            secondaryLabel = secondaryLabel,
+            colors = colors,
+            accessibilityUiState = accessibilityUiState,
+        )
+    }
+}
+
+@Composable
+private fun LargeTileLabels(
+    label: String,
+    secondaryLabel: String?,
+    colors: TileColors,
+    accessibilityUiState: AccessibilityUiState? = null,
+) {
+    Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
+        Text(label, color = colors.label, modifier = Modifier.tileMarquee())
+        if (!TextUtils.isEmpty(secondaryLabel)) {
+            Text(
+                secondaryLabel ?: "",
+                color = colors.secondaryLabel,
+                modifier =
+                    Modifier.tileMarquee().thenIf(
+                        accessibilityUiState?.stateDescription?.contains(secondaryLabel ?: "") ==
+                            true
+                    ) {
+                        Modifier.clearAndSetSemantics {}
+                    },
+            )
+        }
+    }
+}
+
+@OptIn(ExperimentalAnimationGraphicsApi::class)
+@Composable
+fun SmallTileContent(
+    modifier: Modifier = Modifier,
+    icon: Icon,
+    color: Color,
+    animateToEnd: Boolean = false,
+) {
+    val iconModifier = modifier.size(CommonTileDefaults.IconSize)
+    val context = LocalContext.current
+    val loadedDrawable =
+        remember(icon, context) {
+            when (icon) {
+                is Icon.Loaded -> icon.drawable
+                is Icon.Resource -> context.getDrawable(icon.res)
+            }
+        }
+    if (loadedDrawable !is Animatable) {
+        Icon(icon = icon, tint = color, modifier = iconModifier)
+    } else if (icon is Icon.Resource) {
+        val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+        val painter =
+            if (animateToEnd) {
+                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
+            } else {
+                var atEnd by remember(icon.res) { mutableStateOf(false) }
+                LaunchedEffect(key1 = icon.res) {
+                    delay(350)
+                    atEnd = true
+                }
+                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
+            }
+        Image(
+            painter = painter,
+            contentDescription = icon.contentDescription?.load(),
+            colorFilter = ColorFilter.tint(color = color),
+            modifier = iconModifier,
+        )
+    }
+}
+
+object CommonTileDefaults {
+    val IconSize = 24.dp
+    val ToggleTargetSize = 56.dp
+    val TileHeight = 72.dp
+    val TilePadding = 8.dp
+    val TileArrangementPadding = 6.dp
+    val InactiveCornerRadius = 50.dp
+
+    @Composable fun longPressLabel() = stringResource(id = R.string.accessibility_long_click_tile)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
new file mode 100644
index 0000000..a43b880
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -0,0 +1,503 @@
+/*
+ * 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(ExperimentalFoundationApi::class)
+
+package com.android.systemui.qs.panels.ui.compose.infinitegrid
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.LocalOverscrollConfiguration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+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.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridItemScope
+import androidx.compose.foundation.lazy.grid.LazyGridScope
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+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.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.fastMap
+import com.android.compose.modifiers.background
+import com.android.systemui.common.ui.compose.load
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.DragAndDropState
+import com.android.systemui.qs.panels.ui.compose.EditTileListState
+import com.android.systemui.qs.panels.ui.compose.dragAndDropRemoveZone
+import com.android.systemui.qs.panels.ui.compose.dragAndDropTileList
+import com.android.systemui.qs.panels.ui.compose.dragAndDropTileSource
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
+import com.android.systemui.qs.panels.ui.model.GridCell
+import com.android.systemui.qs.panels.ui.model.SpacerGridCell
+import com.android.systemui.qs.panels.ui.model.TileGridCell
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.groupAndSort
+import com.android.systemui.res.R
+
+object TileType
+
+@Composable
+fun DefaultEditTileGrid(
+    currentListState: EditTileListState,
+    otherTiles: List<SizedTile<EditTileViewModel>>,
+    columns: Int,
+    modifier: Modifier,
+    onAddTile: (TileSpec, Int) -> Unit,
+    onRemoveTile: (TileSpec) -> Unit,
+    onSetTiles: (List<TileSpec>) -> Unit,
+    onResize: (TileSpec) -> Unit,
+) {
+    val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
+        onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
+    }
+
+    CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
+        Column(
+            verticalArrangement =
+                spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
+            modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()),
+        ) {
+            AnimatedContent(
+                targetState = currentListState.dragInProgress,
+                modifier = Modifier.wrapContentSize(),
+                label = "",
+            ) { dragIsInProgress ->
+                EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) {
+                    if (dragIsInProgress) {
+                        RemoveTileTarget()
+                    } else {
+                        Text(text = "Hold and drag to rearrange tiles.")
+                    }
+                }
+            }
+
+            CurrentTilesGrid(currentListState, columns, onRemoveTile, onResize, onSetTiles)
+
+            // Hide available tiles when dragging
+            AnimatedVisibility(
+                visible = !currentListState.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, addTileToEnd, currentListState)
+                }
+            }
+
+            // Drop zone to remove tiles dragged out of the tile grid
+            Spacer(
+                modifier =
+                    Modifier.fillMaxWidth()
+                        .weight(1f)
+                        .dragAndDropRemoveZone(currentListState, onRemoveTile)
+            )
+        }
+    }
+}
+
+@Composable
+private fun EditGridHeader(
+    modifier: Modifier = Modifier,
+    content: @Composable BoxScope.() -> Unit,
+) {
+    CompositionLocalProvider(
+        LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f)
+    ) {
+        Box(
+            contentAlignment = Alignment.Center,
+            modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight),
+        ) {
+            content()
+        }
+    }
+}
+
+@Composable
+private fun RemoveTileTarget() {
+    Row(
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = tileHorizontalArrangement(),
+        modifier =
+            Modifier.fillMaxHeight()
+                .border(1.dp, LocalContentColor.current, shape = CircleShape)
+                .padding(10.dp),
+    ) {
+        Icon(imageVector = Icons.Default.Clear, contentDescription = null)
+        Text(text = "Remove")
+    }
+}
+
+@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(
+    listState: EditTileListState,
+    columns: Int,
+    onClick: (TileSpec) -> Unit,
+    onResize: (TileSpec) -> Unit,
+    onSetTiles: (List<TileSpec>) -> Unit,
+) {
+    val currentListState by rememberUpdatedState(listState)
+    val tilePadding = CommonTileDefaults.TileArrangementPadding
+
+    CurrentTilesContainer {
+        val tileHeight = CommonTileDefaults.TileHeight
+        val totalRows = listState.tiles.lastOrNull()?.row ?: 0
+        val totalHeight = gridHeight(totalRows + 1, tileHeight, tilePadding)
+        val gridState = rememberLazyGridState()
+        var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
+
+        TileLazyGrid(
+            state = gridState,
+            modifier =
+                Modifier.height(totalHeight)
+                    .dragAndDropTileList(gridState, gridContentOffset, listState) {
+                        onSetTiles(currentListState.tileSpecs())
+                    }
+                    .onGloballyPositioned { coordinates ->
+                        gridContentOffset = coordinates.positionInRoot()
+                    }
+                    .testTag(CURRENT_TILES_GRID_TEST_TAG),
+            columns = GridCells.Fixed(columns),
+        ) {
+            EditTiles(listState.tiles, onClick, listState, onResize = onResize)
+        }
+    }
+}
+
+@Composable
+private fun AvailableTileGrid(
+    tiles: List<SizedTile<EditTileViewModel>>,
+    columns: Int,
+    onClick: (TileSpec) -> Unit,
+    dragAndDropState: DragAndDropState,
+) {
+    // Available tiles aren't visible during drag and drop, so the row isn't needed
+    val groupedTiles =
+        remember(tiles.fastMap { it.tile.category }, tiles.fastMap { it.tile.label }) {
+            groupAndSort(tiles.fastMap { TileGridCell(it, 0) })
+        }
+    val labelColors = EditModeTileDefaults.editTileColors()
+
+    // Available tiles
+    Column(
+        verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+        horizontalAlignment = Alignment.Start,
+        modifier =
+            Modifier.fillMaxWidth().wrapContentHeight().testTag(AVAILABLE_TILES_GRID_TEST_TAG),
+    ) {
+        groupedTiles.forEach { (category, tiles) ->
+            Text(
+                text = category.label.load() ?: "",
+                fontSize = 20.sp,
+                color = labelColors.label,
+                modifier =
+                    Modifier.fillMaxWidth()
+                        .background(Color.Black)
+                        .padding(start = 16.dp, bottom = 8.dp, top = 8.dp),
+            )
+            tiles.chunked(columns).forEach { row ->
+                Row(
+                    horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+                    modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Max),
+                ) {
+                    row.forEachIndexed { index, tileGridCell ->
+                        AvailableTileGridCell(
+                            cell = tileGridCell,
+                            index = index,
+                            dragAndDropState = dragAndDropState,
+                            onClick = onClick,
+                            modifier = Modifier.weight(1f).fillMaxHeight(),
+                        )
+                    }
+
+                    // Spacers for incomplete rows
+                    repeat(columns - row.size) { Spacer(modifier = Modifier.weight(1f)) }
+                }
+            }
+        }
+    }
+}
+
+fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp {
+    return ((tileHeight + padding) * rows) - padding
+}
+
+private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any {
+    return when (this) {
+        is TileGridCell -> {
+            if (dragAndDropState.isMoving(tile.tileSpec)) index else key
+        }
+        is SpacerGridCell -> index
+    }
+}
+
+fun LazyGridScope.EditTiles(
+    cells: List<GridCell>,
+    onClick: (TileSpec) -> Unit,
+    dragAndDropState: DragAndDropState,
+    onResize: (TileSpec) -> Unit = {},
+) {
+    items(
+        count = cells.size,
+        key = { cells[it].key(it, dragAndDropState) },
+        span = { cells[it].span },
+        contentType = { TileType },
+    ) { index ->
+        when (val cell = cells[index]) {
+            is TileGridCell ->
+                if (dragAndDropState.isMoving(cell.tile.tileSpec)) {
+                    // If the tile is being moved, replace it with a visible spacer
+                    SpacerGridCell(
+                        Modifier.background(
+                                color = MaterialTheme.colorScheme.secondary,
+                                alpha = { EditModeTileDefaults.PLACEHOLDER_ALPHA },
+                                shape = RoundedCornerShape(InactiveCornerRadius),
+                            )
+                            .animateItem()
+                    )
+                } else {
+                    TileGridCell(
+                        cell = cell,
+                        index = index,
+                        dragAndDropState = dragAndDropState,
+                        onClick = onClick,
+                        onResize = onResize,
+                    )
+                }
+            is SpacerGridCell -> SpacerGridCell()
+        }
+    }
+}
+
+@Composable
+private fun LazyGridItemScope.TileGridCell(
+    cell: TileGridCell,
+    index: Int,
+    dragAndDropState: DragAndDropState,
+    onClick: (TileSpec) -> Unit,
+    onResize: (TileSpec) -> Unit = {},
+) {
+    val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
+    val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+
+    EditTile(
+        tileViewModel = cell.tile,
+        iconOnly = cell.isIcon,
+        modifier =
+            Modifier.animateItem()
+                .semantics(mergeDescendants = true) {
+                    onClick(onClickActionName) { false }
+                    this.stateDescription = stateDescription
+                }
+                .dragAndDropTileSource(
+                    SizedTileImpl(cell.tile, cell.width),
+                    dragAndDropState,
+                    onClick,
+                    onResize,
+                ),
+    )
+}
+
+@Composable
+private fun AvailableTileGridCell(
+    cell: TileGridCell,
+    index: Int,
+    dragAndDropState: DragAndDropState,
+    modifier: Modifier = Modifier,
+    onClick: (TileSpec) -> Unit,
+) {
+    val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
+    val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+    val colors = EditModeTileDefaults.editTileColors()
+
+    // Displays the tile as an icon tile with the label underneath
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = spacedBy(CommonTileDefaults.TilePadding, Alignment.Top),
+        modifier = modifier,
+    ) {
+        EditTile(
+            tileViewModel = cell.tile,
+            iconOnly = true,
+            colors = colors,
+            modifier =
+                Modifier.semantics(mergeDescendants = true) {
+                        onClick(onClickActionName) { false }
+                        this.stateDescription = stateDescription
+                    }
+                    .dragAndDropTileSource(
+                        SizedTileImpl(cell.tile, cell.width),
+                        dragAndDropState,
+                        onTap = onClick,
+                    ),
+        )
+        Box(Modifier.fillMaxSize()) {
+            Text(
+                cell.tile.label.text,
+                maxLines = 2,
+                color = colors.label,
+                overflow = TextOverflow.Ellipsis,
+                textAlign = TextAlign.Center,
+                modifier = Modifier.align(Alignment.Center),
+            )
+        }
+    }
+}
+
+@Composable
+private fun SpacerGridCell(modifier: Modifier = Modifier) {
+    // By default, spacers are invisible and exist purely to catch drag movements
+    Box(modifier.height(CommonTileDefaults.TileHeight).fillMaxWidth().tilePadding())
+}
+
+@Composable
+fun EditTile(
+    tileViewModel: EditTileViewModel,
+    iconOnly: Boolean,
+    modifier: Modifier = Modifier,
+    colors: TileColors = EditModeTileDefaults.editTileColors(),
+) {
+    EditTileContainer(colors = colors, modifier = modifier) {
+        if (iconOnly) {
+            SmallTileContent(
+                icon = tileViewModel.icon,
+                color = colors.icon,
+                modifier = Modifier.align(Alignment.Center),
+            )
+        } else {
+            LargeTileContent(
+                label = tileViewModel.label.text,
+                secondaryLabel = tileViewModel.appName?.text,
+                icon = tileViewModel.icon,
+                colors = colors,
+            )
+        }
+    }
+}
+
+@Composable
+private fun EditTileContainer(
+    colors: TileColors,
+    modifier: Modifier = Modifier,
+    content: @Composable BoxScope.() -> Unit,
+) {
+    Box(
+        modifier =
+            modifier
+                .height(CommonTileDefaults.TileHeight)
+                .fillMaxWidth()
+                .drawBehind {
+                    drawRoundRect(
+                        SolidColor(colors.background),
+                        cornerRadius = CornerRadius(InactiveCornerRadius.toPx()),
+                    )
+                }
+                .tilePadding(),
+        content = content,
+    )
+}
+
+private object EditModeTileDefaults {
+    const val PLACEHOLDER_ALPHA = .3f
+    val EditGridHeaderHeight = 60.dp
+
+    @Composable
+    fun editTileColors(): TileColors =
+        TileColors(
+            background = MaterialTheme.colorScheme.surfaceVariant,
+            iconBackground = MaterialTheme.colorScheme.surfaceVariant,
+            label = MaterialTheme.colorScheme.onSurfaceVariant,
+            secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
+            icon = MaterialTheme.colorScheme.onSurfaceVariant,
+        )
+}
+
+private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
+private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
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/infinitegrid/InfiniteGridLayout.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index c75b601..f96c27d 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/infinitegrid/InfiniteGridLayout.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.panels.ui.compose
+package com.android.systemui.qs.panels.ui.compose.infinitegrid
 
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridItemSpan
@@ -26,6 +26,8 @@
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
+import com.android.systemui.qs.panels.ui.compose.rememberEditListState
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
@@ -61,7 +63,7 @@
                 Tile(
                     tile = sizedTiles[index].tile,
                     iconOnly = iconTilesViewModel.isIconTile(sizedTiles[index].tile.spec),
-                    modifier = Modifier
+                    modifier = Modifier,
                 )
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
new file mode 100644
index 0000000..aa6c08e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -0,0 +1,329 @@
+/*
+ * 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(ExperimentalFoundationApi::class)
+
+package com.android.systemui.qs.panels.ui.compose.infinitegrid
+
+import android.content.res.Resources
+import android.service.quicksettings.Tile.STATE_ACTIVE
+import android.service.quicksettings.Tile.STATE_INACTIVE
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridScope
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.toggleableState
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.Expandable
+import com.android.compose.modifiers.thenIf
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel
+import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
+import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.toUiState
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.res.R
+import java.util.function.Supplier
+
+private const val TEST_TAG_SMALL = "qs_tile_small"
+private const val TEST_TAG_LARGE = "qs_tile_large"
+
+@Composable
+fun TileLazyGrid(
+    columns: GridCells,
+    modifier: Modifier = Modifier,
+    state: LazyGridState = rememberLazyGridState(),
+    content: LazyGridScope.() -> Unit,
+) {
+    LazyVerticalGrid(
+        state = state,
+        columns = columns,
+        verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+        horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding),
+        modifier = modifier,
+        content = content,
+    )
+}
+
+@Composable
+fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) {
+    val state by tile.state.collectAsStateWithLifecycle(tile.currentState)
+    val resources = resources()
+    val uiState = remember(state, resources) { state.toUiState(resources) }
+    val colors = TileDefaults.getColorForState(uiState)
+
+    // TODO(b/361789146): Draw the shapes instead of clipping
+    val tileShape = TileDefaults.animateTileShape(uiState.state)
+
+    TileContainer(
+        color =
+            if (iconOnly || !uiState.handlesSecondaryClick) {
+                colors.iconBackground
+            } else {
+                colors.background
+            },
+        shape = tileShape,
+        iconOnly = iconOnly,
+        onClick = tile::onClick,
+        onLongClick = tile::onLongClick,
+        uiState = uiState,
+        modifier = modifier,
+    ) { expandable ->
+        val icon = getTileIcon(icon = uiState.icon)
+        if (iconOnly) {
+            SmallTileContent(
+                icon = icon,
+                color = colors.icon,
+                modifier = Modifier.align(Alignment.Center),
+            )
+        } else {
+            val iconShape = TileDefaults.animateIconShape(uiState.state)
+            LargeTileContent(
+                label = uiState.label,
+                secondaryLabel = uiState.secondaryLabel,
+                icon = icon,
+                colors = colors,
+                iconShape = iconShape,
+                toggleClickSupported = state.handlesSecondaryClick,
+                onClick = {
+                    if (state.handlesSecondaryClick) {
+                        tile.onSecondaryClick()
+                    }
+                },
+                onLongClick = { tile.onLongClick(expandable) },
+            )
+        }
+    }
+}
+
+@Composable
+private fun TileContainer(
+    color: Color,
+    shape: Shape,
+    iconOnly: Boolean,
+    uiState: TileUiState,
+    modifier: Modifier = Modifier,
+    onClick: (Expandable) -> Unit = {},
+    onLongClick: (Expandable) -> Unit = {},
+    content: @Composable BoxScope.(Expandable) -> Unit,
+) {
+    Expandable(color = color, shape = shape, modifier = modifier.clip(shape)) {
+        val longPressLabel = longPressLabel()
+        Box(
+            modifier =
+                Modifier.height(CommonTileDefaults.TileHeight)
+                    .fillMaxWidth()
+                    .combinedClickable(
+                        onClick = { onClick(it) },
+                        onLongClick = { onLongClick(it) },
+                        onClickLabel = uiState.accessibilityUiState.clickLabel,
+                        onLongClickLabel = longPressLabel,
+                    )
+                    .semantics {
+                        role = uiState.accessibilityUiState.accessibilityRole
+                        if (uiState.accessibilityUiState.accessibilityRole == Role.Switch) {
+                            uiState.accessibilityUiState.toggleableState?.let {
+                                toggleableState = it
+                            }
+                        }
+                        stateDescription = uiState.accessibilityUiState.stateDescription
+                    }
+                    .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE)
+                    .thenIf(iconOnly) {
+                        Modifier.semantics {
+                            contentDescription = uiState.accessibilityUiState.contentDescription
+                        }
+                    }
+                    .tilePadding()
+        ) {
+            content(it)
+        }
+    }
+}
+
+@Composable
+private fun getTileIcon(icon: Supplier<QSTile.Icon?>): Icon {
+    val context = LocalContext.current
+    return icon.get()?.let {
+        if (it is QSTileImpl.ResourceIcon) {
+            Icon.Resource(it.resId, null)
+        } else {
+            Icon.Loaded(it.getDrawable(context), null)
+        }
+    } ?: Icon.Resource(R.drawable.ic_error_outline, null)
+}
+
+fun tileHorizontalArrangement(): Arrangement.Horizontal {
+    return spacedBy(space = CommonTileDefaults.TileArrangementPadding, alignment = Alignment.Start)
+}
+
+fun Modifier.tileMarquee(): Modifier {
+    return basicMarquee(iterations = 1, initialDelayMillis = 200)
+}
+
+fun Modifier.tilePadding(): Modifier {
+    return padding(CommonTileDefaults.TilePadding)
+}
+
+data class TileColors(
+    val background: Color,
+    val iconBackground: Color,
+    val label: Color,
+    val secondaryLabel: Color,
+    val icon: Color,
+)
+
+private object TileDefaults {
+    val ActiveIconCornerRadius = 16.dp
+    val ActiveTileCornerRadius = 24.dp
+
+    /** An active tile without dual target uses the active color as background */
+    @Composable
+    fun activeTileColors(): TileColors =
+        TileColors(
+            background = MaterialTheme.colorScheme.primary,
+            iconBackground = MaterialTheme.colorScheme.primary,
+            label = MaterialTheme.colorScheme.onPrimary,
+            secondaryLabel = MaterialTheme.colorScheme.onPrimary,
+            icon = MaterialTheme.colorScheme.onPrimary,
+        )
+
+    /** An active tile with dual target only show the active color on the icon */
+    @Composable
+    fun activeDualTargetTileColors(): TileColors =
+        TileColors(
+            background = MaterialTheme.colorScheme.surfaceVariant,
+            iconBackground = MaterialTheme.colorScheme.primary,
+            label = MaterialTheme.colorScheme.onSurfaceVariant,
+            secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
+            icon = MaterialTheme.colorScheme.onPrimary,
+        )
+
+    @Composable
+    fun inactiveTileColors(): TileColors =
+        TileColors(
+            background = MaterialTheme.colorScheme.surfaceVariant,
+            iconBackground = MaterialTheme.colorScheme.surfaceVariant,
+            label = MaterialTheme.colorScheme.onSurfaceVariant,
+            secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
+            icon = MaterialTheme.colorScheme.onSurfaceVariant,
+        )
+
+    @Composable
+    fun unavailableTileColors(): TileColors =
+        TileColors(
+            background = MaterialTheme.colorScheme.surface,
+            iconBackground = MaterialTheme.colorScheme.surface,
+            label = MaterialTheme.colorScheme.onSurface,
+            secondaryLabel = MaterialTheme.colorScheme.onSurface,
+            icon = MaterialTheme.colorScheme.onSurface,
+        )
+
+    @Composable
+    fun getColorForState(uiState: TileUiState): TileColors {
+        return when (uiState.state) {
+            STATE_ACTIVE -> {
+                if (uiState.handlesSecondaryClick) {
+                    activeDualTargetTileColors()
+                } else {
+                    activeTileColors()
+                }
+            }
+            STATE_INACTIVE -> inactiveTileColors()
+            else -> unavailableTileColors()
+        }
+    }
+
+    @Composable
+    fun animateIconShape(state: Int): Shape {
+        return animateShape(
+            state = state,
+            activeCornerRadius = ActiveIconCornerRadius,
+            label = "QSTileCornerRadius",
+        )
+    }
+
+    @Composable
+    fun animateTileShape(state: Int): Shape {
+        return animateShape(
+            state = state,
+            activeCornerRadius = ActiveTileCornerRadius,
+            label = "QSTileIconCornerRadius",
+        )
+    }
+
+    @Composable
+    fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape {
+        val animatedCornerRadius by
+            animateDpAsState(
+                targetValue =
+                    if (state == STATE_ACTIVE) {
+                        activeCornerRadius
+                    } else {
+                        InactiveCornerRadius
+                    },
+                label = label,
+            )
+        return RoundedCornerShape(animatedCornerRadius)
+    }
+}
+
+/**
+ * A composable function that returns the [Resources]. It will be recomposed when [Configuration]
+ * gets updated.
+ */
+@Composable
+@ReadOnlyComposable
+private fun resources(): Resources {
+    LocalConfiguration.current
+    return LocalContext.current.resources
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
index 08ee856..b16a707 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.qs.shared.model.CategoryAndName
 
 /** Represents an item from a grid associated with a row and a span */
-interface GridCell {
+sealed interface GridCell {
     val row: Int
     val span: GridItemSpan
 }
@@ -38,30 +38,26 @@
     override val tile: EditTileViewModel,
     override val row: Int,
     override val width: Int,
-    override val span: GridItemSpan = GridItemSpan(width)
+    override val span: GridItemSpan = GridItemSpan(width),
 ) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile {
     val key: String = "${tile.tileSpec.spec}-$row"
 
     constructor(
         sizedTile: SizedTile<EditTileViewModel>,
-        row: Int
-    ) : this(
-        tile = sizedTile.tile,
-        row = row,
-        width = sizedTile.width,
-    )
+        row: Int,
+    ) : this(tile = sizedTile.tile, row = row, width = sizedTile.width)
 }
 
 /** Represents an empty space used to fill incomplete rows. Will always display as a 1x1 tile */
 @Immutable
 data class SpacerGridCell(
     override val row: Int,
-    override val span: GridItemSpan = GridItemSpan(1)
+    override val span: GridItemSpan = GridItemSpan(1),
 ) : GridCell
 
 fun List<SizedTile<EditTileViewModel>>.toGridCells(
     columns: Int,
-    includeSpacers: Boolean = false
+    includeSpacers: Boolean = false,
 ): List<GridCell> {
     return splitInRowsSequence(this, columns)
         .flatMapIndexed { rowIndex, sizedTiles ->
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
index 0bcb6b7..9677d47 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
@@ -18,8 +18,6 @@
 
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.flags.NewQsUI
-import com.android.systemui.qs.panels.domain.interactor.GridConsistencyInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.AccessibilityTilesInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.AutoAddInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
@@ -36,16 +34,11 @@
     private val autoAddInteractor: AutoAddInteractor,
     private val featureFlags: QSPipelineFlagsRepository,
     private val restoreReconciliationInteractor: RestoreReconciliationInteractor,
-    private val gridConsistencyInteractor: GridConsistencyInteractor,
 ) : CoreStartable {
 
     override fun start() {
         accessibilityTilesInteractor.init(currentTilesInteractor)
         autoAddInteractor.init(currentTilesInteractor)
         restoreReconciliationInteractor.start()
-
-        if (NewQsUI.isEnabled) {
-            gridConsistencyInteractor.start()
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index e11ffcc..b7e2cf2 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -61,6 +61,7 @@
 import com.android.systemui.scene.session.shared.SessionStorage
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.logger.SceneLogger
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.NotificationShadeWindowController
@@ -228,8 +229,10 @@
                                         is ObservableTransitionState.Idle -> {
                                             if (state.currentScene != Scenes.Gone) {
                                                 true to "scene is not Gone"
+                                            } else if (state.currentOverlays.isNotEmpty()) {
+                                                true to "overlay is shown"
                                             } else {
-                                                false to "scene is Gone"
+                                                false to "scene is Gone and no overlays are shown"
                                             }
                                         }
                                         is ObservableTransitionState.Transition -> {
@@ -712,19 +715,21 @@
                     if (isDeviceLocked) {
                         sceneInteractor.transitionState
                             .mapNotNull { it as? ObservableTransitionState.Idle }
-                            .map { it.currentScene }
+                            .map { it.currentScene to it.currentOverlays }
                             .distinctUntilChanged()
-                            .map { sceneKey ->
-                                when (sceneKey) {
+                            .map { (sceneKey, currentOverlays) ->
+                                when {
                                     // When locked, showing the lockscreen scene should be reported
                                     // as "interacting" while showing other scenes should report as
                                     // "not interacting".
                                     //
                                     // This is done here in order to match the legacy
                                     // implementation. The real reason why is lost to lore and myth.
-                                    Scenes.Lockscreen -> true
-                                    Scenes.Bouncer -> false
-                                    Scenes.Shade -> false
+                                    Overlays.NotificationsShade in currentOverlays -> false
+                                    Overlays.QuickSettingsShade in currentOverlays -> null
+                                    sceneKey == Scenes.Lockscreen -> true
+                                    sceneKey == Scenes.Bouncer -> false
+                                    sceneKey == Scenes.Shade -> false
                                     else -> null
                                 }
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
index 751448f..7b6b0f6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
-import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.phone.PredictiveBackSysUiFlag
 
@@ -39,7 +38,6 @@
     inline val isEnabled
         get() =
             sceneContainer() && // mainAconfigFlag
-                ComposeLockscreen.isEnabled &&
                 KeyguardBottomAreaRefactor.isEnabled &&
                 KeyguardWmStateRefactor.isEnabled &&
                 MigrateClocksToBlueprint.isEnabled &&
@@ -55,7 +53,6 @@
     /** The set of secondary flags which must be enabled for scene container to work properly */
     inline fun getSecondaryFlags(): Sequence<FlagToken> =
         sequenceOf(
-            ComposeLockscreen.token,
             KeyguardBottomAreaRefactor.token,
             KeyguardWmStateRefactor.token,
             MigrateClocksToBlueprint.token,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 42499f0..f76c5fd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -137,7 +137,6 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver;
-import com.android.systemui.keyguard.shared.ComposeLockscreen;
 import com.android.systemui.keyguard.shared.model.Edge;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -186,6 +185,7 @@
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
@@ -207,7 +207,6 @@
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -2511,11 +2510,6 @@
             return 0;
         }
 
-        if (ComposeLockscreen.isEnabled()) {
-            return (int) mKeyguardInteractor.getNotificationContainerBounds()
-                    .getValue().getTop();
-        }
-
         if (!mKeyguardBypassController.getBypassEnabled()) {
             if (MigrateClocksToBlueprint.isEnabled() && !mSplitShadeEnabled) {
                 return (int) mKeyguardInteractor.getNotificationContainerBounds()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt
index c0302bc..9af4b8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt
@@ -25,6 +25,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 
 /**
  * Repository used for tracking the state of notification remote input (e.g. when the user presses
@@ -33,14 +34,21 @@
 interface RemoteInputRepository {
     /** Whether remote input is currently active for any notification. */
     val isRemoteInputActive: Flow<Boolean>
+
+    /**
+     * The bottom bound of the currently focused remote input notification row, or null if there
+     * isn't one.
+     */
+    val remoteInputRowBottomBound: Flow<Float?>
+
+    fun setRemoteInputRowBottomBound(bottom: Float?)
 }
 
 @SysUISingleton
 class RemoteInputRepositoryImpl
 @Inject
-constructor(
-    private val notificationRemoteInputManager: NotificationRemoteInputManager,
-) : RemoteInputRepository {
+constructor(private val notificationRemoteInputManager: NotificationRemoteInputManager) :
+    RemoteInputRepository {
     override val isRemoteInputActive: Flow<Boolean> = conflatedCallbackFlow {
         trySend(false) // initial value is false
         val callback =
@@ -52,6 +60,12 @@
         notificationRemoteInputManager.addControllerCallback(callback)
         awaitClose { notificationRemoteInputManager.removeControllerCallback(callback) }
     }
+
+    override val remoteInputRowBottomBound = MutableStateFlow<Float?>(null)
+
+    override fun setRemoteInputRowBottomBound(bottom: Float?) {
+        remoteInputRowBottomBound.value = bottom
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt
index 68f727b..b83b0cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt
@@ -20,13 +20,24 @@
 import com.android.systemui.statusbar.data.repository.RemoteInputRepository
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.mapNotNull
 
 /**
  * Interactor used for business logic pertaining to the notification remote input (e.g. when the
  * user presses "reply" on a notification and the keyboard opens).
  */
 @SysUISingleton
-class RemoteInputInteractor @Inject constructor(remoteInputRepository: RemoteInputRepository) {
+class RemoteInputInteractor
+@Inject
+constructor(private val remoteInputRepository: RemoteInputRepository) {
     /** Is remote input currently active for a notification? */
     val isRemoteInputActive: Flow<Boolean> = remoteInputRepository.isRemoteInputActive
+
+    /** The bottom bound of the currently focused remote input notification row. */
+    val remoteInputRowBottomBound: Flow<Float> =
+        remoteInputRepository.remoteInputRowBottomBound.mapNotNull { it }
+
+    fun setRemoteInputRowBottomBound(bottom: Float?) {
+        remoteInputRepository.setRemoteInputRowBottomBound(bottom)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index cb3e26b..5003a6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -21,6 +21,7 @@
 
 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
+import static com.android.systemui.statusbar.policy.RemoteInputView.FOCUS_ANIMATION_MIN_SCALE;
 import static com.android.systemui.util.ColorUtilKt.hexColorString;
 
 import android.animation.Animator;
@@ -83,6 +84,7 @@
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.StatusBarIconView;
@@ -118,6 +120,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
+import com.android.systemui.statusbar.policy.RemoteInputView;
 import com.android.systemui.statusbar.policy.SmartReplyConstants;
 import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent;
 import com.android.systemui.util.Compile;
@@ -830,6 +833,20 @@
         mPrivateLayout.setRemoteInputController(r);
     }
 
+    /**
+     * Return the cumulative y-value that the actions container expands via its scale animator when
+     * remote input is activated.
+     */
+    public float getRemoteInputActionsContainerExpandedOffset() {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+        RemoteInputView expandedRemoteInput = mPrivateLayout.getExpandedRemoteInput();
+        if (expandedRemoteInput == null) return 0f;
+        View actionsContainerLayout = expandedRemoteInput.getActionsContainerLayout();
+        if (actionsContainerLayout == null) return 0f;
+
+        return actionsContainerLayout.getHeight() * (1 - FOCUS_ANIMATION_MIN_SCALE) * 0.5f;
+    }
+
     public void addChildNotification(ExpandableNotificationRow row) {
         addChildNotification(row, -1);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 7543f3b..e7c67f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -99,6 +99,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.ColorUpdateLogger;
 import com.android.systemui.statusbar.notification.FakeShadowView;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
 import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController;
 import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -120,7 +121,6 @@
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape;
 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.HeadsUpUtil;
 import com.android.systemui.statusbar.policy.ScrollAdapter;
@@ -740,6 +740,15 @@
         updateFooter();
     }
 
+    void sendRemoteInputRowBottomBound(Float bottom) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        if (bottom != null) {
+            bottom += getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.notification_content_margin);
+        }
+        mScrollViewFields.sendRemoteInputRowBottomBound(bottom);
+    }
+
     /** Setter for filtered notifs, to be removed with the FooterViewRefactor flag. */
     public void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) {
         FooterViewRefactor.assertInLegacyMode();
@@ -1274,6 +1283,11 @@
     }
 
     @Override
+    public void setRemoteInputRowBottomBoundConsumer(@Nullable Consumer<Float> consumer) {
+        mScrollViewFields.setRemoteInputRowBottomBoundConsumer(consumer);
+    }
+
+    @Override
     public void setHeadsUpHeightConsumer(@Nullable Consumer<Float> consumer) {
         mScrollViewFields.setHeadsUpHeightConsumer(consumer);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index e5f63c1..dad6894 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -98,6 +98,9 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.ColorUpdateLogger;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.HeadsUpNotificationViewControllerEmptyImpl;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper.HeadsUpNotificationViewController;
 import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
@@ -129,9 +132,6 @@
 import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.notification.HeadsUpNotificationViewControllerEmptyImpl;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper.HeadsUpNotificationViewController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -1605,6 +1605,9 @@
         return new RemoteInputController.Delegate() {
             public void setRemoteInputActive(NotificationEntry entry,
                     boolean remoteInputActive) {
+                if (SceneContainerFlag.isEnabled()) {
+                    sendRemoteInputRowBottomBound(entry, remoteInputActive);
+                }
                 mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
                 entry.notifyHeightChanged(true /* needsAnimation */);
                 if (!FooterViewRefactor.isEnabled()) {
@@ -1620,6 +1623,15 @@
                 mView.requestDisallowLongPress();
                 mView.requestDisallowDismiss();
             }
+
+            private void sendRemoteInputRowBottomBound(NotificationEntry entry,
+                    boolean remoteInputActive) {
+                ExpandableNotificationRow row = entry.getRow();
+                float top = row.getTranslationY();
+                int height = row.getActualHeight();
+                float bottom = top + height + row.getRemoteInputActionsContainerExpandedOffset();
+                mView.sendRemoteInputRowBottomBound(remoteInputActive ? bottom : null);
+            }
         };
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
index aa39539..c08ed61 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -57,6 +57,13 @@
      * guts off of this gesture, we can notify the placeholder through here.
      */
     var currentGestureInGutsConsumer: Consumer<Boolean>? = null
+
+    /**
+     * When a notification begins remote input, its bottom Y bound is sent to the placeholder
+     * through here in order to adjust to accommodate the IME.
+     */
+    var remoteInputRowBottomBoundConsumer: Consumer<Float?>? = null
+
     /**
      * Any time the heads up height is recalculated, it should be updated here to be used by the
      * placeholder
@@ -75,6 +82,10 @@
     fun sendCurrentGestureInGuts(isCurrentGestureInGuts: Boolean) =
         currentGestureInGutsConsumer?.accept(isCurrentGestureInGuts)
 
+    /** send [bottomY] to the [remoteInputRowBottomBoundConsumer], if present. */
+    fun sendRemoteInputRowBottomBound(bottomY: Float?) =
+        remoteInputRowBottomBoundConsumer?.accept(bottomY)
+
     /** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */
     fun sendHeadsUpHeight(headsUpHeight: Float) = headsUpHeightConsumer?.accept(headsUpHeight)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 235b4da..41c0293 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -74,6 +74,9 @@
     /** Set a consumer for current gesture in guts events */
     fun setCurrentGestureInGutsConsumer(consumer: Consumer<Boolean>?)
 
+    /** Set a consumer for current remote input notification row bottom bound events */
+    fun setRemoteInputRowBottomBoundConsumer(consumer: Consumer<Float?>?)
+
     /** Set a consumer for heads up height changed events */
     fun setHeadsUpHeightConsumer(consumer: Consumer<Float>?)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 6d5553f..2e37dea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -108,10 +108,14 @@
                 view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
                 view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer)
                 view.setCurrentGestureInGutsConsumer(viewModel.currentGestureInGutsConsumer)
+                view.setRemoteInputRowBottomBoundConsumer(
+                    viewModel.remoteInputRowBottomBoundConsumer
+                )
                 DisposableHandle {
                     view.setSyntheticScrollConsumer(null)
                     view.setCurrentGestureOverscrollConsumer(null)
                     view.setCurrentGestureInGutsConsumer(null)
+                    view.setRemoteInputRowBottomBoundConsumer(null)
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 8d7007b..5b2e02d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
@@ -56,6 +57,7 @@
     dumpManager: DumpManager,
     stackAppearanceInteractor: NotificationStackAppearanceInteractor,
     shadeInteractor: ShadeInteractor,
+    private val remoteInputInteractor: RemoteInputInteractor,
     private val sceneInteractor: SceneInteractor,
     // TODO(b/336364825) Remove Lazy when SceneContainerFlag is released -
     // while the flag is off, creating this object too early results in a crash
@@ -240,6 +242,10 @@
     val currentGestureInGutsConsumer: (Boolean) -> Unit =
         stackAppearanceInteractor::setCurrentGestureInGuts
 
+    /** Receives the bottom bound of the currently focused remote input notification row. */
+    val remoteInputRowBottomBoundConsumer: (Float?) -> Unit =
+        remoteInputInteractor::setRemoteInputRowBottomBound
+
     /** Whether the notification stack is scrollable or not. */
     val isScrollable: Flow<Boolean> =
         combine(sceneInteractor.currentScene, sceneInteractor.currentOverlays) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 69c1bf3..c8e8358 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
@@ -49,6 +50,7 @@
     private val sceneInteractor: SceneInteractor,
     private val shadeInteractor: ShadeInteractor,
     private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
+    remoteInputInteractor: RemoteInputInteractor,
     featureFlags: FeatureFlagsClassic,
     dumpManager: DumpManager,
 ) :
@@ -132,6 +134,12 @@
     val isCurrentGestureOverscroll: Flow<Boolean> =
         interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll")
 
+    /** Whether remote input is currently active for any notification. */
+    val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive
+
+    /** The bottom bound of the currently focused remote input notification row. */
+    val remoteInputRowBottomBound = remoteInputInteractor.remoteInputRowBottomBound
+
     /** Sets whether the notification stack is scrolled to the top. */
     fun setScrolledToTop(scrolledToTop: Boolean) {
         interactor.setScrolledToTop(scrolledToTop)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 31776cf..16d5f8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -106,7 +106,7 @@
     private static final long FOCUS_ANIMATION_CROSSFADE_DURATION = 50;
     private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33;
     private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83;
-    private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f;
+    public static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f;
     private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120;
     private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index dbeaa59..ba45942 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -27,7 +27,10 @@
 import com.android.settingslib.notification.modes.ZenIconLoader
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.modes.shared.ModesUi
 import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
+import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
+import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
 import com.android.systemui.statusbar.policy.domain.model.ZenModeInfo
 import java.time.Duration
@@ -51,7 +54,17 @@
     private val notificationSettingsRepository: NotificationSettingsRepository,
     @Background private val bgDispatcher: CoroutineDispatcher,
     private val iconLoader: ZenIconLoader,
+    private val deviceProvisioningRepository: DeviceProvisioningRepository,
+    private val userSetupRepository: UserSetupRepository,
 ) {
+    val isZenAvailable: Flow<Boolean> =
+        combine(
+            deviceProvisioningRepository.isDeviceProvisioned,
+            userSetupRepository.isUserSetUp,
+        ) { isDeviceProvisioned, isUserSetUp ->
+            isDeviceProvisioned && isUserSetUp
+        }
+
     val isZenModeEnabled: Flow<Boolean> =
         zenModeRepository.globalZenMode
             .map {
@@ -80,6 +93,18 @@
 
     val modes: Flow<List<ZenMode>> = zenModeRepository.modes
 
+    /**
+     * Returns the special "manual DND" mode.
+     *
+     * This is only meant as a temporary solution for "legacy" UI pieces that handle DND
+     * specifically; any new or migrated features should use modes more generally, through [modes]
+     * or [activeModes].
+     */
+    val dndMode: Flow<ZenMode?> by lazy {
+        ModesUi.assertInNewMode()
+        zenModeRepository.modes.map { modes -> modes.singleOrNull { it.isManualDnd } }
+    }
+
     /** Flow returning the currently active mode(s), if any. */
     val activeModes: Flow<ActiveZenModes> =
         modes
@@ -113,10 +138,11 @@
                         Log.e(
                             TAG,
                             "Interactor cannot handle showing the zen duration prompt. " +
-                                "Please use EnableZenModeDialog when this setting is active."
+                                "Please use EnableZenModeDialog when this setting is active.",
                         )
                         null
                     }
+
                     ZEN_DURATION_FOREVER -> null
                     else -> Duration.ofMinutes(zenDuration.toLong())
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
index af93880..27bc6d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
@@ -59,32 +59,26 @@
         )
 
     CompositionLocalProvider(LocalContentColor provides contentColor) {
-        Surface(
-            color = tileColor,
-            shape = RoundedCornerShape(16.dp),
-        ) {
+        Surface(color = tileColor, shape = RoundedCornerShape(16.dp)) {
             Row(
                 modifier =
                     Modifier.combinedClickable(
                             onClick = viewModel.onClick,
                             onLongClick = viewModel.onLongClick,
-                            onLongClickLabel = viewModel.onLongClickLabel
+                            onLongClickLabel = viewModel.onLongClickLabel,
                         )
-                        .padding(20.dp)
+                        .padding(16.dp)
                         .semantics { stateDescription = viewModel.stateDescription },
                 verticalAlignment = Alignment.CenterVertically,
                 horizontalArrangement =
-                    Arrangement.spacedBy(
-                        space = 10.dp,
-                        alignment = Alignment.Start,
-                    ),
+                    Arrangement.spacedBy(space = 8.dp, alignment = Alignment.Start),
             ) {
                 Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp))
                 Column {
                     Text(
                         viewModel.text,
                         fontWeight = FontWeight.W500,
-                        modifier = Modifier.tileMarquee().testTag("name")
+                        modifier = Modifier.tileMarquee().testTag("name"),
                     )
                     Text(
                         viewModel.subtext,
@@ -94,7 +88,7 @@
                                 .testTag(if (viewModel.enabled) "stateOn" else "stateOff")
                                 .clearAndSetSemantics {
                                     contentDescription = viewModel.subtextDescription
-                                }
+                                },
                     )
                 }
             }
@@ -103,8 +97,5 @@
 }
 
 private fun Modifier.tileMarquee(): Modifier {
-    return this.basicMarquee(
-        iterations = 1,
-        initialDelayMillis = 200,
-    )
+    return this.basicMarquee(iterations = 1)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
index 73d361f6..5953ea5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
@@ -19,7 +19,6 @@
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.runtime.Composable
@@ -27,23 +26,20 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.Flags
 import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
 
 @Composable
 fun ModeTileGrid(viewModel: ModesDialogViewModel) {
     val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList())
 
-    // TODO(b/346519570): Handle what happens when we have more than a few modes.
     LazyVerticalGrid(
-        columns = GridCells.Fixed(2),
-        modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp),
+        columns = GridCells.Fixed(if (Flags.modesDialogSingleRows()) 1 else 2),
+        modifier = Modifier.fillMaxWidth().heightIn(max = 300.dp),
         verticalArrangement = Arrangement.spacedBy(8.dp),
         horizontalArrangement = Arrangement.spacedBy(8.dp),
     ) {
-        items(
-            tiles.size,
-            key = { index -> tiles[index].id },
-        ) { index ->
+        items(tiles.size, key = { index -> tiles[index].id }) { index ->
             ModeTile(viewModel = tiles[index])
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index db4f9ef..7166428 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -35,7 +35,6 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
 import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
 import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix;
-import static com.android.systemui.Flags.hapticVolumeSlider;
 import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED;
 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
 
@@ -928,10 +927,8 @@
     }
 
     private void addSliderHapticsToRow(VolumeRow row) {
-        if (hapticVolumeSlider()) {
-            row.createPlugin(mVibratorHelper, mSystemClock);
-            HapticSliderViewBinder.bind(row.slider, row.mHapticPlugin);
-        }
+        row.createPlugin(mVibratorHelper, mSystemClock);
+        HapticSliderViewBinder.bind(row.slider, row.mHapticPlugin);
     }
 
     @VisibleForTesting void addSliderHapticsToRows() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
index c65a117..d72b72c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -32,6 +32,7 @@
 import android.content.ClipDescription;
 import android.content.ClipboardManager;
 import android.os.PersistableBundle;
+import android.os.UserHandle;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.provider.Settings;
@@ -101,8 +102,18 @@
         when(mClipboardManager.getPrimaryClip()).thenReturn(mSampleClipData);
         when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
 
-        mClipboardListener = new ClipboardListener(getContext(), mOverlayControllerProvider,
-                mClipboardToast, mClipboardManager, mKeyguardManager, mUiEventLogger);
+        mClipboardListener = new ClipboardListener(
+                getContext(),
+                mOverlayControllerProvider,
+                mClipboardToast,
+                user -> {
+                    if (UserHandle.CURRENT.equals(user)) {
+                        return mClipboardManager;
+                    }
+                    return null;
+                },
+                mKeyguardManager,
+                mUiEventLogger);
     }
 
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 411ff91..8731853 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -77,7 +77,8 @@
     private static final int TEST_CURRENT_VOLUME = 10;
 
     // Mock
-    private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
+    private MediaSwitchingController mMediaSwitchingController =
+            mock(MediaSwitchingController.class);
     private MediaOutputDialog mMediaOutputDialog = mock(MediaOutputDialog.class);
     private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
     private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
@@ -95,13 +96,13 @@
 
     @Before
     public void setUp() {
-        when(mMediaOutputController.getMediaItemList()).thenReturn(mMediaItems);
-        when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
-        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
-        when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
-        when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
-        when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
-        when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(true);
+        when(mMediaSwitchingController.getMediaItemList()).thenReturn(mMediaItems);
+        when(mMediaSwitchingController.hasAdjustVolumeUserRestriction()).thenReturn(false);
+        when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(false);
+        when(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
+        when(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
+        when(mMediaSwitchingController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
+        when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(true);
         when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
         when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
         when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
@@ -116,7 +117,7 @@
         mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1));
         mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2));
 
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -142,7 +143,7 @@
 
     @Test
     public void onBindViewHolder_bindPairNew_verifyView() {
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -161,11 +162,13 @@
 
     @Test
     public void onBindViewHolder_bindGroup_withSessionName_verifyView() {
-        when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(
-                mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect(
-                        Collectors.toList()));
-        when(mMediaOutputController.getSessionName()).thenReturn(TEST_SESSION_NAME);
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        when(mMediaSwitchingController.getSelectedMediaDevice())
+                .thenReturn(
+                        mMediaItems.stream()
+                                .map((item) -> item.getMediaDevice().get())
+                                .collect(Collectors.toList()));
+        when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -181,11 +184,13 @@
 
     @Test
     public void onBindViewHolder_bindGroup_noSessionName_verifyView() {
-        when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(
-                mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect(
-                        Collectors.toList()));
-        when(mMediaOutputController.getSessionName()).thenReturn(null);
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        when(mMediaSwitchingController.getSelectedMediaDevice())
+                .thenReturn(
+                        mMediaItems.stream()
+                                .map((item) -> item.getMediaDevice().get())
+                                .collect(Collectors.toList()));
+        when(mMediaSwitchingController.getSessionName()).thenReturn(null);
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -214,7 +219,7 @@
 
     @Test
     public void onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() {
-        when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
+        when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -230,9 +235,9 @@
 
     @Test
     public void onBindViewHolder_bindConnectedRemoteDevice_verifyView() {
-        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
-                ImmutableList.of(mMediaDevice2));
-        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+        when(mMediaSwitchingController.getSelectableMediaDevice())
+                .thenReturn(ImmutableList.of(mMediaDevice2));
+        when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -249,9 +254,9 @@
 
     @Test
     public void onBindViewHolder_bindConnectedRemoteDevice_verifyContentDescriptionNotNull() {
-        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
-                ImmutableList.of(mMediaDevice2));
-        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+        when(mMediaSwitchingController.getSelectableMediaDevice())
+                .thenReturn(ImmutableList.of(mMediaDevice2));
+        when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -263,9 +268,8 @@
 
     @Test
     public void onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() {
-        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
-                ImmutableList.of());
-        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+        when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
+        when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -283,9 +287,8 @@
     @Test
     public void onBindViewHolder_bindConnectedRemoteDeviceWithOnGoingSession_verifyView() {
         when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
-        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
-                ImmutableList.of());
-        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+        when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
+        when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -305,9 +308,8 @@
     public void onBindViewHolder_bindConnectedRemoteDeviceWithHostOnGoingSession_verifyView() {
         when(mMediaDevice1.hasOngoingSession()).thenReturn(true);
         when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true);
-        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
-                ImmutableList.of());
-        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+        when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of());
+        when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -326,8 +328,8 @@
 
     @Test
     public void onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() {
-        when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true);
-        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+        when(mMediaSwitchingController.hasMutingExpectedDevice()).thenReturn(true);
+        when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
         assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
@@ -340,8 +342,8 @@
     @Test
     public void onBindViewHolder_isMutingExpectedDevice_verifyView() {
         when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true);
-        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
-        when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
+        when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+        when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -378,14 +380,14 @@
 
         mOnSeekBarChangeListenerCaptor.getValue().onStopTrackingTouch(mViewHolder.mSeekBar);
         assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
-        verify(mMediaOutputController).logInteractionAdjustVolume(mMediaDevice1);
+        verify(mMediaSwitchingController).logInteractionAdjustVolume(mMediaDevice1);
     }
 
     @Test
     public void onBindViewHolder_bindSelectableDevice_verifyView() {
         List<MediaDevice> selectableDevices = new ArrayList<>();
         selectableDevices.add(mMediaDevice2);
-        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+        when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
         assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
@@ -440,7 +442,7 @@
 
     @Test
     public void subStatusSupported_onBindViewHolder_bindHostDeviceWithOngoingSession_verifyView() {
-        when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
+        when(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
         when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true);
         when(mMediaDevice1.hasSubtext()).thenReturn(true);
         when(mMediaDevice1.getSubtext()).thenReturn(SUBTEXT_CUSTOM);
@@ -540,7 +542,7 @@
 
     @Test
     public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
-        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
+        when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(true);
         when(mMediaDevice1.getState()).thenReturn(
                 LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -556,7 +558,7 @@
 
     @Test
     public void onBindViewHolder_bindGroupingDevice_verifyView() {
-        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
+        when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(false);
         when(mMediaDevice1.getState()).thenReturn(
                 LocalMediaManager.MediaDeviceState.STATE_GROUPING);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -572,7 +574,7 @@
 
     @Test
     public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
-        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
+        when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(true);
         when(mMediaDevice2.getState()).thenReturn(
                 LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
@@ -586,7 +588,7 @@
 
     @Test
     public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -595,16 +597,16 @@
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
         mViewHolder.mContainerLayout.performClick();
 
-        verify(mMediaOutputController).launchBluetoothPairing(mViewHolder.mContainerLayout);
+        verify(mMediaSwitchingController).launchBluetoothPairing(mViewHolder.mContainerLayout);
     }
 
     @Test
     public void onItemClick_clickDevice_verifyConnectDevice() {
-        when(mMediaOutputController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
+        when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
         assertThat(mMediaDevice2.getState()).isEqualTo(
                 LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
         when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -613,16 +615,16 @@
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
         mViewHolder.mContainerLayout.performClick();
 
-        verify(mMediaOutputController).connectDevice(mMediaDevice2);
+        verify(mMediaSwitchingController).connectDevice(mMediaDevice2);
     }
 
     @Test
     public void onItemClick_clickDeviceWithSessionOngoing_verifyShowsDialog() {
-        when(mMediaOutputController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(true);
+        when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(true);
         assertThat(mMediaDevice2.getState()).isEqualTo(
                 LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
         when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -633,66 +635,68 @@
         mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 1);
         spyMediaDeviceViewHolder.mContainerLayout.performClick();
 
-        verify(mMediaOutputController, never()).connectDevice(mMediaDevice2);
+        verify(mMediaSwitchingController, never()).connectDevice(mMediaDevice2);
         verify(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2);
     }
 
     @Test
     public void onItemClick_clicksWithMutingExpectedDeviceExist_cancelsMuteAwaitConnection() {
-        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
-        when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true);
-        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+        when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(false);
+        when(mMediaSwitchingController.hasMutingExpectedDevice()).thenReturn(true);
+        when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false);
         when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(false);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
         mViewHolder.mContainerLayout.performClick();
 
-        verify(mMediaOutputController).cancelMuteAwaitConnection();
+        verify(mMediaSwitchingController).cancelMuteAwaitConnection();
     }
 
     @Test
     public void onGroupActionTriggered_clicksEndAreaOfSelectableDevice_triggerGrouping() {
         List<MediaDevice> selectableDevices = new ArrayList<>();
         selectableDevices.add(mMediaDevice2);
-        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+        when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
         mViewHolder.mEndTouchArea.performClick();
 
-        verify(mMediaOutputController).addDeviceToPlayMedia(mMediaDevice2);
+        verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2);
     }
 
     @Test
     public void onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() {
-        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(
-                ImmutableList.of(mMediaDevice2));
-        when(mMediaOutputController.getDeselectableMediaDevice()).thenReturn(
-                ImmutableList.of(mMediaDevice1));
-        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true);
+        when(mMediaSwitchingController.getSelectableMediaDevice())
+                .thenReturn(ImmutableList.of(mMediaDevice2));
+        when(mMediaSwitchingController.getDeselectableMediaDevice())
+                .thenReturn(ImmutableList.of(mMediaDevice1));
+        when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
         mViewHolder.mEndTouchArea.performClick();
 
-        verify(mMediaOutputController).removeDeviceFromPlayMedia(mMediaDevice1);
+        verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1);
     }
 
     @Test
     public void onItemClick_onGroupActionTriggered_verifySeekbarDisabled() {
-        when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(
-                mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect(
-                        Collectors.toList()));
-        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        when(mMediaSwitchingController.getSelectedMediaDevice())
+                .thenReturn(
+                        mMediaItems.stream()
+                                .map((item) -> item.getMediaDevice().get())
+                                .collect(Collectors.toList()));
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController);
         mMediaOutputAdapter.updateItems();
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
         List<MediaDevice> selectableDevices = new ArrayList<>();
         selectableDevices.add(mMediaDevice1);
-        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
-        when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(true);
+        when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+        when(mMediaSwitchingController.hasAdjustVolumeUserRestriction()).thenReturn(true);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
         mViewHolder.mContainerLayout.performClick();
@@ -702,11 +706,11 @@
 
     @Test
     public void onBindViewHolder_volumeControlChangeToEnabled_enableSeekbarAgain() {
-        when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(false);
+        when(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(false);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
         assertThat(mViewHolder.mSeekBar.isEnabled()).isFalse();
 
-        when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
+        when(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
         assertThat(mViewHolder.mSeekBar.isEnabled()).isTrue();
@@ -719,7 +723,7 @@
 
         mMediaOutputAdapter.updateColorScheme(wallpaperColors, true);
 
-        verify(mMediaOutputController).setCurrentColorScheme(wallpaperColors, true);
+        verify(mMediaSwitchingController).setCurrentColorScheme(wallpaperColors, true);
     }
 
     @Test
@@ -727,7 +731,7 @@
         mMediaOutputAdapter.updateItems();
         List<MediaItem> updatedList = new ArrayList<>();
         updatedList.add(MediaItem.createPairNewDeviceMediaItem());
-        when(mMediaOutputController.getMediaItemList()).thenReturn(updatedList);
+        when(mMediaSwitchingController.getMediaItemList()).thenReturn(updatedList);
         assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaItems.size());
 
         mMediaOutputAdapter.updateItems();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index c8cc6b5..47371df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -104,7 +104,7 @@
 
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
-    private MediaOutputController mMediaOutputController;
+    private MediaSwitchingController mMediaSwitchingController;
     private int mHeaderIconRes;
     private IconCompat mIconCompat;
     private CharSequence mHeaderTitle;
@@ -132,8 +132,8 @@
                 VolumePanelGlobalStateInteractorKosmosKt.getVolumePanelGlobalStateInteractor(
                         mKosmos);
 
-        mMediaOutputController =
-                new MediaOutputController(
+        mMediaSwitchingController =
+                new MediaSwitchingController(
                         mContext,
                         TEST_PACKAGE,
                         mContext.getUser(),
@@ -153,12 +153,13 @@
 
         // Using a fake package will cause routing operations to fail, so we intercept
         // scanning-related operations.
-        mMediaOutputController.mLocalMediaManager = mock(LocalMediaManager.class);
-        doNothing().when(mMediaOutputController.mLocalMediaManager).startScan();
-        doNothing().when(mMediaOutputController.mLocalMediaManager).stopScan();
+        mMediaSwitchingController.mLocalMediaManager = mock(LocalMediaManager.class);
+        doNothing().when(mMediaSwitchingController.mLocalMediaManager).startScan();
+        doNothing().when(mMediaSwitchingController.mLocalMediaManager).stopScan();
 
-        mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender,
-                mMediaOutputController);
+        mMediaOutputBaseDialogImpl =
+                new MediaOutputBaseDialogImpl(
+                        mContext, mBroadcastSender, mMediaSwitchingController);
         mMediaOutputBaseDialogImpl.onCreate(new Bundle());
     }
 
@@ -176,7 +177,7 @@
     public void refresh_withIconCompat_iconIsVisible() {
         mIconCompat = IconCompat.createWithBitmap(
                 Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888));
-        when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaOutputController);
+        when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaSwitchingController);
 
         mMediaOutputBaseDialogImpl.refresh();
         final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
@@ -263,7 +264,7 @@
         when(mMediaOutputBaseAdapter.isDragging()).thenReturn(true);
         mMediaOutputBaseDialogImpl.refresh();
 
-        assertThat(mMediaOutputController.isRefreshing()).isFalse();
+        assertThat(mMediaSwitchingController.isRefreshing()).isFalse();
     }
 
     @Test
@@ -335,12 +336,14 @@
 
     class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog {
 
-        MediaOutputBaseDialogImpl(Context context, BroadcastSender broadcastSender,
-                MediaOutputController mediaOutputController) {
+        MediaOutputBaseDialogImpl(
+                Context context,
+                BroadcastSender broadcastSender,
+                MediaSwitchingController mediaSwitchingController) {
             super(
                     context,
                     broadcastSender,
-                    mediaOutputController, /* includePlaybackAndAppMetadata */
+                    mediaSwitchingController, /* includePlaybackAndAppMetadata */
                     true);
 
             mAdapter = mMediaOutputBaseAdapter;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
index 189a561..f0902e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
@@ -119,7 +119,7 @@
     private UserTracker mUserTracker = mock(UserTracker.class);
 
     private MediaOutputBroadcastDialog mMediaOutputBroadcastDialog;
-    private MediaOutputController mMediaOutputController;
+    private MediaSwitchingController mMediaSwitchingController;
 
     @Before
     public void setUp() {
@@ -133,8 +133,8 @@
                 VolumePanelGlobalStateInteractorKosmosKt.getVolumePanelGlobalStateInteractor(
                         mKosmos);
 
-        mMediaOutputController =
-                new MediaOutputController(
+        mMediaSwitchingController =
+                new MediaSwitchingController(
                         mContext,
                         TEST_PACKAGE,
                         mContext.getUser(),
@@ -151,9 +151,10 @@
                         mFlags,
                         volumePanelGlobalStateInteractor,
                         mUserTracker);
-        mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
-        mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false,
-                mBroadcastSender, mMediaOutputController);
+        mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
+        mMediaOutputBroadcastDialog =
+                new MediaOutputBroadcastDialog(
+                        mContext, false, mBroadcastSender, mMediaSwitchingController);
         mMediaOutputBroadcastDialog.show();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index 90c2930..d3ecb3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -119,7 +119,7 @@
 
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private MediaOutputDialog mMediaOutputDialog;
-    private MediaOutputController mMediaOutputController;
+    private MediaSwitchingController mMediaSwitchingController;
     private final List<String> mFeatures = new ArrayList<>();
 
     @Override
@@ -146,8 +146,8 @@
                 VolumePanelGlobalStateInteractorKosmosKt.getVolumePanelGlobalStateInteractor(
                         mKosmos);
 
-        mMediaOutputController =
-                new MediaOutputController(
+        mMediaSwitchingController =
+                new MediaSwitchingController(
                         mContext,
                         TEST_PACKAGE,
                         mContext.getUser(),
@@ -164,8 +164,8 @@
                         mFlags,
                         volumePanelGlobalStateInteractor,
                         mUserTracker);
-        mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
-        mMediaOutputDialog = makeTestDialog(mMediaOutputController);
+        mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
+        mMediaOutputDialog = makeTestDialog(mMediaSwitchingController);
         mMediaOutputDialog.show();
 
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
@@ -388,12 +388,15 @@
     public void getStopButtonText_notSupportsBroadcast_returnsDefaultText() {
         String stopText = mContext.getText(
                 R.string.media_output_dialog_button_stop_casting).toString();
-        MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
-        when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false);
+        MediaSwitchingController mockMediaSwitchingController =
+                mock(MediaSwitchingController.class);
+        when(mockMediaSwitchingController.isBroadcastSupported()).thenReturn(false);
 
-        withTestDialog(mockMediaOutputController, testDialog -> {
-            assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
-        });
+        withTestDialog(
+                mockMediaSwitchingController,
+                testDialog -> {
+                    assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
+                });
     }
 
     @Test
@@ -401,28 +404,35 @@
     public void getStopButtonText_supportsBroadcast_returnsBroadcastText() {
         String stopText = mContext.getText(R.string.media_output_broadcast).toString();
         MediaDevice mMediaDevice = mock(MediaDevice.class);
-        MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
-        when(mockMediaOutputController.isBroadcastSupported()).thenReturn(true);
-        when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice);
-        when(mockMediaOutputController.isBluetoothLeDevice(any())).thenReturn(true);
-        when(mockMediaOutputController.isPlaying()).thenReturn(true);
-        when(mockMediaOutputController.isBluetoothLeBroadcastEnabled()).thenReturn(false);
-        withTestDialog(mockMediaOutputController, testDialog -> {
-            assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
-        });
+        MediaSwitchingController mockMediaSwitchingController =
+                mock(MediaSwitchingController.class);
+        when(mockMediaSwitchingController.isBroadcastSupported()).thenReturn(true);
+        when(mockMediaSwitchingController.getCurrentConnectedMediaDevice())
+                .thenReturn(mMediaDevice);
+        when(mockMediaSwitchingController.isBluetoothLeDevice(any())).thenReturn(true);
+        when(mockMediaSwitchingController.isPlaying()).thenReturn(true);
+        when(mockMediaSwitchingController.isBluetoothLeBroadcastEnabled()).thenReturn(false);
+        withTestDialog(
+                mockMediaSwitchingController,
+                testDialog -> {
+                    assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
+                });
     }
 
     @Test
     public void onStopButtonClick_notPlaying_releaseSession() {
-        MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
-        when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false);
-        when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(null);
-        when(mockMediaOutputController.isPlaying()).thenReturn(false);
-        withTestDialog(mockMediaOutputController, testDialog -> {
-            testDialog.onStopButtonClick();
-        });
+        MediaSwitchingController mockMediaSwitchingController =
+                mock(MediaSwitchingController.class);
+        when(mockMediaSwitchingController.isBroadcastSupported()).thenReturn(false);
+        when(mockMediaSwitchingController.getCurrentConnectedMediaDevice()).thenReturn(null);
+        when(mockMediaSwitchingController.isPlaying()).thenReturn(false);
+        withTestDialog(
+                mockMediaSwitchingController,
+                testDialog -> {
+                    testDialog.onStopButtonClick();
+                });
 
-        verify(mockMediaOutputController).releaseSession();
+        verify(mockMediaSwitchingController).releaseSession();
         verify(mDialogTransitionAnimator).disableAllCurrentDialogsExitAnimations();
     }
 
@@ -430,14 +440,14 @@
     // Check the visibility metric logging by creating a new MediaOutput dialog,
     // and verify if the calling times increases.
     public void onCreate_ShouldLogVisibility() {
-        withTestDialog(mMediaOutputController, testDialog -> {});
+        withTestDialog(mMediaSwitchingController, testDialog -> {});
 
         verify(mUiEventLogger, times(2))
                 .log(MediaOutputDialog.MediaOutputEvent.MEDIA_OUTPUT_DIALOG_SHOW);
     }
 
     @NonNull
-    private MediaOutputDialog makeTestDialog(MediaOutputController controller) {
+    private MediaOutputDialog makeTestDialog(MediaSwitchingController controller) {
         return new MediaOutputDialog(
                 mContext,
                 false,
@@ -448,7 +458,8 @@
                 true);
     }
 
-    private void withTestDialog(MediaOutputController controller, Consumer<MediaOutputDialog> c) {
+    private void withTestDialog(
+            MediaSwitchingController controller, Consumer<MediaOutputDialog> c) {
         MediaOutputDialog testDialog = makeTestDialog(controller);
         testDialog.show();
         c.accept(testDialog);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
similarity index 75%
rename from packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 714fad9..d3e20c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -43,6 +43,7 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.MediaDescription;
 import android.media.MediaMetadata;
@@ -58,6 +59,7 @@
 import android.os.PowerExemptionManager;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.service.notification.StatusBarNotification;
 import android.testing.TestableLooper;
 import android.text.TextUtils;
@@ -67,8 +69,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.media.flags.Flags;
 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.InputMediaDevice;
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
 import com.android.systemui.SysuiTestCase;
@@ -101,7 +105,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class MediaOutputControllerTest extends SysuiTestCase {
+public class MediaSwitchingControllerTest extends SysuiTestCase {
     private static final String TEST_DEVICE_1_ID = "test_device_1_id";
     private static final String TEST_DEVICE_2_ID = "test_device_2_id";
     private static final String TEST_DEVICE_3_ID = "test_device_3_id";
@@ -126,8 +130,7 @@
     private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager;
     @Mock
     private LocalBluetoothManager mLocalBluetoothManager;
-    @Mock
-    private MediaOutputController.Callback mCb;
+    @Mock private MediaSwitchingController.Callback mCb;
     @Mock
     private MediaDevice mMediaDevice1;
     @Mock
@@ -166,7 +169,8 @@
 
     private FeatureFlags mFlags = mock(FeatureFlags.class);
     private View mDialogLaunchView = mock(View.class);
-    private MediaOutputController.Callback mCallback = mock(MediaOutputController.Callback.class);
+    private MediaSwitchingController.Callback mCallback =
+            mock(MediaSwitchingController.Callback.class);
 
     final Notification mNotification = mock(Notification.class);
     private final VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor =
@@ -175,7 +179,7 @@
 
     private Context mSpyContext;
     private String mPackageName = null;
-    private MediaOutputController mMediaOutputController;
+    private MediaSwitchingController mMediaSwitchingController;
     private LocalMediaManager mLocalMediaManager;
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private List<MediaDevice> mMediaDevices = new ArrayList<>();
@@ -203,9 +207,8 @@
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(
                 mCachedBluetoothDeviceManager);
 
-
-        mMediaOutputController =
-                new MediaOutputController(
+        mMediaSwitchingController =
+                new MediaSwitchingController(
                         mSpyContext,
                         mPackageName,
                         mContext.getUser(),
@@ -222,9 +225,9 @@
                         mFlags,
                         mVolumePanelGlobalStateInteractor,
                         mUserTracker);
-        mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
+        mLocalMediaManager = spy(mMediaSwitchingController.mLocalMediaManager);
         when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false);
-        mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
+        mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
         MediaDescription.Builder builder = new MediaDescription.Builder();
         builder.setTitle(TEST_SONG);
         builder.setSubtitle(TEST_ARTIST);
@@ -264,26 +267,26 @@
 
     @Test
     public void start_verifyLocalMediaManagerInit() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
-        verify(mLocalMediaManager).registerCallback(mMediaOutputController);
+        verify(mLocalMediaManager).registerCallback(mMediaSwitchingController);
         verify(mLocalMediaManager).startScan();
     }
 
     @Test
     public void stop_verifyLocalMediaManagerDeinit() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mLocalMediaManager);
 
-        mMediaOutputController.stop();
+        mMediaSwitchingController.stop();
 
-        verify(mLocalMediaManager).unregisterCallback(mMediaOutputController);
+        verify(mLocalMediaManager).unregisterCallback(mMediaSwitchingController);
         verify(mLocalMediaManager).stopScan();
     }
 
     @Test
     public void start_notificationNotFound_mediaControllerInitFromSession() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
         verify(mSessionMediaController).registerCallback(any());
     }
@@ -291,7 +294,7 @@
     @Test
     public void start_MediaNotificationFound_mediaControllerNotInitFromSession() {
         when(mNotification.isMediaNotification()).thenReturn(true);
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
         verify(mSessionMediaController, never()).registerCallback(any());
         verifyZeroInteractions(mMediaSessionManager);
@@ -299,8 +302,8 @@
 
     @Test
     public void start_withoutPackageName_verifyMediaControllerInit() {
-        mMediaOutputController =
-                new MediaOutputController(
+        mMediaSwitchingController =
+                new MediaSwitchingController(
                         mSpyContext,
                         null,
                         mContext.getUser(),
@@ -318,32 +321,32 @@
                         mVolumePanelGlobalStateInteractor,
                         mUserTracker);
 
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
         verify(mSessionMediaController, never()).registerCallback(any());
     }
 
     @Test
     public void start_nearbyMediaDevicesManagerNotNull_registersNearbyDevicesCallback() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
         verify(mNearbyMediaDevicesManager).registerNearbyDevicesCallback(any());
     }
 
     @Test
     public void stop_withPackageName_verifyMediaControllerDeinit() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mSessionMediaController);
 
-        mMediaOutputController.stop();
+        mMediaSwitchingController.stop();
 
         verify(mSessionMediaController).unregisterCallback(any());
     }
 
     @Test
     public void stop_withoutPackageName_verifyMediaControllerDeinit() {
-        mMediaOutputController =
-                new MediaOutputController(
+        mMediaSwitchingController =
+                new MediaSwitchingController(
                         mSpyContext,
                         null,
                         mSpyContext.getUser(),
@@ -361,26 +364,26 @@
                         mVolumePanelGlobalStateInteractor,
                         mUserTracker);
 
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
-        mMediaOutputController.stop();
+        mMediaSwitchingController.stop();
 
         verify(mSessionMediaController, never()).unregisterCallback(any());
     }
 
     @Test
     public void stop_nearbyMediaDevicesManagerNotNull_unregistersNearbyDevicesCallback() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mSessionMediaController);
 
-        mMediaOutputController.stop();
+        mMediaSwitchingController.stop();
 
         verify(mNearbyMediaDevicesManager).unregisterNearbyDevicesCallback(any());
     }
 
     @Test
     public void tryToLaunchMediaApplication_nullIntent_skip() {
-        mMediaOutputController.tryToLaunchMediaApplication(mDialogLaunchView);
+        mMediaSwitchingController.tryToLaunchMediaApplication(mDialogLaunchView);
 
         verify(mCb, never()).dismissDialog();
     }
@@ -391,9 +394,9 @@
                 .thenReturn(mController);
         Intent intent = new Intent(mPackageName);
         doReturn(intent).when(mPackageManager).getLaunchIntentForPackage(mPackageName);
-        mMediaOutputController.start(mCallback);
+        mMediaSwitchingController.start(mCallback);
 
-        mMediaOutputController.tryToLaunchMediaApplication(mDialogLaunchView);
+        mMediaSwitchingController.tryToLaunchMediaApplication(mDialogLaunchView);
 
         verify(mStarter).startActivity(any(Intent.class), anyBoolean(),
                 Mockito.eq(mController));
@@ -403,11 +406,12 @@
     public void tryToLaunchInAppRoutingIntent_componentNameNotNull_startActivity() {
         when(mDialogTransitionAnimator.createActivityTransitionController(any(View.class)))
                 .thenReturn(mController);
-        mMediaOutputController.start(mCallback);
+        mMediaSwitchingController.start(mCallback);
         when(mLocalMediaManager.getLinkedItemComponentName()).thenReturn(
                 new ComponentName(mPackageName, ""));
 
-        mMediaOutputController.tryToLaunchInAppRoutingIntent(TEST_DEVICE_1_ID, mDialogLaunchView);
+        mMediaSwitchingController.tryToLaunchInAppRoutingIntent(
+                TEST_DEVICE_1_ID, mDialogLaunchView);
 
         verify(mStarter).startActivity(any(Intent.class), anyBoolean(),
                 Mockito.eq(mController));
@@ -415,9 +419,9 @@
 
     @Test
     public void onDevicesUpdated_unregistersNearbyDevicesCallback() throws RemoteException {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
-        mMediaOutputController.onDevicesUpdated(ImmutableList.of());
+        mMediaSwitchingController.onDevicesUpdated(ImmutableList.of());
 
         verify(mNearbyMediaDevicesManager).unregisterNearbyDevicesCallback(any());
     }
@@ -425,11 +429,11 @@
     @Test
     public void onDeviceListUpdate_withNearbyDevices_updatesRangeInformation()
             throws RemoteException {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        mMediaOutputController.onDevicesUpdated(mNearbyDevices);
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.onDevicesUpdated(mNearbyDevices);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
 
         verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_FAR);
         verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_CLOSE);
@@ -438,11 +442,11 @@
     @Test
     public void onDeviceListUpdate_withNearbyDevices_rankByRangeInformation()
             throws RemoteException {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        mMediaOutputController.onDevicesUpdated(mNearbyDevices);
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.onDevicesUpdated(mNearbyDevices);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
 
         assertThat(mMediaDevices.get(0).getId()).isEqualTo(TEST_DEVICE_1_ID);
     }
@@ -451,11 +455,11 @@
     public void routeProcessSupport_onDeviceListUpdate_preferenceExist_NotUpdatesRangeInformation()
             throws RemoteException {
         when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        mMediaOutputController.onDevicesUpdated(mNearbyDevices);
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.onDevicesUpdated(mNearbyDevices);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
 
         verify(mMediaDevice1, never()).setRangeZone(anyInt());
         verify(mMediaDevice2, never()).setRangeZone(anyInt());
@@ -463,7 +467,8 @@
 
     @Test
     public void onDeviceListUpdate_verifyDeviceListCallback() {
-        // This test relies on mMediaOutputController.start being called while the selected device
+        // This test relies on mMediaSwitchingController.start being called while the selected
+        // device
         // list has exactly one item, and that item's id is:
         // - Different from both ids in mMediaDevices.
         // - Different from the id of the route published by the device under test (usually the
@@ -475,12 +480,12 @@
                 .when(mLocalMediaManager)
                 .getSelectedMediaDevice();
 
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
         final List<MediaDevice> devices = new ArrayList<>();
-        for (MediaItem item : mMediaOutputController.getMediaItemList()) {
+        for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
             if (item.getMediaDevice().isPresent()) {
                 devices.add(item.getMediaDevice().get());
             }
@@ -488,14 +493,15 @@
 
         assertThat(devices.containsAll(mMediaDevices)).isTrue();
         assertThat(devices.size()).isEqualTo(mMediaDevices.size());
-        assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo(
-                mMediaDevices.size() + 2);
+        assertThat(mMediaSwitchingController.getMediaItemList().size())
+                .isEqualTo(mMediaDevices.size() + 2);
         verify(mCb).onDeviceListChanged();
     }
 
     @Test
     public void advanced_onDeviceListUpdateWithConnectedDeviceRemote_verifyItemSize() {
-        // This test relies on mMediaOutputController.start being called while the selected device
+        // This test relies on mMediaSwitchingController.start being called while the selected
+        // device
         // list has exactly one item, and that item's id is:
         // - Different from both ids in mMediaDevices.
         // - Different from the id of the route published by the device under test (usually the
@@ -510,12 +516,12 @@
         when(mMediaDevice1.getFeatures()).thenReturn(
                 ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK));
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
         final List<MediaDevice> devices = new ArrayList<>();
-        for (MediaItem item : mMediaOutputController.getMediaItemList()) {
+        for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
             if (item.getMediaDevice().isPresent()) {
                 devices.add(item.getMediaDevice().get());
             }
@@ -523,23 +529,72 @@
 
         assertThat(devices.containsAll(mMediaDevices)).isTrue();
         assertThat(devices.size()).isEqualTo(mMediaDevices.size());
-        assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo(
-                mMediaDevices.size() + 1);
+        assertThat(mMediaSwitchingController.getMediaItemList().size())
+                .isEqualTo(mMediaDevices.size() + 1);
         verify(mCb).onDeviceListChanged();
     }
 
+    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void onInputDeviceListUpdate_verifyDeviceListCallback() {
+        AudioDeviceInfo[] audioDeviceInfos = {};
+        when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS))
+                .thenReturn(audioDeviceInfos);
+        mMediaSwitchingController.start(mCb);
+
+        // Output devices have changed.
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+
+        final int MAX_VOLUME = 1;
+        final int CURRENT_VOLUME = 0;
+        final boolean IS_VOLUME_FIXED = true;
+        final MediaDevice mediaDevice3 =
+                InputMediaDevice.create(
+                        mContext,
+                        TEST_DEVICE_3_ID,
+                        AudioDeviceInfo.TYPE_BUILTIN_MIC,
+                        MAX_VOLUME,
+                        CURRENT_VOLUME,
+                        IS_VOLUME_FIXED);
+        final MediaDevice mediaDevice4 =
+                InputMediaDevice.create(
+                        mContext,
+                        TEST_DEVICE_4_ID,
+                        AudioDeviceInfo.TYPE_WIRED_HEADSET,
+                        MAX_VOLUME,
+                        CURRENT_VOLUME,
+                        IS_VOLUME_FIXED);
+        final List<MediaDevice> inputDevices = new ArrayList<>();
+        inputDevices.add(mediaDevice3);
+        inputDevices.add(mediaDevice4);
+
+        // Input devices have changed.
+        mMediaSwitchingController.mInputDeviceCallback.onInputDeviceListUpdated(inputDevices);
+
+        final List<MediaDevice> devices = new ArrayList<>();
+        for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
+            if (item.getMediaDevice().isPresent()) {
+                devices.add(item.getMediaDevice().get());
+            }
+        }
+
+        assertThat(devices).containsAtLeastElementsIn(mMediaDevices);
+        assertThat(devices).hasSize(mMediaDevices.size() + inputDevices.size());
+        verify(mCb, atLeastOnce()).onDeviceListChanged();
+    }
+
     @Test
     public void advanced_categorizeMediaItems_withSuggestedDevice_verifyDeviceListSize() {
         when(mMediaDevice1.isSuggestedDevice()).thenReturn(true);
         when(mMediaDevice2.isSuggestedDevice()).thenReturn(false);
 
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
-        mMediaOutputController.getMediaItemList().clear();
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.getMediaItemList().clear();
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
         final List<MediaDevice> devices = new ArrayList<>();
         int dividerSize = 0;
-        for (MediaItem item : mMediaOutputController.getMediaItemList()) {
+        for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
             if (item.getMediaDevice().isPresent()) {
                 devices.add(item.getMediaDevice().get());
             }
@@ -556,33 +611,33 @@
 
     @Test
     public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
-        mMediaOutputController.mIsRefreshing = true;
+        mMediaSwitchingController.mIsRefreshing = true;
 
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
 
-        assertThat(mMediaOutputController.mNeedRefresh).isTrue();
+        assertThat(mMediaSwitchingController.mNeedRefresh).isTrue();
     }
 
     @Test
     public void advanced_onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
-        mMediaOutputController.mIsRefreshing = true;
+        mMediaSwitchingController.mIsRefreshing = true;
 
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
 
-        assertThat(mMediaOutputController.mNeedRefresh).isTrue();
+        assertThat(mMediaSwitchingController.mNeedRefresh).isTrue();
     }
 
     @Test
     public void cancelMuteAwaitConnection_cancelsWithMediaManager() {
         when(mAudioManager.getMutingExpectedDevice()).thenReturn(mock(AudioDeviceAttributes.class));
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        mMediaOutputController.cancelMuteAwaitConnection();
+        mMediaSwitchingController.cancelMuteAwaitConnection();
 
         verify(mAudioManager).cancelMuteAwaitConnection(any());
     }
@@ -590,17 +645,17 @@
     @Test
     public void cancelMuteAwaitConnection_audioManagerIsNull_noAction() {
         when(mAudioManager.getMutingExpectedDevice()).thenReturn(null);
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
-        mMediaOutputController.cancelMuteAwaitConnection();
+        mMediaSwitchingController.cancelMuteAwaitConnection();
 
         verify(mAudioManager, never()).cancelMuteAwaitConnection(any());
     }
 
     @Test
     public void getAppSourceName_packageNameIsNull_returnsNull() {
-        MediaOutputController testMediaOutputController =
-                new MediaOutputController(
+        MediaSwitchingController testMediaSwitchingController =
+                new MediaSwitchingController(
                         mSpyContext,
                         "",
                         mSpyContext.getUser(),
@@ -617,25 +672,25 @@
                         mFlags,
                         mVolumePanelGlobalStateInteractor,
                         mUserTracker);
-        testMediaOutputController.start(mCb);
+        testMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        testMediaOutputController.getAppSourceName();
+        testMediaSwitchingController.getAppSourceName();
 
-        assertThat(testMediaOutputController.getAppSourceName()).isNull();
+        assertThat(testMediaSwitchingController.getAppSourceName()).isNull();
     }
 
     @Test
     public void isActiveItem_deviceNotConnected_returnsFalse() {
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
 
-        assertThat(mMediaOutputController.isActiveItem(mMediaDevice1)).isFalse();
+        assertThat(mMediaSwitchingController.isActiveItem(mMediaDevice1)).isFalse();
     }
 
     @Test
     public void getNotificationSmallIcon_packageNameIsNull_returnsNull() {
-        MediaOutputController testMediaOutputController =
-                new MediaOutputController(
+        MediaSwitchingController testMediaSwitchingController =
+                new MediaSwitchingController(
                         mSpyContext,
                         "",
                         mSpyContext.getUser(),
@@ -652,23 +707,23 @@
                         mFlags,
                         mVolumePanelGlobalStateInteractor,
                         mUserTracker);
-        testMediaOutputController.start(mCb);
+        testMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        testMediaOutputController.getAppSourceName();
+        testMediaSwitchingController.getAppSourceName();
 
-        assertThat(testMediaOutputController.getNotificationSmallIcon()).isNull();
+        assertThat(testMediaSwitchingController.getNotificationSmallIcon()).isNull();
     }
 
     @Test
     public void refreshDataSetIfNeeded_needRefreshIsTrue_setsToFalse() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
-        mMediaOutputController.mNeedRefresh = true;
+        mMediaSwitchingController.mNeedRefresh = true;
 
-        mMediaOutputController.refreshDataSetIfNeeded();
+        mMediaSwitchingController.refreshDataSetIfNeeded();
 
-        assertThat(mMediaOutputController.mNeedRefresh).isFalse();
+        assertThat(mMediaSwitchingController.mNeedRefresh).isFalse();
     }
 
     @Test
@@ -677,13 +732,13 @@
                 ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK));
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
 
-        assertThat(mMediaOutputController.isCurrentConnectedDeviceRemote()).isTrue();
+        assertThat(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).isTrue();
     }
 
     @Test
     public void addDeviceToPlayMedia_callsLocalMediaManager() {
-        MediaOutputController testMediaOutputController =
-                new MediaOutputController(
+        MediaSwitchingController testMediaSwitchingController =
+                new MediaSwitchingController(
                         mSpyContext,
                         null,
                         mSpyContext.getUser(),
@@ -702,16 +757,16 @@
                         mUserTracker);
 
         LocalMediaManager mockLocalMediaManager = mock(LocalMediaManager.class);
-        testMediaOutputController.mLocalMediaManager = mockLocalMediaManager;
+        testMediaSwitchingController.mLocalMediaManager = mockLocalMediaManager;
 
-        testMediaOutputController.addDeviceToPlayMedia(mMediaDevice2);
+        testMediaSwitchingController.addDeviceToPlayMedia(mMediaDevice2);
         verify(mockLocalMediaManager).addDeviceToPlayMedia(mMediaDevice2);
     }
 
     @Test
     public void removeDeviceFromPlayMedia_callsLocalMediaManager() {
-        MediaOutputController testMediaOutputController =
-                new MediaOutputController(
+        MediaSwitchingController testMediaSwitchingController =
+                new MediaSwitchingController(
                         mSpyContext,
                         null,
                         mSpyContext.getUser(),
@@ -730,15 +785,15 @@
                         mUserTracker);
 
         LocalMediaManager mockLocalMediaManager = mock(LocalMediaManager.class);
-        testMediaOutputController.mLocalMediaManager = mockLocalMediaManager;
+        testMediaSwitchingController.mLocalMediaManager = mockLocalMediaManager;
 
-        testMediaOutputController.removeDeviceFromPlayMedia(mMediaDevice2);
+        testMediaSwitchingController.removeDeviceFromPlayMedia(mMediaDevice2);
         verify(mockLocalMediaManager).removeDeviceFromPlayMedia(mMediaDevice2);
     }
 
     @Test
     public void getDeselectableMediaDevice_triggersFromLocalMediaManager() {
-        mMediaOutputController.getDeselectableMediaDevice();
+        mMediaSwitchingController.getDeselectableMediaDevice();
 
         verify(mLocalMediaManager).getDeselectableMediaDevice();
     }
@@ -746,108 +801,108 @@
     @Test
     public void adjustSessionVolume_adjustWithoutId_triggersFromLocalMediaManager() {
         int testVolume = 10;
-        mMediaOutputController.adjustSessionVolume(testVolume);
+        mMediaSwitchingController.adjustSessionVolume(testVolume);
 
         verify(mLocalMediaManager).adjustSessionVolume(testVolume);
     }
 
     @Test
     public void logInteractionAdjustVolume_triggersFromMetricLogger() {
-        MediaOutputMetricLogger spyMediaOutputMetricLogger = spy(
-                mMediaOutputController.mMetricLogger);
-        mMediaOutputController.mMetricLogger = spyMediaOutputMetricLogger;
+        MediaOutputMetricLogger spyMediaOutputMetricLogger =
+                spy(mMediaSwitchingController.mMetricLogger);
+        mMediaSwitchingController.mMetricLogger = spyMediaOutputMetricLogger;
 
-        mMediaOutputController.logInteractionAdjustVolume(mMediaDevice1);
+        mMediaSwitchingController.logInteractionAdjustVolume(mMediaDevice1);
 
         verify(spyMediaOutputMetricLogger).logInteractionAdjustVolume(mMediaDevice1);
     }
 
     @Test
     public void getSessionVolumeMax_triggersFromLocalMediaManager() {
-        mMediaOutputController.getSessionVolumeMax();
+        mMediaSwitchingController.getSessionVolumeMax();
 
         verify(mLocalMediaManager).getSessionVolumeMax();
     }
 
     @Test
     public void getSessionVolume_triggersFromLocalMediaManager() {
-        mMediaOutputController.getSessionVolume();
+        mMediaSwitchingController.getSessionVolume();
 
         verify(mLocalMediaManager).getSessionVolume();
     }
 
     @Test
     public void getSessionName_triggersFromLocalMediaManager() {
-        mMediaOutputController.getSessionName();
+        mMediaSwitchingController.getSessionName();
 
         verify(mLocalMediaManager).getSessionName();
     }
 
     @Test
     public void releaseSession_triggersFromLocalMediaManager() {
-        mMediaOutputController.releaseSession();
+        mMediaSwitchingController.releaseSession();
 
         verify(mLocalMediaManager).releaseSession();
     }
 
     @Test
     public void isAnyDeviceTransferring_noDevicesStateIsConnecting_returnsFalse() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
 
-        assertThat(mMediaOutputController.isAnyDeviceTransferring()).isFalse();
+        assertThat(mMediaSwitchingController.isAnyDeviceTransferring()).isFalse();
     }
 
     @Test
     public void isAnyDeviceTransferring_deviceStateIsConnecting_returnsTrue() {
         when(mMediaDevice1.getState()).thenReturn(
                 LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
 
-        assertThat(mMediaOutputController.isAnyDeviceTransferring()).isTrue();
+        assertThat(mMediaSwitchingController.isAnyDeviceTransferring()).isTrue();
     }
 
     @Test
     public void isAnyDeviceTransferring_advancedLayoutSupport() {
         when(mMediaDevice1.getState()).thenReturn(
                 LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
-        mMediaOutputController.start(mCb);
-        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        mMediaSwitchingController.start(mCb);
+        mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
 
-        assertThat(mMediaOutputController.isAnyDeviceTransferring()).isTrue();
+        assertThat(mMediaSwitchingController.isAnyDeviceTransferring()).isTrue();
     }
 
     @Test
     public void isPlaying_stateIsNull() {
         when(mSessionMediaController.getPlaybackState()).thenReturn(null);
 
-        assertThat(mMediaOutputController.isPlaying()).isFalse();
+        assertThat(mMediaSwitchingController.isPlaying()).isFalse();
     }
 
     @Test
     public void onSelectedDeviceStateChanged_verifyCallback() {
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
-        mMediaOutputController.connectDevice(mMediaDevice1);
+        mMediaSwitchingController.connectDevice(mMediaDevice1);
 
-        mMediaOutputController.onSelectedDeviceStateChanged(mMediaDevice1,
-                LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
+        mMediaSwitchingController.onSelectedDeviceStateChanged(
+                mMediaDevice1, LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
 
         verify(mCb).onRouteChanged();
     }
 
     @Test
     public void onDeviceAttributesChanged_verifyCallback() {
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
 
-        mMediaOutputController.onDeviceAttributesChanged();
+        mMediaSwitchingController.onDeviceAttributesChanged();
 
         verify(mCb).onRouteChanged();
     }
@@ -855,11 +910,11 @@
     @Test
     public void onRequestFailed_verifyCallback() {
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
         reset(mCb);
-        mMediaOutputController.connectDevice(mMediaDevice2);
+        mMediaSwitchingController.connectDevice(mMediaDevice2);
 
-        mMediaOutputController.onRequestFailed(0 /* reason */);
+        mMediaSwitchingController.onRequestFailed(0 /* reason */);
 
         verify(mCb, atLeastOnce()).onRouteChanged();
     }
@@ -868,37 +923,40 @@
     public void getHeaderTitle_withoutMetadata_returnDefaultString() {
         when(mSessionMediaController.getMetadata()).thenReturn(null);
 
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
-        assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(
-                mContext.getText(R.string.controls_media_title));
+        assertThat(
+                        mMediaSwitchingController
+                                .getHeaderTitle()
+                                .equals(mContext.getText(R.string.controls_media_title)))
+                .isTrue();
     }
 
     @Test
     public void getHeaderTitle_withMetadata_returnSongName() {
         when(mSessionMediaController.getMetadata()).thenReturn(mMediaMetadata);
 
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
-        assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(TEST_SONG);
+        assertThat(mMediaSwitchingController.getHeaderTitle().equals(TEST_SONG)).isTrue();
     }
 
     @Test
     public void getHeaderSubTitle_withoutMetadata_returnNull() {
         when(mSessionMediaController.getMetadata()).thenReturn(null);
 
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
-        assertThat(mMediaOutputController.getHeaderSubTitle()).isNull();
+        assertThat(mMediaSwitchingController.getHeaderSubTitle()).isNull();
     }
 
     @Test
     public void getHeaderSubTitle_withMetadata_returnArtistName() {
         when(mSessionMediaController.getMetadata()).thenReturn(mMediaMetadata);
 
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.start(mCb);
 
-        assertThat(mMediaOutputController.getHeaderSubTitle()).isEqualTo(TEST_ARTIST);
+        assertThat(mMediaSwitchingController.getHeaderSubTitle().equals(TEST_ARTIST)).isTrue();
     }
 
     @Test
@@ -911,8 +969,8 @@
         mRoutingSessionInfos.add(mRemoteSessionInfo);
         when(mLocalMediaManager.getRemoteRoutingSessions()).thenReturn(mRoutingSessionInfos);
 
-        assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).containsExactly(
-                mRemoteSessionInfo);
+        assertThat(mMediaSwitchingController.getActiveRemoteMediaDevices())
+                .containsExactly(mRemoteSessionInfo);
     }
 
     @Test
@@ -933,7 +991,8 @@
         selectableMediaDevices.add(selectableMediaDevice2);
         doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice();
         doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice();
-        final List<MediaDevice> groupMediaDevices = mMediaOutputController.getGroupMediaDevices();
+        final List<MediaDevice> groupMediaDevices =
+                mMediaSwitchingController.getGroupMediaDevices();
         // Reset order
         selectedMediaDevices.clear();
         selectedMediaDevices.add(selectedMediaDevice2);
@@ -941,7 +1000,7 @@
         selectableMediaDevices.clear();
         selectableMediaDevices.add(selectableMediaDevice2);
         selectableMediaDevices.add(selectableMediaDevice1);
-        final List<MediaDevice> newDevices = mMediaOutputController.getGroupMediaDevices();
+        final List<MediaDevice> newDevices = mMediaSwitchingController.getGroupMediaDevices();
 
         assertThat(newDevices.size()).isEqualTo(groupMediaDevices.size());
         for (int i = 0; i < groupMediaDevices.size(); i++) {
@@ -970,7 +1029,8 @@
         selectableMediaDevices.add(selectableMediaDevice2);
         doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice();
         doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice();
-        final List<MediaDevice> groupMediaDevices = mMediaOutputController.getGroupMediaDevices();
+        final List<MediaDevice> groupMediaDevices =
+                mMediaSwitchingController.getGroupMediaDevices();
         // Reset order
         selectedMediaDevices.clear();
         selectedMediaDevices.add(selectedMediaDevice2);
@@ -979,7 +1039,7 @@
         selectableMediaDevices.add(selectableMediaDevice3);
         selectableMediaDevices.add(selectableMediaDevice2);
         selectableMediaDevices.add(selectableMediaDevice1);
-        final List<MediaDevice> newDevices = mMediaOutputController.getGroupMediaDevices();
+        final List<MediaDevice> newDevices = mMediaSwitchingController.getGroupMediaDevices();
 
         assertThat(newDevices.size()).isEqualTo(5);
         for (int i = 0; i < groupMediaDevices.size(); i++) {
@@ -991,8 +1051,8 @@
 
     @Test
     public void getNotificationLargeIcon_withoutPackageName_returnsNull() {
-        mMediaOutputController =
-                new MediaOutputController(
+        mMediaSwitchingController =
+                new MediaSwitchingController(
                         mSpyContext,
                         null,
                         mSpyContext.getUser(),
@@ -1010,7 +1070,7 @@
                         mVolumePanelGlobalStateInteractor,
                         mUserTracker);
 
-        assertThat(mMediaOutputController.getNotificationIcon()).isNull();
+        assertThat(mMediaSwitchingController.getNotificationIcon()).isNull();
     }
 
     @Test
@@ -1028,7 +1088,7 @@
         when(notification.isMediaNotification()).thenReturn(true);
         when(notification.getLargeIcon()).thenReturn(null);
 
-        assertThat(mMediaOutputController.getNotificationIcon()).isNull();
+        assertThat(mMediaSwitchingController.getNotificationIcon()).isNull();
     }
 
     @Test
@@ -1047,7 +1107,7 @@
         when(notification.isMediaNotification()).thenReturn(true);
         when(notification.getLargeIcon()).thenReturn(icon);
 
-        assertThat(mMediaOutputController.getNotificationIcon()).isInstanceOf(IconCompat.class);
+        assertThat(mMediaSwitchingController.getNotificationIcon()).isInstanceOf(IconCompat.class);
     }
 
     @Test
@@ -1066,7 +1126,7 @@
         when(notification.isMediaNotification()).thenReturn(false);
         when(notification.getLargeIcon()).thenReturn(icon);
 
-        assertThat(mMediaOutputController.getNotificationIcon()).isNull();
+        assertThat(mMediaSwitchingController.getNotificationIcon()).isNull();
     }
 
     @Test
@@ -1084,7 +1144,7 @@
         when(notification.isMediaNotification()).thenReturn(true);
         when(notification.getSmallIcon()).thenReturn(null);
 
-        assertThat(mMediaOutputController.getNotificationSmallIcon()).isNull();
+        assertThat(mMediaSwitchingController.getNotificationSmallIcon()).isNull();
     }
 
     @Test
@@ -1103,8 +1163,8 @@
         when(notification.isMediaNotification()).thenReturn(true);
         when(notification.getSmallIcon()).thenReturn(icon);
 
-        assertThat(mMediaOutputController.getNotificationSmallIcon()).isInstanceOf(
-                IconCompat.class);
+        assertThat(mMediaSwitchingController.getNotificationSmallIcon())
+                .isInstanceOf(IconCompat.class);
     }
 
     @Test
@@ -1112,8 +1172,8 @@
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
         when(mMediaDevice1.getIcon()).thenReturn(mDrawable);
 
-        assertThat(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).isInstanceOf(
-                IconCompat.class);
+        assertThat(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice1))
+                .isInstanceOf(IconCompat.class);
     }
 
     @Test
@@ -1121,13 +1181,13 @@
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
         when(mMediaDevice1.getIcon()).thenReturn(null);
 
-        assertThat(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).isInstanceOf(
-                IconCompat.class);
+        assertThat(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice1))
+                .isInstanceOf(IconCompat.class);
     }
 
     @Test
     public void setColorFilter_setColorFilterToDrawable() {
-        mMediaOutputController.setColorFilter(mDrawable, true);
+        mMediaSwitchingController.setColorFilter(mDrawable, true);
 
         verify(mDrawable).setColorFilter(any(PorterDuffColorFilter.class));
     }
@@ -1150,11 +1210,11 @@
         selectableMediaDevices.add(selectableMediaDevice2);
         doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice();
         doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice();
-        assertThat(mMediaOutputController.getGroupMediaDevices().isEmpty()).isFalse();
+        assertThat(mMediaSwitchingController.getGroupMediaDevices().isEmpty()).isFalse();
 
-        mMediaOutputController.resetGroupMediaDevices();
+        mMediaSwitchingController.resetGroupMediaDevices();
 
-        assertThat(mMediaOutputController.mGroupMediaDevices.isEmpty()).isTrue();
+        assertThat(mMediaSwitchingController.mGroupMediaDevices.isEmpty()).isTrue();
     }
 
     @Test
@@ -1164,7 +1224,7 @@
 
         when(mMediaDevice1.isVolumeFixed()).thenReturn(true);
 
-        assertThat(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).isFalse();
+        assertThat(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).isFalse();
     }
 
     @Test
@@ -1174,7 +1234,7 @@
 
         when(mMediaDevice1.isVolumeFixed()).thenReturn(false);
 
-        assertThat(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).isTrue();
+        assertThat(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).isTrue();
     }
 
     @Test
@@ -1187,7 +1247,7 @@
         when(mMediaDevice2.getDeviceType()).thenReturn(
                 MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
 
-        mMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
+        mMediaSwitchingController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
 
         verify(mPowerExemptionManager).addToTemporaryAllowList(anyString(), anyInt(), anyString(),
                 anyLong());
@@ -1195,8 +1255,8 @@
 
     @Test
     public void setTemporaryAllowListExceptionIfNeeded_packageNameIsNull_NoAction() {
-        MediaOutputController testMediaOutputController =
-                new MediaOutputController(
+        MediaSwitchingController testMediaSwitchingController =
+                new MediaSwitchingController(
                         mSpyContext,
                         null,
                         mSpyContext.getUser(),
@@ -1214,7 +1274,7 @@
                         mVolumePanelGlobalStateInteractor,
                         mUserTracker);
 
-        testMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
+        testMediaSwitchingController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
 
         verify(mPowerExemptionManager, never()).addToTemporaryAllowList(anyString(), anyInt(),
                 anyString(),
@@ -1223,22 +1283,22 @@
 
     @Test
     public void onMetadataChanged_triggersOnMetadataChanged() {
-        mMediaOutputController.mCallback = this.mCallback;
+        mMediaSwitchingController.mCallback = this.mCallback;
 
-        mMediaOutputController.mCb.onMetadataChanged(mMediaMetadata);
+        mMediaSwitchingController.mCb.onMetadataChanged(mMediaMetadata);
 
-        verify(mMediaOutputController.mCallback).onMediaChanged();
+        verify(mMediaSwitchingController.mCallback).onMediaChanged();
     }
 
     @Test
     public void onPlaybackStateChanged_updateWithNullState_onMediaStoppedOrPaused() {
         when(mPlaybackState.getState()).thenReturn(PlaybackState.STATE_PLAYING);
-        mMediaOutputController.mCallback = this.mCallback;
-        mMediaOutputController.start(mCb);
+        mMediaSwitchingController.mCallback = this.mCallback;
+        mMediaSwitchingController.start(mCb);
 
-        mMediaOutputController.mCb.onPlaybackStateChanged(null);
+        mMediaSwitchingController.mCb.onPlaybackStateChanged(null);
 
-        verify(mMediaOutputController.mCallback).onMediaStoppedOrPaused();
+        verify(mMediaSwitchingController.mCallback).onMediaStoppedOrPaused();
     }
 
     @Test
@@ -1246,9 +1306,9 @@
         when(mDialogTransitionAnimator.createActivityTransitionController(mDialogLaunchView))
                 .thenReturn(mActivityTransitionAnimatorController);
         when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
-        mMediaOutputController.mCallback = this.mCallback;
+        mMediaSwitchingController.mCallback = this.mCallback;
 
-        mMediaOutputController.launchBluetoothPairing(mDialogLaunchView);
+        mMediaSwitchingController.launchBluetoothPairing(mDialogLaunchView);
 
         verify(mCallback).dismissDialog();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index 70af5e7..755adc6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -26,7 +26,6 @@
 import androidx.compose.ui.test.hasContentDescription
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onChildAt
 import androidx.compose.ui.test.onChildren
 import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithTag
@@ -40,6 +39,7 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.shared.model.TileCategory
@@ -57,7 +57,7 @@
     @Composable
     private fun EditTileGridUnderTest(
         listState: EditTileListState,
-        onSetTiles: (List<TileSpec>) -> Unit
+        onSetTiles: (List<TileSpec>) -> Unit,
     ) {
         DefaultEditTileGrid(
             currentListState = listState,
@@ -182,7 +182,7 @@
     private fun ComposeContentTestRule.assertTileGridContainsExactly(specs: List<String>) {
         onNodeWithTag(CURRENT_TILES_GRID_TEST_TAG).onChildren().apply {
             fetchSemanticsNodes().forEachIndexed { index, _ ->
-                get(index).onChildAt(0).assert(hasContentDescription(specs[index]))
+                get(index).assert(hasContentDescription(specs[index]))
             }
         }
     }
@@ -198,7 +198,7 @@
                     icon =
                         Icon.Resource(
                             android.R.drawable.star_on,
-                            ContentDescription.Loaded(tileSpec)
+                            ContentDescription.Loaded(tileSpec),
                         ),
                     label = AnnotatedString(tileSpec),
                     appName = null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 1e2648b22..ecc7909 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -20,7 +20,6 @@
 import static android.media.AudioManager.RINGER_MODE_SILENT;
 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
 
-import static com.android.systemui.Flags.FLAG_HAPTIC_VOLUME_SLIDER;
 import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
 import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN;
 import static com.android.systemui.volume.VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST;
@@ -51,7 +50,6 @@
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.os.SystemClock;
-import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.provider.Settings;
 import android.testing.TestableLooper;
@@ -285,23 +283,8 @@
     }
 
     @Test
-    @DisableFlags(FLAG_HAPTIC_VOLUME_SLIDER)
-    public void addSliderHaptics_withHapticsDisabled_doesNotDeliverOnProgressChangedHaptics() {
-        // GIVEN that the slider haptics flag is disabled and we try to add haptics to volume rows
-        mDialog.addSliderHapticsToRows();
-
-        // WHEN haptics try to be delivered to a volume stream
-        boolean canDeliverHaptics =
-                mDialog.canDeliverProgressHapticsToStream(AudioSystem.STREAM_MUSIC, true, 50);
-
-        // THEN the result is that haptics are not successfully delivered
-        assertFalse(canDeliverHaptics);
-    }
-
-    @Test
-    @EnableFlags(FLAG_HAPTIC_VOLUME_SLIDER)
-    public void addSliderHaptics_withHapticsEnabled_canDeliverOnProgressChangedHaptics() {
-        // GIVEN that the slider haptics flag is enabled and we try to add haptics to volume rows
+    public void addSliderHaptics_canDeliverOnProgressChangedHaptics() {
+        // GIVEN that the slider haptics are added to rows
         mDialog.addSliderHapticsToRows();
 
         // WHEN haptics try to be delivered to a volume stream
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
index 1ed10fbe..8922b2f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
 import com.android.systemui.flags.fakeSystemPropertiesHelper
 import com.android.systemui.flags.systemPropertiesHelper
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.domain.interactor.trustInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -37,5 +38,6 @@
         powerInteractor = powerInteractor,
         biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor,
         systemPropertiesHelper = fakeSystemPropertiesHelper,
+        keyguardTransitionInteractor = keyguardTransitionInteractor,
     )
 }
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
index 1d2bce2..1df3ef4 100644
--- 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
@@ -19,7 +19,7 @@
 import com.android.systemui.kosmos.Kosmos
 import java.time.Instant
 
-var Kosmos.contextualEducationRepository: ContextualEducationRepository by
+var Kosmos.contextualEducationRepository: FakeContextualEducationRepository by
     Kosmos.Fixture { FakeContextualEducationRepository() }
 
 var Kosmos.fakeEduClock: FakeEduClock by Kosmos.Fixture { 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
index fb4e2fb..4667bf5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt
@@ -17,39 +17,77 @@
 package com.android.systemui.education.data.repository
 
 import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.ALL_APPS
+import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.contextualeducation.GestureType.HOME
+import com.android.systemui.contextualeducation.GestureType.OVERVIEW
 import com.android.systemui.education.data.model.EduDeviceConnectionTime
 import com.android.systemui.education.data.model.GestureEduModel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
 
 class FakeContextualEducationRepository : ContextualEducationRepository {
 
-    private val userGestureMap = mutableMapOf<Int, GestureEduModel>()
-    private val _gestureEduModels = MutableStateFlow(GestureEduModel(userId = 0))
-    private val gestureEduModelsFlow = _gestureEduModels.asStateFlow()
+    private val userGestureMap = mutableMapOf<Int, MutableMap<GestureType, GestureEduModel>>()
+
+    private val _backGestureEduModels = MutableStateFlow(GestureEduModel(BACK, userId = 0))
+    private val backGestureEduModelsFlow = _backGestureEduModels.asStateFlow()
+
+    private val _homeGestureEduModels = MutableStateFlow(GestureEduModel(HOME, userId = 0))
+    private val homeEduModelsFlow = _homeGestureEduModels.asStateFlow()
+
+    private val _allAppsGestureEduModels = MutableStateFlow(GestureEduModel(ALL_APPS, userId = 0))
+    private val allAppsGestureEduModels = _allAppsGestureEduModels.asStateFlow()
+
+    private val _overviewsGestureEduModels = MutableStateFlow(GestureEduModel(OVERVIEW, userId = 0))
+    private val overviewsGestureEduModels = _overviewsGestureEduModels.asStateFlow()
 
     private val userEduDeviceConnectionTimeMap = mutableMapOf<Int, EduDeviceConnectionTime>()
     private val _eduDeviceConnectionTime = MutableStateFlow(EduDeviceConnectionTime())
     private val eduDeviceConnectionTime = _eduDeviceConnectionTime.asStateFlow()
 
+    private val _keyboardShortcutTriggered = MutableStateFlow<GestureType?>(null)
+
     private var currentUser: Int = 0
 
     override fun setUser(userId: Int) {
         if (!userGestureMap.contains(userId)) {
-            userGestureMap[userId] = GestureEduModel(userId = userId)
+            userGestureMap[userId] = createGestureEduModelMap(userId = userId)
             userEduDeviceConnectionTimeMap[userId] = EduDeviceConnectionTime()
         }
         // save data of current user to the map
-        userGestureMap[currentUser] = _gestureEduModels.value
-        userEduDeviceConnectionTimeMap[currentUser] = _eduDeviceConnectionTime.value
+        val currentUserMap = userGestureMap[currentUser]!!
+        currentUserMap[BACK] = _backGestureEduModels.value
+        currentUserMap[HOME] = _homeGestureEduModels.value
+        currentUserMap[ALL_APPS] = _allAppsGestureEduModels.value
+        currentUserMap[OVERVIEW] = _overviewsGestureEduModels.value
+
         // switch to data of new user
-        _gestureEduModels.value = userGestureMap[userId]!!
+        val newUserGestureMap = userGestureMap[userId]!!
+        newUserGestureMap[BACK]?.let { _backGestureEduModels.value = it }
+        newUserGestureMap[HOME]?.let { _homeGestureEduModels.value = it }
+        newUserGestureMap[ALL_APPS]?.let { _allAppsGestureEduModels.value = it }
+        newUserGestureMap[OVERVIEW]?.let { _overviewsGestureEduModels.value = it }
+
+        userEduDeviceConnectionTimeMap[currentUser] = _eduDeviceConnectionTime.value
         _eduDeviceConnectionTime.value = userEduDeviceConnectionTimeMap[userId]!!
     }
 
+    private fun createGestureEduModelMap(userId: Int): MutableMap<GestureType, GestureEduModel> {
+        val gestureModelMap = mutableMapOf<GestureType, GestureEduModel>()
+        GestureType.values().forEach { gestureModelMap[it] = GestureEduModel(it, userId = userId) }
+        return gestureModelMap
+    }
+
     override fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> {
-        return gestureEduModelsFlow
+        return when (gestureType) {
+            BACK -> backGestureEduModelsFlow
+            HOME -> homeEduModelsFlow
+            ALL_APPS -> allAppsGestureEduModels
+            OVERVIEW -> overviewsGestureEduModels
+        }
     }
 
     override fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime> {
@@ -60,8 +98,16 @@
         gestureType: GestureType,
         transform: (GestureEduModel) -> GestureEduModel
     ) {
-        val currentModel = _gestureEduModels.value
-        _gestureEduModels.value = transform(currentModel)
+        val gestureModels =
+            when (gestureType) {
+                BACK -> _backGestureEduModels
+                HOME -> _homeGestureEduModels
+                ALL_APPS -> _allAppsGestureEduModels
+                OVERVIEW -> _overviewsGestureEduModels
+            }
+
+        val currentModel = gestureModels.value
+        gestureModels.value = transform(currentModel)
     }
 
     override suspend fun updateEduDeviceConnectionTime(
@@ -70,4 +116,11 @@
         val currentModel = _eduDeviceConnectionTime.value
         _eduDeviceConnectionTime.value = transform(currentModel)
     }
+
+    override val keyboardShortcutTriggered: Flow<GestureType>
+        get() = _keyboardShortcutTriggered.filterNotNull()
+
+    fun setKeyboardShortcutTriggered(gestureType: GestureType) {
+        _keyboardShortcutTriggered.value = gestureType
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
index 80f6fc2..2d275f9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
@@ -40,8 +40,7 @@
                     touchpadRepository,
                     userRepository
                 ),
-            clock = fakeEduClock,
-            inputManager = mockEduInputManager
+            clock = fakeEduClock
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
index c252924..c0152b26 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.flags
 
 import android.platform.test.annotations.EnableFlags
-import com.android.systemui.Flags.FLAG_COMPOSE_LOCKSCREEN
 import com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR
 import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
 import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
@@ -31,7 +30,6 @@
  * that feature. It is also picked up by [SceneContainerRule] to set non-aconfig prerequisites.
  */
 @EnableFlags(
-    FLAG_COMPOSE_LOCKSCREEN,
     FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
     FLAG_KEYGUARD_WM_STATE_REFACTOR,
     FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt
index 5ad973a..2b81da3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt
@@ -20,8 +20,10 @@
 import com.google.android.msdl.data.model.MSDLToken
 import com.google.android.msdl.domain.InteractionProperties
 import com.google.android.msdl.domain.MSDLPlayer
+import com.google.android.msdl.logging.MSDLEvent
 
 class FakeMSDLPlayer : MSDLPlayer {
+    private val history = arrayListOf<MSDLEvent>()
     var currentFeedbackLevel = FeedbackLevel.DEFAULT
     var latestTokenPlayed: MSDLToken? = null
         private set
@@ -34,5 +36,8 @@
     override fun playToken(token: MSDLToken, properties: InteractionProperties?) {
         latestTokenPlayed = token
         latestPropertiesPlayed = properties
+        history.add(MSDLEvent(token, properties))
     }
+
+    override fun getHistory(): List<MSDLEvent> = history
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
index ca748b66..80db1e9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.haptics.qs
 
+import com.android.systemui.classifier.fakeFalsingManager
 import com.android.systemui.haptics.vibratorHelper
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.log.core.FakeLogBuffer
@@ -26,6 +27,7 @@
         QSLongPressEffect(
             vibratorHelper,
             keyguardStateController,
+            fakeFalsingManager,
             FakeLogBuffer.Factory.create(),
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorKosmos.kt
deleted file mode 100644
index edbc4c1..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorKosmos.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.log.core.FakeLogBuffer
-import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
-
-val Kosmos.gridConsistencyInteractor by
-    Kosmos.Fixture {
-        GridConsistencyInteractor(
-            gridLayoutTypeInteractor,
-            currentTilesInteractor,
-            gridConsistencyInteractorsMap,
-            noopGridConsistencyInteractor,
-            FakeLogBuffer.Factory.create(),
-            applicationCoroutineScope,
-        )
-    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt
index 34e99d3..c951642 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt
@@ -27,6 +27,3 @@
 
 val Kosmos.gridLayoutMap: Map<GridLayoutType, GridLayout> by
     Kosmos.Fixture { mapOf(Pair(InfiniteGridLayoutType, infiniteGridLayout)) }
-
-var Kosmos.gridConsistencyInteractorsMap: Map<GridLayoutType, GridTypeConsistencyInteractor> by
-    Kosmos.Fixture { mapOf(Pair(InfiniteGridLayoutType, infiniteGridConsistencyInteractor)) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
deleted file mode 100644
index 320c2ec..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.infiniteGridConsistencyInteractor by
-    Kosmos.Fixture {
-        InfiniteGridConsistencyInteractor(iconTilesInteractor, fixedColumnsSizeInteractor)
-    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index be00152..3f62b4d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.qs.panels.domain.interactor
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
 import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractorKosmos.kt
deleted file mode 100644
index e3beff7..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractorKosmos.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.panels.domain.interactor
-
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.noopGridConsistencyInteractor by Kosmos.Fixture { NoopGridConsistencyInteractor() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt
index c416ea1..91602c2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt
@@ -16,8 +16,13 @@
 
 package com.android.systemui.statusbar.data.repository
 
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
 
 class FakeRemoteInputRepository : RemoteInputRepository {
     override val isRemoteInputActive = MutableStateFlow(false)
+    override val remoteInputRowBottomBound: Flow<Float?> = flowOf(null)
+
+    override fun setRemoteInputRowBottomBound(bottom: Float?) {}
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt
index 6370a5d..7244d46 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
 
 val Kosmos.notificationScrollViewModel by Fixture {
@@ -29,6 +30,7 @@
         dumpManager = dumpManager,
         stackAppearanceInteractor = notificationStackAppearanceInteractor,
         shadeInteractor = shadeInteractor,
+        remoteInputInteractor = remoteInputInteractor,
         sceneInteractor = sceneInteractor,
         keyguardInteractor = { keyguardInteractor },
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
index 8bfc390..e5cf0a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
 
@@ -31,6 +32,7 @@
         sceneInteractor = sceneInteractor,
         shadeInteractor = shadeInteractor,
         headsUpNotificationInteractor = headsUpNotificationInteractor,
+        remoteInputInteractor = remoteInputInteractor,
         featureFlags = featureFlagsClassic,
         dumpManager = dumpManager,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
index 61b53c9..99cd830 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
@@ -22,6 +22,8 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository
+import com.android.systemui.statusbar.policy.data.repository.deviceProvisioningRepository
+import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
 import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 
 val Kosmos.zenModeInteractor by Fixture {
@@ -31,5 +33,7 @@
         notificationSettingsRepository = notificationSettingsRepository,
         bgDispatcher = testDispatcher,
         iconLoader = zenIconLoader,
+        deviceProvisioningRepository = deviceProvisioningRepository,
+        userSetupRepository = userSetupRepository,
     )
 }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index d1a3bf9..10e4f38 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -343,6 +343,8 @@
     data: [
         ":framework-res",
         ":ravenwood-empty-res",
+        ":framework-platform-compat-config",
+        ":services-platform-compat-config",
     ],
     libs: [
         "100-framework-minus-apex.ravenwood",
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java
index c3b7087..1f98334 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java
@@ -16,7 +16,15 @@
 
 package com.android.server.appfunctions;
 
+import android.annotation.NonNull;
+import android.os.UserHandle;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
 import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -33,5 +41,50 @@
                     /* unit= */ TimeUnit.SECONDS,
                     /* workQueue= */ new LinkedBlockingQueue<>());
 
+    /** A map of per-user executors for queued work. */
+    @GuardedBy("sLock")
+    private static final SparseArray<ExecutorService> mPerUserExecutorsLocked = new SparseArray<>();
+
+    private static final Object sLock = new Object();
+
+    /**
+     * Returns a per-user executor for queued metadata sync request.
+     *
+     * <p>The work submitted to these executor (Sync request) needs to be synchronous per user hence
+     * the use of a single thread.
+     *
+     * <p>Note: Use a different executor if not calling {@code submitSyncRequest} on a {@code
+     * MetadataSyncAdapter}.
+     */
+    // TODO(b/357551503): Restrict the scope of this executor to the MetadataSyncAdapter itself.
+    public static ExecutorService getPerUserSyncExecutor(@NonNull UserHandle user) {
+        synchronized (sLock) {
+            ExecutorService executor = mPerUserExecutorsLocked.get(user.getIdentifier(), null);
+            if (executor == null) {
+                executor = Executors.newSingleThreadExecutor();
+                mPerUserExecutorsLocked.put(user.getIdentifier(), executor);
+            }
+            return executor;
+        }
+    }
+
+    /**
+     * Shuts down and removes the per-user executor for queued work.
+     *
+     * <p>This should be called when the user is removed.
+     */
+    public static void shutDownAndRemoveUserExecutor(@NonNull UserHandle user)
+            throws InterruptedException {
+        ExecutorService executor;
+        synchronized (sLock) {
+            executor = mPerUserExecutorsLocked.get(user.getIdentifier());
+            mPerUserExecutorsLocked.remove(user.getIdentifier());
+        }
+        if (executor != null) {
+            executor.shutdown();
+            var unused = executor.awaitTermination(30, TimeUnit.SECONDS);
+        }
+    }
+
     private AppFunctionExecutors() {}
 }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
index 02800cb..c293087 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.appfunctions;
 
+import android.annotation.NonNull;
 import android.app.appfunctions.AppFunctionManagerConfiguration;
 import android.content.Context;
 
@@ -36,4 +37,14 @@
             publishBinderService(Context.APP_FUNCTION_SERVICE, mServiceImpl);
         }
     }
+
+    @Override
+    public void onUserUnlocked(@NonNull TargetUser user) {
+        mServiceImpl.onUserUnlocked(user);
+    }
+
+    @Override
+    public void onUserStopping(@NonNull TargetUser user) {
+        mServiceImpl.onUserStopping(user);
+    }
 }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 2362b91..cf039df 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -19,29 +19,35 @@
 import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_EXECUTOR;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appfunctions.AppFunctionStaticMetadataHelper;
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
 import android.app.appfunctions.ExecuteAppFunctionResponse;
 import android.app.appfunctions.IAppFunctionManager;
 import android.app.appfunctions.IAppFunctionService;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
 import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.observer.DocumentChangeInfo;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.app.appsearch.observer.SchemaChangeInfo;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Slog;
-import android.app.appsearch.AppSearchResult;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService.TargetUser;
 import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
 import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
 
+import java.io.IOException;
 import java.util.Objects;
 import java.util.concurrent.CompletionException;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 
 /** Implementation of the AppFunctionManagerService. */
 public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
@@ -51,9 +57,11 @@
     private final CallerValidator mCallerValidator;
     private final ServiceHelper mInternalServiceHelper;
     private final ServiceConfig mServiceConfig;
+    private final Context mContext;
 
     public AppFunctionManagerServiceImpl(@NonNull Context context) {
         this(
+                context,
                 new RemoteServiceCallerImpl<>(
                         context, IAppFunctionService.Stub::asInterface, THREAD_POOL_EXECUTOR),
                 new CallerValidatorImpl(context),
@@ -63,10 +71,12 @@
 
     @VisibleForTesting
     AppFunctionManagerServiceImpl(
+            Context context,
             RemoteServiceCaller<IAppFunctionService> remoteServiceCaller,
             CallerValidator callerValidator,
             ServiceHelper appFunctionInternalServiceHelper,
             ServiceConfig serviceConfig) {
+        mContext = Objects.requireNonNull(context);
         mRemoteServiceCaller = Objects.requireNonNull(remoteServiceCaller);
         mCallerValidator = Objects.requireNonNull(callerValidator);
         mInternalServiceHelper = Objects.requireNonNull(appFunctionInternalServiceHelper);
@@ -90,6 +100,26 @@
         }
     }
 
+    /** Called when the user is unlocked. */
+    public void onUserUnlocked(TargetUser user) {
+        Objects.requireNonNull(user);
+
+        registerAppSearchObserver(user);
+        trySyncRuntimeMetadata(user);
+    }
+
+    /** Called when the user is stopping. */
+    public void onUserStopping(@NonNull TargetUser user) {
+        Objects.requireNonNull(user);
+
+        try {
+            AppFunctionExecutors.shutDownAndRemoveUserExecutor(user.getUserHandle());
+            MetadataSyncPerUser.removeUserSyncAdapter(user.getUserHandle());
+        } catch (InterruptedException e) {
+            Slog.e(TAG, "Unable to remove data for: " + user.getUserHandle(), e);
+        }
+    }
+
     private void executeAppFunctionInternal(
             ExecuteAppFunctionAidlRequest requestInternal,
             SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) {
@@ -132,53 +162,55 @@
             return;
         }
 
-        var unused = mCallerValidator
-                .verifyCallerCanExecuteAppFunction(
-                        validatedCallingPackage,
-                        targetPackageName,
-                        requestInternal.getClientRequest().getFunctionIdentifier())
-                .thenAccept(
-                        canExecute -> {
-                            if (!canExecute) {
-                                safeExecuteAppFunctionCallback.onResult(
-                                        ExecuteAppFunctionResponse.newFailure(
-                                                ExecuteAppFunctionResponse.RESULT_DENIED,
-                                                "Caller does not have permission to execute the"
-                                                        + " appfunction",
-                                                /* extras= */ null));
-                                return;
-                            }
-                            Intent serviceIntent =
-                                    mInternalServiceHelper.resolveAppFunctionService(
-                                            targetPackageName, targetUser);
-                            if (serviceIntent == null) {
-                                safeExecuteAppFunctionCallback.onResult(
-                                        ExecuteAppFunctionResponse.newFailure(
-                                                ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
-                                                "Cannot find the target service.",
-                                                /* extras= */ null));
-                                return;
-                            }
-                            final long token = Binder.clearCallingIdentity();
-                            try {
-                                bindAppFunctionServiceUnchecked(
-                                        requestInternal,
-                                        serviceIntent,
-                                        targetUser,
-                                        safeExecuteAppFunctionCallback,
-                                        /* bindFlags= */ Context.BIND_AUTO_CREATE,
-                                        /* timeoutInMillis= */ mServiceConfig
-                                                .getExecuteAppFunctionTimeoutMillis());
-                            } finally {
-                                Binder.restoreCallingIdentity(token);
-                            }
-                        })
-                .exceptionally(
-                        ex -> {
-                            safeExecuteAppFunctionCallback.onResult(
-                                    mapExceptionToExecuteAppFunctionResponse(ex));
-                            return null;
-                        });
+        var unused =
+                mCallerValidator
+                        .verifyCallerCanExecuteAppFunction(
+                                validatedCallingPackage,
+                                targetPackageName,
+                                requestInternal.getClientRequest().getFunctionIdentifier())
+                        .thenAccept(
+                                canExecute -> {
+                                    if (!canExecute) {
+                                        safeExecuteAppFunctionCallback.onResult(
+                                                ExecuteAppFunctionResponse.newFailure(
+                                                        ExecuteAppFunctionResponse.RESULT_DENIED,
+                                                        "Caller does not have permission to execute"
+                                                                + " the appfunction",
+                                                        /* extras= */ null));
+                                        return;
+                                    }
+                                    Intent serviceIntent =
+                                            mInternalServiceHelper.resolveAppFunctionService(
+                                                    targetPackageName, targetUser);
+                                    if (serviceIntent == null) {
+                                        safeExecuteAppFunctionCallback.onResult(
+                                                ExecuteAppFunctionResponse.newFailure(
+                                                        ExecuteAppFunctionResponse
+                                                                .RESULT_INTERNAL_ERROR,
+                                                        "Cannot find the target service.",
+                                                        /* extras= */ null));
+                                        return;
+                                    }
+                                    final long token = Binder.clearCallingIdentity();
+                                    try {
+                                        bindAppFunctionServiceUnchecked(
+                                                requestInternal,
+                                                serviceIntent,
+                                                targetUser,
+                                                safeExecuteAppFunctionCallback,
+                                                /* bindFlags= */ Context.BIND_AUTO_CREATE,
+                                                /* timeoutInMillis= */ mServiceConfig
+                                                        .getExecuteAppFunctionTimeoutMillis());
+                                    } finally {
+                                        Binder.restoreCallingIdentity(token);
+                                    }
+                                })
+                        .exceptionally(
+                                ex -> {
+                                    safeExecuteAppFunctionCallback.onResult(
+                                            mapExceptionToExecuteAppFunctionResponse(ex));
+                                    return null;
+                                });
     }
 
     private void bindAppFunctionServiceUnchecked(
@@ -256,7 +288,7 @@
     }
 
     private ExecuteAppFunctionResponse mapExceptionToExecuteAppFunctionResponse(Throwable e) {
-        if(e instanceof CompletionException) {
+        if (e instanceof CompletionException) {
             e = e.getCause();
         }
 
@@ -291,4 +323,103 @@
         }
         return ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR;
     }
+
+    private void registerAppSearchObserver(@NonNull TargetUser user) {
+        AppSearchManager perUserAppSearchManager =
+                mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0)
+                        .getSystemService(AppSearchManager.class);
+        if (perUserAppSearchManager == null) {
+            Slog.d(TAG, "AppSearch Manager not found for user: " + user.getUserIdentifier());
+            return;
+        }
+        try (FutureGlobalSearchSession futureGlobalSearchSession =
+                new FutureGlobalSearchSession(
+                        perUserAppSearchManager, AppFunctionExecutors.THREAD_POOL_EXECUTOR)) {
+            AppFunctionMetadataObserver appFunctionMetadataObserver =
+                    new AppFunctionMetadataObserver(
+                            user.getUserHandle(),
+                            mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0));
+            var unused =
+                    futureGlobalSearchSession
+                            .registerObserverCallbackAsync(
+                                    "android",
+                                    new ObserverSpec.Builder().build(),
+                                    THREAD_POOL_EXECUTOR,
+                                    appFunctionMetadataObserver)
+                            .whenComplete(
+                                    (voidResult, ex) -> {
+                                        if (ex != null) {
+                                            Slog.e(TAG, "Failed to register observer: ", ex);
+                                        }
+                                    });
+
+        } catch (IOException ex) {
+            Slog.e(TAG, "Failed to close observer session: ", ex);
+        }
+    }
+
+    private void trySyncRuntimeMetadata(@NonNull TargetUser user) {
+        MetadataSyncAdapter metadataSyncAdapter =
+                MetadataSyncPerUser.getPerUserMetadataSyncAdapter(
+                        user.getUserHandle(),
+                        mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0));
+        if (metadataSyncAdapter != null) {
+            var unused =
+                    metadataSyncAdapter
+                            .submitSyncRequest()
+                            .whenComplete(
+                                    (isSuccess, ex) -> {
+                                        if (ex != null || !isSuccess) {
+                                            Slog.e(TAG, "Sync was not successful");
+                                        }
+                                    });
+        }
+    }
+
+    private static class AppFunctionMetadataObserver implements ObserverCallback {
+        @Nullable private final MetadataSyncAdapter mPerUserMetadataSyncAdapter;
+
+        AppFunctionMetadataObserver(@NonNull UserHandle userHandle, @NonNull Context userContext) {
+            mPerUserMetadataSyncAdapter =
+                    MetadataSyncPerUser.getPerUserMetadataSyncAdapter(userHandle, userContext);
+        }
+
+        @Override
+        public void onDocumentChanged(@NonNull DocumentChangeInfo documentChangeInfo) {
+            if (mPerUserMetadataSyncAdapter == null) {
+                return;
+            }
+            if (documentChangeInfo
+                            .getDatabaseName()
+                            .equals(AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)
+                    && documentChangeInfo
+                            .getNamespace()
+                            .equals(
+                                    AppFunctionStaticMetadataHelper
+                                            .APP_FUNCTION_STATIC_NAMESPACE)) {
+                var unused = mPerUserMetadataSyncAdapter.submitSyncRequest();
+            }
+        }
+
+        @Override
+        public void onSchemaChanged(@NonNull SchemaChangeInfo schemaChangeInfo) {
+            if (mPerUserMetadataSyncAdapter == null) {
+                return;
+            }
+            if (schemaChangeInfo
+                    .getDatabaseName()
+                    .equals(AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)) {
+                boolean shouldInitiateSync = false;
+                for (String schemaName : schemaChangeInfo.getChangedSchemaNames()) {
+                    if (schemaName.startsWith(AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE)) {
+                        shouldInitiateSync = true;
+                        break;
+                    }
+                }
+                if (shouldInitiateSync) {
+                    var unused = mPerUserMetadataSyncAdapter.submitSyncRequest();
+                }
+            }
+        }
+    }
 }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
index e2573590..8c6f50e 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -24,6 +24,8 @@
 import android.app.appfunctions.AppFunctionRuntimeMetadata;
 import android.app.appfunctions.AppFunctionStaticMetadataHelper;
 import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchManager.SearchContext;
 import android.app.appsearch.AppSearchResult;
 import android.app.appsearch.AppSearchSchema;
 import android.app.appsearch.PackageIdentifier;
@@ -61,9 +63,8 @@
  */
 public class MetadataSyncAdapter {
     private static final String TAG = MetadataSyncAdapter.class.getSimpleName();
-    private final FutureAppSearchSession mRuntimeMetadataSearchSession;
-    private final FutureAppSearchSession mStaticMetadataSearchSession;
     private final Executor mSyncExecutor;
+    private final AppSearchManager mAppSearchManager;
     private final PackageManager mPackageManager;
 
     // Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility
@@ -73,13 +74,11 @@
 
     public MetadataSyncAdapter(
             @NonNull Executor syncExecutor,
-            @NonNull FutureAppSearchSession runtimeMetadataSearchSession,
-            @NonNull FutureAppSearchSession staticMetadataSearchSession,
-            @NonNull PackageManager packageManager) {
+            @NonNull PackageManager packageManager,
+            @NonNull AppSearchManager appSearchManager) {
         mSyncExecutor = Objects.requireNonNull(syncExecutor);
-        mRuntimeMetadataSearchSession = Objects.requireNonNull(runtimeMetadataSearchSession);
-        mStaticMetadataSearchSession = Objects.requireNonNull(staticMetadataSearchSession);
         mPackageManager = Objects.requireNonNull(packageManager);
+        mAppSearchManager = Objects.requireNonNull(appSearchManager);
     }
 
     /**
@@ -89,31 +88,54 @@
      *     synchronization was successful.
      */
     public AndroidFuture<Boolean> submitSyncRequest() {
+        SearchContext staticMetadataSearchContext =
+                new SearchContext.Builder(
+                                AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)
+                        .build();
+        SearchContext runtimeMetadataSearchContext =
+                new SearchContext.Builder(
+                                AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB)
+                        .build();
         AndroidFuture<Boolean> settableSyncStatus = new AndroidFuture<>();
         mSyncExecutor.execute(
                 () -> {
-                    try {
-                        trySyncAppFunctionMetadataBlocking();
+                    try (FutureAppSearchSession staticMetadataSearchSession =
+                                    new FutureAppSearchSessionImpl(
+                                            mAppSearchManager,
+                                            AppFunctionExecutors.THREAD_POOL_EXECUTOR,
+                                            staticMetadataSearchContext);
+                            FutureAppSearchSession runtimeMetadataSearchSession =
+                                    new FutureAppSearchSessionImpl(
+                                            mAppSearchManager,
+                                            AppFunctionExecutors.THREAD_POOL_EXECUTOR,
+                                            runtimeMetadataSearchContext)) {
+
+                        trySyncAppFunctionMetadataBlocking(
+                                staticMetadataSearchSession, runtimeMetadataSearchSession);
                         settableSyncStatus.complete(true);
-                    } catch (Exception e) {
-                        settableSyncStatus.completeExceptionally(e);
+
+                    } catch (Exception ex) {
+                        settableSyncStatus.completeExceptionally(ex);
                     }
                 });
         return settableSyncStatus;
     }
 
     @WorkerThread
-    private void trySyncAppFunctionMetadataBlocking()
+    @VisibleForTesting
+    void trySyncAppFunctionMetadataBlocking(
+            @NonNull FutureAppSearchSession staticMetadataSearchSession,
+            @NonNull FutureAppSearchSession runtimeMetadataSearchSession)
             throws ExecutionException, InterruptedException {
         ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap =
                 getPackageToFunctionIdMap(
-                        mStaticMetadataSearchSession,
+                        staticMetadataSearchSession,
                         AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE,
                         AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID,
                         AppFunctionStaticMetadataHelper.PROPERTY_PACKAGE_NAME);
         ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap =
                 getPackageToFunctionIdMap(
-                        mRuntimeMetadataSearchSession,
+                        runtimeMetadataSearchSession,
                         RUNTIME_SCHEMA_TYPE,
                         AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID,
                         AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME);
@@ -134,7 +156,7 @@
             RemoveByDocumentIdRequest removeByDocumentIdRequest =
                     buildRemoveRuntimeMetadataRequest(removedFunctionsDiffMap);
             AppSearchBatchResult<String, Void> removeDocumentBatchResult =
-                    mRuntimeMetadataSearchSession.remove(removeByDocumentIdRequest).get();
+                    runtimeMetadataSearchSession.remove(removeByDocumentIdRequest).get();
             if (!removeDocumentBatchResult.isSuccess()) {
                 throw convertFailedAppSearchResultToException(
                         removeDocumentBatchResult.getFailures().values());
@@ -144,13 +166,14 @@
         if (!addedFunctionsDiffMap.isEmpty()) {
             // TODO(b/357551503): only set schema on package diff
             SetSchemaRequest addSetSchemaRequest =
-                    buildSetSchemaRequestForRuntimeMetadataSchemas(appRuntimeMetadataSchemas);
+                    buildSetSchemaRequestForRuntimeMetadataSchemas(
+                            mPackageManager, appRuntimeMetadataSchemas);
             Objects.requireNonNull(
-                    mRuntimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
+                    runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
             PutDocumentsRequest putDocumentsRequest =
                     buildPutRuntimeMetadataRequest(addedFunctionsDiffMap);
             AppSearchBatchResult<String, Void> putDocumentBatchResult =
-                    mRuntimeMetadataSearchSession.put(putDocumentsRequest).get();
+                    runtimeMetadataSearchSession.put(putDocumentsRequest).get();
             if (!putDocumentBatchResult.isSuccess()) {
                 throw convertFailedAppSearchResultToException(
                         putDocumentBatchResult.getFailures().values());
@@ -211,6 +234,7 @@
 
     @NonNull
     private SetSchemaRequest buildSetSchemaRequestForRuntimeMetadataSchemas(
+            @NonNull PackageManager packageManager,
             @NonNull Set<AppSearchSchema> metadataSchemaSet) {
         Objects.requireNonNull(metadataSchemaSet);
         SetSchemaRequest.Builder setSchemaRequestBuilder =
@@ -220,7 +244,7 @@
             String packageName =
                     AppFunctionRuntimeMetadata.getPackageNameFromSchema(
                             runtimeMetadataSchema.getSchemaType());
-            byte[] packageCert = getCertificate(packageName);
+            byte[] packageCert = getCertificate(packageManager, packageName);
             if (packageCert == null) {
                 continue;
             }
@@ -399,13 +423,15 @@
 
     /** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found. */
     @Nullable
-    private byte[] getCertificate(@NonNull String packageName) {
+    private byte[] getCertificate(
+            @NonNull PackageManager packageManager, @NonNull String packageName) {
+        Objects.requireNonNull(packageManager);
         Objects.requireNonNull(packageName);
         PackageInfo packageInfo;
         try {
             packageInfo =
                     Objects.requireNonNull(
-                            mPackageManager.getPackageInfo(
+                            packageManager.getPackageInfo(
                                     packageName,
                                     PackageManager.GET_META_DATA
                                             | PackageManager.GET_SIGNING_CERTIFICATES));
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java
new file mode 100644
index 0000000..f421527
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+/** A Singleton class that manages per-user metadata sync adapters. */
+public final class MetadataSyncPerUser {
+    private static final String TAG = MetadataSyncPerUser.class.getSimpleName();
+
+    /** A map of per-user adapter for synchronizing appFunction metadata. */
+    @GuardedBy("sLock")
+    private static final SparseArray<MetadataSyncAdapter> sPerUserMetadataSyncAdapter =
+            new SparseArray<>();
+
+    private static final Object sLock = new Object();
+
+    /**
+     * Returns the per-user metadata sync adapter for the given user.
+     *
+     * @param user The user for which to get the metadata sync adapter.
+     * @param userContext The user context for the given user.
+     * @return The metadata sync adapter for the given user.
+     */
+    @Nullable
+    public static MetadataSyncAdapter getPerUserMetadataSyncAdapter(
+            UserHandle user, Context userContext) {
+        synchronized (sLock) {
+            MetadataSyncAdapter metadataSyncAdapter =
+                    sPerUserMetadataSyncAdapter.get(user.getIdentifier(), null);
+            if (metadataSyncAdapter == null) {
+                AppSearchManager perUserAppSearchManager =
+                        userContext.getSystemService(AppSearchManager.class);
+                PackageManager perUserPackageManager = userContext.getPackageManager();
+                if (perUserAppSearchManager != null) {
+                    metadataSyncAdapter =
+                            new MetadataSyncAdapter(
+                                    AppFunctionExecutors.getPerUserSyncExecutor(user),
+                                    perUserPackageManager,
+                                    perUserAppSearchManager);
+                    sPerUserMetadataSyncAdapter.put(user.getIdentifier(), metadataSyncAdapter);
+                    return metadataSyncAdapter;
+                }
+            }
+            return metadataSyncAdapter;
+        }
+    }
+
+    /**
+     * Removes the per-user metadata sync adapter for the given user.
+     *
+     * @param user The user for which to remove the metadata sync adapter.
+     */
+    public static void removeUserSyncAdapter(UserHandle user) {
+        synchronized (sLock) {
+            sPerUserMetadataSyncAdapter.remove(user.getIdentifier());
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index e145c90..55d9c6e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -533,7 +533,8 @@
             AudioDeviceInfo.TYPE_BLE_SPEAKER,
             AudioDeviceInfo.TYPE_LINE_ANALOG,
             AudioDeviceInfo.TYPE_HDMI,
-            AudioDeviceInfo.TYPE_AUX_LINE
+            AudioDeviceInfo.TYPE_AUX_LINE,
+            AudioDeviceInfo.TYPE_BUS
     };
 
     /*package */ static boolean isValidCommunicationDevice(@NonNull AudioDeviceInfo device) {
diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java
index 38eb416..ddea285 100644
--- a/services/core/java/com/android/server/display/DisplayControl.java
+++ b/services/core/java/com/android/server/display/DisplayControl.java
@@ -109,7 +109,7 @@
     /**
      * Sets the HDR conversion mode for the device.
      *
-     * Returns the system preferred Hdr output type nn case when HDR conversion mode is
+     * Returns the system preferred HDR output type in case when HDR conversion mode is
      * {@link android.hardware.display.HdrConversionMode#HDR_CONVERSION_SYSTEM}.
      * Returns Hdr::INVALID in other cases.
      * @hide
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index 93bd926..acf4db3 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -318,13 +318,16 @@
      */
     public Display.HdrCapabilities hdrCapabilities;
 
+    /** When true, all HDR capabilities are hidden from public APIs */
+    public boolean isForceSdr;
+
     /**
      * Indicates whether this display supports Auto Low Latency Mode.
      */
     public boolean allmSupported;
 
     /**
-     * Indicates whether this display suppors Game content type.
+     * Indicates whether this display supports Game content type.
      */
     public boolean gameContentTypeSupported;
 
@@ -516,6 +519,7 @@
                 || !Arrays.equals(supportedModes, other.supportedModes)
                 || !Arrays.equals(supportedColorModes, other.supportedColorModes)
                 || !Objects.equals(hdrCapabilities, other.hdrCapabilities)
+                || isForceSdr != other.isForceSdr
                 || allmSupported != other.allmSupported
                 || gameContentTypeSupported != other.gameContentTypeSupported
                 || densityDpi != other.densityDpi
@@ -560,6 +564,7 @@
         colorMode = other.colorMode;
         supportedColorModes = other.supportedColorModes;
         hdrCapabilities = other.hdrCapabilities;
+        isForceSdr = other.isForceSdr;
         allmSupported = other.allmSupported;
         gameContentTypeSupported = other.gameContentTypeSupported;
         densityDpi = other.densityDpi;
@@ -603,6 +608,7 @@
         sb.append(", colorMode ").append(colorMode);
         sb.append(", supportedColorModes ").append(Arrays.toString(supportedColorModes));
         sb.append(", hdrCapabilities ").append(hdrCapabilities);
+        sb.append(", isForceSdr ").append(isForceSdr);
         sb.append(", allmSupported ").append(allmSupported);
         sb.append(", gameContentTypeSupported ").append(gameContentTypeSupported);
         sb.append(", density ").append(densityDpi);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3c2167e..e7fd8f7 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -48,6 +48,7 @@
 import static android.provider.Settings.Secure.RESOLUTION_MODE_FULL;
 import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH;
 import static android.provider.Settings.Secure.RESOLUTION_MODE_UNKNOWN;
+import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
 
 import static com.android.server.display.layout.Layout.Display.POSITION_REAR;
 
@@ -284,7 +285,7 @@
     @GuardedBy("mSyncRoot")
     private int[] mUserDisabledHdrTypes = {};
     @Display.HdrCapabilities.HdrType
-    private int[] mSupportedHdrOutputType;
+    private int[] mSupportedHdrOutputTypes;
     @GuardedBy("mSyncRoot")
     private boolean mAreUserDisabledHdrTypesAllowed = true;
 
@@ -299,10 +300,10 @@
     // HDR conversion mode chosen by user
     @GuardedBy("mSyncRoot")
     private HdrConversionMode mHdrConversionMode = null;
-    // Actual HDR conversion mode, which takes app overrides into account.
-    private HdrConversionMode mOverrideHdrConversionMode = null;
+    // Whether app has disabled HDR conversion
+    private boolean mShouldDisableHdrConversion = false;
     @GuardedBy("mSyncRoot")
-    private int mSystemPreferredHdrOutputType = Display.HdrCapabilities.HDR_TYPE_INVALID;
+    private int mSystemPreferredHdrOutputType = HDR_TYPE_INVALID;
 
 
     // The synchronization root for the display manager.
@@ -1419,7 +1420,8 @@
         }
     }
 
-    private void setUserDisabledHdrTypesInternal(int[] userDisabledHdrTypes) {
+    @VisibleForTesting
+    void setUserDisabledHdrTypesInternal(int[] userDisabledHdrTypes) {
         synchronized (mSyncRoot) {
             if (userDisabledHdrTypes == null) {
                 Slog.e(TAG, "Null is not an expected argument to "
@@ -1437,6 +1439,7 @@
             if (Arrays.equals(mUserDisabledHdrTypes, userDisabledHdrTypes)) {
                 return;
             }
+
             String userDisabledFormatsString = "";
             if (userDisabledHdrTypes.length != 0) {
                 userDisabledFormatsString = TextUtils.join(",",
@@ -1452,6 +1455,15 @@
                             handleLogicalDisplayChangedLocked(display);
                         });
             }
+            /* Note: it may be expected to reset the Conversion Mode when an HDR type is enabled
+             and the Conversion Mode is set to System Preferred. This is handled in the Settings
+             code because in the special case where HDR is indirectly disabled by Force SDR
+             Conversion, manually enabling HDR is not recognized as an action that reduces the
+             disabled HDR count. Thus, this case needs to be checked in the Settings code when we
+             know we're enabling an HDR mode. If we split checking for SystemConversion and
+             isForceSdr in two places, we may have duplicate calls to resetting to System Conversion
+             and get two black screens.
+             */
         }
     }
 
@@ -1464,19 +1476,20 @@
         return true;
     }
 
-    private void setAreUserDisabledHdrTypesAllowedInternal(
+    @VisibleForTesting
+    void setAreUserDisabledHdrTypesAllowedInternal(
             boolean areUserDisabledHdrTypesAllowed) {
         synchronized (mSyncRoot) {
             if (mAreUserDisabledHdrTypesAllowed == areUserDisabledHdrTypesAllowed) {
                 return;
             }
             mAreUserDisabledHdrTypesAllowed = areUserDisabledHdrTypesAllowed;
-            if (mUserDisabledHdrTypes.length == 0) {
-                return;
-            }
             Settings.Global.putInt(mContext.getContentResolver(),
                     Settings.Global.ARE_USER_DISABLED_HDR_FORMATS_ALLOWED,
                     areUserDisabledHdrTypesAllowed ? 1 : 0);
+            if (mUserDisabledHdrTypes.length == 0) {
+                return;
+            }
             int userDisabledHdrTypes[] = {};
             if (!mAreUserDisabledHdrTypesAllowed) {
                 userDisabledHdrTypes = mUserDisabledHdrTypes;
@@ -1487,6 +1500,14 @@
                         display.setUserDisabledHdrTypes(finalUserDisabledHdrTypes);
                         handleLogicalDisplayChangedLocked(display);
                     });
+            // When HDR conversion mode is set to SYSTEM, modification to
+            // areUserDisabledHdrTypesAllowed requires refreshing the HDR conversion mode to tell
+            // the system which HDR types it is not allowed to use.
+            if (getHdrConversionModeInternal().getConversionMode()
+                    == HdrConversionMode.HDR_CONVERSION_SYSTEM) {
+                setHdrConversionModeInternal(
+                        new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+            }
         }
     }
 
@@ -2357,7 +2378,7 @@
         final int preferredHdrOutputType =
                 hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_FORCE
                         ? hdrConversionMode.getPreferredHdrOutputType()
-                        : Display.HdrCapabilities.HDR_TYPE_INVALID;
+                        : HDR_TYPE_INVALID;
         Settings.Global.putInt(mContext.getContentResolver(),
                 Settings.Global.HDR_FORCE_CONVERSION_TYPE, preferredHdrOutputType);
     }
@@ -2370,7 +2391,7 @@
                 ? Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.HDR_FORCE_CONVERSION_TYPE,
                         Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION)
-                : Display.HdrCapabilities.HDR_TYPE_INVALID;
+                : HDR_TYPE_INVALID;
         mHdrConversionMode = new HdrConversionMode(conversionMode, preferredHdrOutputType);
         setHdrConversionModeInternal(mHdrConversionMode);
     }
@@ -2507,22 +2528,38 @@
         });
     }
 
+    /**
+     * Returns the HDR output types that are supported by the device's HDR conversion capabilities,
+     * stripping out any user-disabled HDR types if mAreUserDisabledHdrTypesAllowed is false.
+     */
     @GuardedBy("mSyncRoot")
-    private int[] getEnabledAutoHdrTypesLocked() {
-        IntArray autoHdrOutputTypesArray = new IntArray();
+    @VisibleForTesting
+    int[] getEnabledHdrOutputTypesLocked() {
+        if (mAreUserDisabledHdrTypesAllowed) {
+            return getSupportedHdrOutputTypesInternal();
+        }
+        // Strip out all HDR formats that are currently user-disabled
+        IntArray enabledHdrOutputTypesArray = new IntArray();
         for (int type : getSupportedHdrOutputTypesInternal()) {
-            boolean isDisabled = false;
+            boolean isEnabled = true;
             for (int disabledType : mUserDisabledHdrTypes) {
                 if (type == disabledType) {
-                    isDisabled = true;
+                    isEnabled = false;
                     break;
                 }
             }
-            if (!isDisabled) {
-                autoHdrOutputTypesArray.add(type);
+            if (isEnabled) {
+                enabledHdrOutputTypesArray.add(type);
             }
         }
-        return autoHdrOutputTypesArray.toArray();
+        return enabledHdrOutputTypesArray.toArray();
+    }
+
+    @VisibleForTesting
+    int[] getEnabledHdrOutputTypes() {
+        synchronized (mSyncRoot) {
+            return getEnabledHdrOutputTypesLocked();
+        }
     }
 
     @GuardedBy("mSyncRoot")
@@ -2531,7 +2568,7 @@
         final int preferredHdrOutputType =
                 mode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM
                         ? mSystemPreferredHdrOutputType : mode.getPreferredHdrOutputType();
-        if (preferredHdrOutputType != Display.HdrCapabilities.HDR_TYPE_INVALID) {
+        if (preferredHdrOutputType != HDR_TYPE_INVALID) {
             int[] hdrTypesWithLatency = mInjector.getHdrOutputTypesWithLatency();
             return ArrayUtils.contains(hdrTypesWithLatency, preferredHdrOutputType);
         }
@@ -2565,41 +2602,57 @@
         if (!mInjector.getHdrOutputConversionSupport()) {
             return;
         }
-        int[] autoHdrOutputTypes = null;
+
         synchronized (mSyncRoot) {
             if (hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM
                     && hdrConversionMode.getPreferredHdrOutputType()
-                    != Display.HdrCapabilities.HDR_TYPE_INVALID) {
+                    != HDR_TYPE_INVALID) {
                 throw new IllegalArgumentException("preferredHdrOutputType must not be set if"
                         + " the conversion mode is HDR_CONVERSION_SYSTEM");
             }
             mHdrConversionMode = hdrConversionMode;
             storeHdrConversionModeLocked(mHdrConversionMode);
 
-            // For auto mode, all supported HDR types are allowed except the ones specifically
-            // disabled by the user.
+            // If the HDR conversion is HDR_CONVERSION_SYSTEM, all supported HDR types are allowed
+            // except the ones specifically disabled by the user.
+            int[] enabledHdrOutputTypes = null;
             if (hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM) {
-                autoHdrOutputTypes = getEnabledAutoHdrTypesLocked();
+                enabledHdrOutputTypes = getEnabledHdrOutputTypesLocked();
             }
 
             int conversionMode = hdrConversionMode.getConversionMode();
             int preferredHdrType = hdrConversionMode.getPreferredHdrOutputType();
+
             // If the HDR conversion is disabled by an app through WindowManager.LayoutParams, then
             // set HDR conversion mode to HDR_CONVERSION_PASSTHROUGH.
-            if (mOverrideHdrConversionMode == null) {
-                // HDR_CONVERSION_FORCE with HDR_TYPE_INVALID is used to represent forcing SDR type.
-                // But, internally SDR is selected by using passthrough mode.
-                if (conversionMode == HdrConversionMode.HDR_CONVERSION_FORCE
-                        && preferredHdrType == Display.HdrCapabilities.HDR_TYPE_INVALID) {
-                    conversionMode = HdrConversionMode.HDR_CONVERSION_PASSTHROUGH;
-                }
+            if (mShouldDisableHdrConversion) {
+                conversionMode = HdrConversionMode.HDR_CONVERSION_PASSTHROUGH;
+                preferredHdrType = -1;
+                enabledHdrOutputTypes = null;
             } else {
-                conversionMode = mOverrideHdrConversionMode.getConversionMode();
-                preferredHdrType = mOverrideHdrConversionMode.getPreferredHdrOutputType();
-                autoHdrOutputTypes = null;
+                // HDR_CONVERSION_FORCE with HDR_TYPE_INVALID is used to represent forcing SDR type.
+                // But, internally SDR is forced by using passthrough mode and not reporting any
+                // HDR capabilities to apps.
+                if (conversionMode == HdrConversionMode.HDR_CONVERSION_FORCE
+                        && preferredHdrType == HDR_TYPE_INVALID) {
+                    conversionMode = HdrConversionMode.HDR_CONVERSION_PASSTHROUGH;
+                    mLogicalDisplayMapper.forEachLocked(
+                            logicalDisplay -> {
+                                if (logicalDisplay.setIsForceSdr(true)) {
+                                    handleLogicalDisplayChangedLocked(logicalDisplay);
+                                }
+                            });
+                } else {
+                    mLogicalDisplayMapper.forEachLocked(
+                            logicalDisplay -> {
+                                if (logicalDisplay.setIsForceSdr(false)) {
+                                    handleLogicalDisplayChangedLocked(logicalDisplay);
+                                }
+                            });
+                }
             }
             mSystemPreferredHdrOutputType = mInjector.setHdrConversionMode(
-                    conversionMode, preferredHdrType, autoHdrOutputTypes);
+                    conversionMode, preferredHdrType, enabledHdrOutputTypes);
         }
     }
 
@@ -2621,8 +2674,8 @@
         }
         HdrConversionMode mode;
         synchronized (mSyncRoot) {
-            mode = mOverrideHdrConversionMode != null
-                    ? mOverrideHdrConversionMode
+            mode = mShouldDisableHdrConversion
+                    ? new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH)
                     : mHdrConversionMode;
             // Handle default: PASSTHROUGH. Don't include the system-preferred type.
             if (mode == null
@@ -2630,8 +2683,6 @@
                 return new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH);
             }
             // Handle default or current mode: SYSTEM. Include the system preferred type.
-            // mOverrideHdrConversionMode and mHdrConversionMode do not include the system
-            // preferred type, it is kept separately in mSystemPreferredHdrOutputType.
             if (mode == null
                     || mode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM) {
                 return new HdrConversionMode(
@@ -2642,10 +2693,10 @@
     }
 
     private @Display.HdrCapabilities.HdrType int[] getSupportedHdrOutputTypesInternal() {
-        if (mSupportedHdrOutputType == null) {
-            mSupportedHdrOutputType = mInjector.getSupportedHdrOutputTypes();
+        if (mSupportedHdrOutputTypes == null) {
+            mSupportedHdrOutputTypes = mInjector.getSupportedHdrOutputTypes();
         }
-        return mSupportedHdrOutputType;
+        return mSupportedHdrOutputTypes;
     }
 
     void setShouldAlwaysRespectAppRequestedModeInternal(boolean enabled) {
@@ -2831,15 +2882,9 @@
             // HDR conversion is disabled in two cases:
             // - HDR conversion introduces latency and minimal post-processing is requested
             // - app requests to disable HDR conversion
-            if (mOverrideHdrConversionMode == null && (disableHdrConversion
-                    || disableHdrConversionForLatency)) {
-                mOverrideHdrConversionMode =
-                            new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH);
-                setHdrConversionModeInternal(mHdrConversionMode);
-                handleLogicalDisplayChangedLocked(display);
-            } else if (mOverrideHdrConversionMode != null && !disableHdrConversion
-                    && !disableHdrConversionForLatency) {
-                mOverrideHdrConversionMode = null;
+            boolean previousShouldDisableHdrConversion = mShouldDisableHdrConversion;
+            mShouldDisableHdrConversion = disableHdrConversion || disableHdrConversionForLatency;
+            if (previousShouldDisableHdrConversion != mShouldDisableHdrConversion) {
                 setHdrConversionModeInternal(mHdrConversionMode);
                 handleLogicalDisplayChangedLocked(display);
             }
@@ -3530,9 +3575,9 @@
         }
 
         int setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
-                int[] autoHdrTypes) {
+                int[] allowedHdrOutputTypes) {
             return DisplayControl.setHdrConversionMode(conversionMode, preferredHdrOutputType,
-                    autoHdrTypes);
+                    allowedHdrOutputTypes);
         }
 
         @Display.HdrCapabilities.HdrType
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index e8be8a4..007e3a8 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -518,6 +518,7 @@
                     deviceInfo.supportedColorModes,
                     deviceInfo.supportedColorModes.length);
             mBaseDisplayInfo.hdrCapabilities = deviceInfo.hdrCapabilities;
+            mBaseDisplayInfo.isForceSdr = deviceInfo.isForceSdr;
             mBaseDisplayInfo.userDisabledHdrTypes = mUserDisabledHdrTypes;
             mBaseDisplayInfo.minimalPostProcessingSupported =
                     deviceInfo.allmSupported || deviceInfo.gameContentTypeSupported;
@@ -899,6 +900,29 @@
     }
 
     /**
+     * Checks whether display is of the type where HDR settings are relevant, and then sets
+     * whether Force SDR conversion mode is active.  isForceSdr is checked by the Display when
+     * returning HDR capabilities.
+     *
+     * @param isForceSdr Whether Force SDR conversion mode is active
+     * @return Whether Display Manager should call handleLogicalDisplayChangedLocked()
+     */
+    public boolean setIsForceSdr(boolean isForceSdr) {
+        int displayType = getDisplayInfoLocked().type;
+        boolean isTargetDisplayType = displayType == Display.TYPE_INTERNAL
+                || displayType == Display.TYPE_EXTERNAL
+                || displayType == Display.TYPE_OVERLAY;
+
+        boolean handleLogicalDisplayChangedLocked = false;
+        if (isTargetDisplayType && mBaseDisplayInfo.isForceSdr != isForceSdr) {
+            mBaseDisplayInfo.isForceSdr = isForceSdr;
+            mInfo.set(null);
+            handleLogicalDisplayChangedLocked = true;
+        }
+        return handleLogicalDisplayChangedLocked;
+    }
+
+    /**
      * Swap the underlying {@link DisplayDevice} with the specified LogicalDisplay.
      *
      * @param targetDisplay The display with which to swap display-devices.
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 164b230..ac75ef7 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1380,7 +1380,8 @@
     @ServiceThreadOnly
     private List<Integer> getCecLocalDeviceTypes() {
         ArrayList<Integer> allLocalDeviceTypes = new ArrayList<>(mCecLocalDevices);
-        if (isDsmEnabled() && !allLocalDeviceTypes.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM)
+        if (!isTvDevice() && isDsmEnabled()
+                && !allLocalDeviceTypes.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM)
                 && isArcSupported() && mSoundbarModeFeatureFlagEnabled) {
             allLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
         }
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index 3f11e78..5ff8568 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -29,7 +29,9 @@
 import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.MotionEvent;
+import android.view.SurfaceControl;
 import android.view.ViewConfiguration;
+import android.view.ViewRootImpl;
 import android.view.WindowManager;
 import android.widget.LinearLayout;
 import android.widget.TextView;
@@ -49,6 +51,7 @@
     private static final float DEFAULT_RES_X = 47f;
     private static final float DEFAULT_RES_Y = 45f;
     private static final int TEXT_PADDING_DP = 12;
+    private static final int ROUNDED_CORNER_RADIUS_DP = 24;
 
     /**
      * Input device ID for the touchpad that this debug view is displaying.
@@ -152,6 +155,30 @@
     }
 
     @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        postDelayed(() -> {
+            final ViewRootImpl viewRootImpl = getRootView().getViewRootImpl();
+            if (viewRootImpl == null) {
+                Slog.d("TouchpadDebugView", "ViewRootImpl is null.");
+                return;
+            }
+
+            SurfaceControl surfaceControl = viewRootImpl.getSurfaceControl();
+            if (surfaceControl != null && surfaceControl.isValid()) {
+                try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
+                    transaction.setCornerRadius(surfaceControl,
+                            TypedValue.applyDimension(COMPLEX_UNIT_DIP,
+                                    ROUNDED_CORNER_RADIUS_DP,
+                                    getResources().getDisplayMetrics())).apply();
+                }
+            } else {
+                Slog.d("TouchpadDebugView", "SurfaceControl is invalid or has been released.");
+            }
+        }, 100);
+    }
+
+    @Override
     public boolean onTouchEvent(MotionEvent event) {
         float deltaX;
         float deltaY;
diff --git a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
index 67c3621..2eed9ba 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
@@ -27,20 +27,28 @@
 import com.android.server.input.TouchpadHardwareProperties;
 import com.android.server.input.TouchpadHardwareState;
 
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+
 public class TouchpadVisualizationView extends View {
     private static final String TAG = "TouchpadVizMain";
     private static final boolean DEBUG = true;
     private static final float DEFAULT_RES_X = 47f;
     private static final float DEFAULT_RES_Y = 45f;
+    private static final float MAX_TRACE_HISTORY_DURATION_SECONDS = 1f;
 
     private final TouchpadHardwareProperties mTouchpadHardwareProperties;
     private float mScaleFactor;
 
-    TouchpadHardwareState mLatestHardwareState = new TouchpadHardwareState(0, 0, 0, 0,
-            new TouchpadFingerState[]{});
+    private final ArrayDeque<TouchpadHardwareState> mHardwareStateHistory =
+            new ArrayDeque<TouchpadHardwareState>();
+    private final Map<Integer, TouchpadFingerState> mTempFingerStatesByTrackingId = new HashMap<>();
 
     private final Paint mOvalStrokePaint;
     private final Paint mOvalFillPaint;
+    private final Paint mTracePaint;
+    private final Paint mCenterPointPaint;
     private final RectF mTempOvalRect = new RectF();
 
     public TouchpadVisualizationView(Context context,
@@ -55,6 +63,29 @@
         mOvalFillPaint = new Paint();
         mOvalFillPaint.setAntiAlias(true);
         mOvalFillPaint.setARGB(255, 0, 0, 0);
+        mTracePaint = new Paint();
+        mTracePaint.setAntiAlias(false);
+        mTracePaint.setARGB(255, 0, 0, 255);
+        mTracePaint.setStyle(Paint.Style.STROKE);
+        mTracePaint.setStrokeWidth(2);
+        mCenterPointPaint = new Paint();
+        mCenterPointPaint.setAntiAlias(true);
+        mCenterPointPaint.setARGB(255, 255, 0, 0);
+        mCenterPointPaint.setStrokeWidth(2);
+    }
+
+    private void removeOldPoints() {
+        float latestTimestamp = mHardwareStateHistory.getLast().getTimestamp();
+
+        while (!mHardwareStateHistory.isEmpty()) {
+            TouchpadHardwareState oldestPoint = mHardwareStateHistory.getFirst();
+            float onScreenTime = latestTimestamp - oldestPoint.getTimestamp();
+            if (onScreenTime >= MAX_TRACE_HISTORY_DURATION_SECONDS) {
+                mHardwareStateHistory.removeFirst();
+            } else {
+                break;
+            }
+        }
     }
 
     private void drawOval(Canvas canvas, float x, float y, float major, float minor, float angle) {
@@ -71,19 +102,22 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
+        if (mHardwareStateHistory.isEmpty()) {
+            return;
+        }
+
+        TouchpadHardwareState latestHardwareState = mHardwareStateHistory.getLast();
+
         float maximumPressure = 0;
-        for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) {
+        for (TouchpadFingerState touchpadFingerState : latestHardwareState.getFingerStates()) {
             maximumPressure = Math.max(maximumPressure, touchpadFingerState.getPressure());
         }
 
-        for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) {
-            float newX = translateRange(mTouchpadHardwareProperties.getLeft(),
-                    mTouchpadHardwareProperties.getRight(), 0, getWidth(),
-                    touchpadFingerState.getPositionX());
+        // Visualizing fingers as ovals
+        for (TouchpadFingerState touchpadFingerState : latestHardwareState.getFingerStates()) {
+            float newX = translateX(touchpadFingerState.getPositionX());
 
-            float newY = translateRange(mTouchpadHardwareProperties.getTop(),
-                    mTouchpadHardwareProperties.getBottom(), 0, getHeight(),
-                    touchpadFingerState.getPositionY());
+            float newY = translateY(touchpadFingerState.getPositionY());
 
             float newAngle = translateRange(0, mTouchpadHardwareProperties.getOrientationMaximum(),
                     0, 90, touchpadFingerState.getOrientation());
@@ -102,6 +136,28 @@
 
             drawOval(canvas, newX, newY, newTouchMajor, newTouchMinor, newAngle);
         }
+
+        mTempFingerStatesByTrackingId.clear();
+
+        // Drawing the trace
+        for (TouchpadHardwareState currentHardwareState : mHardwareStateHistory) {
+            for (TouchpadFingerState currentFingerState : currentHardwareState.getFingerStates()) {
+                TouchpadFingerState prevFingerState = mTempFingerStatesByTrackingId.put(
+                        currentFingerState.getTrackingId(), currentFingerState);
+
+                if (prevFingerState == null) {
+                    continue;
+                }
+
+                float currentX = translateX(currentFingerState.getPositionX());
+                float currentY = translateY(currentFingerState.getPositionY());
+                float prevX = translateX(prevFingerState.getPositionX());
+                float prevY = translateY(prevFingerState.getPositionY());
+
+                canvas.drawLine(prevX, prevY, currentX, currentY, mTracePaint);
+                canvas.drawPoint(currentX, currentY, mCenterPointPaint);
+            }
+        }
     }
 
     /**
@@ -114,7 +170,18 @@
             logHardwareState(schs);
         }
 
-        mLatestHardwareState = schs;
+        if (!mHardwareStateHistory.isEmpty()
+                && mHardwareStateHistory.getLast().getFingerCount() == 0
+                && schs.getFingerCount() > 0) {
+            mHardwareStateHistory.clear();
+        }
+
+        mHardwareStateHistory.addLast(schs);
+        removeOldPoints();
+
+        if (DEBUG) {
+            logFingerTrace();
+        }
 
         invalidate();
     }
@@ -128,6 +195,16 @@
         mScaleFactor = scaleFactor;
     }
 
+    private float translateX(float x) {
+        return translateRange(mTouchpadHardwareProperties.getLeft(),
+                mTouchpadHardwareProperties.getRight(), 0, getWidth(), x);
+    }
+
+    private float translateY(float y) {
+        return translateRange(mTouchpadHardwareProperties.getTop(),
+                mTouchpadHardwareProperties.getBottom(), 0, getHeight(), y);
+    }
+
     private float translateRange(float rangeBeforeMin, float rangeBeforeMax,
             float rangeAfterMin, float rangeAfterMax, float value) {
         return rangeAfterMin + (value - rangeBeforeMin) / (rangeBeforeMax - rangeBeforeMin) * (
@@ -154,4 +231,10 @@
         }
     }
 
-}
+    private void logFingerTrace() {
+        Slog.d(TAG, "Trace size= " + mHardwareStateHistory.size());
+        for (TouchpadFingerState tfs : mHardwareStateHistory.getLast().getFingerStates()) {
+            Slog.d(TAG, "ID= " + tfs.getTrackingId());
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java
index d495ef5..50bfbc3 100644
--- a/services/core/java/com/android/server/notification/ZenModeConditions.java
+++ b/services/core/java/com/android/server/notification/ZenModeConditions.java
@@ -157,7 +157,7 @@
         }
         // empty rule? disable and bail early
         if (rule.component == null && rule.enabler == null) {
-            if (!android.app.Flags.modesUi() || (android.app.Flags.modesUi() && !isManual)) {
+            if (!isManual) {
                 Log.w(TAG, "No component found for automatic rule: " + rule.conditionId);
                 rule.enabled = false;
             }
diff --git a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
index 4135161..5aea356 100644
--- a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
+++ b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
@@ -18,6 +18,7 @@
 
 import static android.media.AudioAttributes.USAGE_ALARM;
 
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.Notification;
@@ -42,6 +43,8 @@
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.List;
+
 public class BackgroundUserSoundNotifier {
 
     private static final boolean DEBUG = false;
@@ -49,11 +52,21 @@
     private static final String BUSN_CHANNEL_ID = "bg_user_sound_channel";
     private static final String BUSN_CHANNEL_NAME = "BackgroundUserSound";
     public static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER";
-    private static final String EXTRA_NOTIFICATION_ID = "com.android.server.EXTRA_CLIENT_UID";
-    private static final String EXTRA_CURRENT_USER_ID = "com.android.server.EXTRA_CURRENT_USER_ID";
     private static final String ACTION_SWITCH_USER = "com.android.server.ACTION_SWITCH_TO_USER";
-    /** ID of user with notification displayed, -1 if notification is not showing*/
-    private int mUserWithNotification = -1;
+    private static final String ACTION_DISMISS_NOTIFICATION =
+            "com.android.server.ACTION_DISMISS_NOTIFICATION";
+    /**
+     * The clientUid from the AudioFocusInfo of the background user,
+     * for which an active notification is currently displayed.
+     * Set to -1 if no notification is being shown.
+     * TODO: b/367615180 - add support for multiple simultaneous alarms
+     */
+    @VisibleForTesting
+    int mNotificationClientUid = -1;
+    @VisibleForTesting
+    AudioPolicy mFocusControlAudioPolicy;
+    @VisibleForTesting
+    BackgroundUserListener mBgUserListener;
     private final Context mSystemUserContext;
     @VisibleForTesting
     final NotificationManager mNotificationManager;
@@ -67,11 +80,18 @@
         mSystemUserContext = context;
         mNotificationManager =  mSystemUserContext.getSystemService(NotificationManager.class);
         mUserManager = mSystemUserContext.getSystemService(UserManager.class);
+        createNotificationChannel();
+        setupFocusControlAudioPolicy();
+    }
+
+    /**
+     * Creates a dedicated channel for background user related notifications.
+     */
+    private void createNotificationChannel() {
         NotificationChannel channel = new NotificationChannel(BUSN_CHANNEL_ID, BUSN_CHANNEL_NAME,
                 NotificationManager.IMPORTANCE_HIGH);
         channel.setSound(null, null);
         mNotificationManager.createNotificationChannel(channel);
-        setupFocusControlAudioPolicy();
     }
 
     private void setupFocusControlAudioPolicy() {
@@ -81,15 +101,16 @@
         ActivityManager am = mSystemUserContext.getSystemService(ActivityManager.class);
 
         registerReceiver(am);
-        BackgroundUserListener bgUserListener = new BackgroundUserListener(mSystemUserContext);
+        mBgUserListener = new BackgroundUserListener(mSystemUserContext);
         AudioPolicy.Builder focusControlPolicyBuilder = new AudioPolicy.Builder(mSystemUserContext);
         focusControlPolicyBuilder.setLooper(Looper.getMainLooper());
 
-        focusControlPolicyBuilder.setAudioPolicyFocusListener(bgUserListener);
+        focusControlPolicyBuilder.setAudioPolicyFocusListener(mBgUserListener);
 
-        AudioPolicy mFocusControlAudioPolicy = focusControlPolicyBuilder.build();
+        mFocusControlAudioPolicy = focusControlPolicyBuilder.build();
         int status = mSystemUserContext.getSystemService(AudioManager.class)
                 .registerAudioPolicy(mFocusControlAudioPolicy);
+
         if (status != AudioManager.SUCCESS) {
             Log.w(LOG_TAG , "Could not register the service's focus"
                     + " control audio policy, error: " + status);
@@ -117,123 +138,170 @@
 
         @SuppressLint("MissingPermission")
         public void onAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified) {
-            BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary(afi);
+            BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary();
         }
     }
 
+    @VisibleForTesting
+    BackgroundUserListener getAudioPolicyFocusListener() {
+        return  mBgUserListener;
+    }
+
     /**
      * Registers a BroadcastReceiver for actions related to background user sound notifications.
      *  When ACTION_MUTE_SOUND is received, it mutes a background user's alarm sound.
      *  When ACTION_SWITCH_USER is received, a switch to the background user with alarm is started.
      */
-    private void registerReceiver(ActivityManager service) {
+    private void registerReceiver(ActivityManager activityManager) {
         BroadcastReceiver backgroundUserNotificationBroadcastReceiver = new BroadcastReceiver() {
             @SuppressLint("MissingPermission")
             @Override
             public void onReceive(Context context, Intent intent) {
-                if (!(intent.hasExtra(EXTRA_NOTIFICATION_ID)
-                        && intent.hasExtra(EXTRA_CURRENT_USER_ID)
-                        && intent.hasExtra(Intent.EXTRA_USER_ID))) {
+                if (mNotificationClientUid == -1) {
                     return;
                 }
-                final int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
+                dismissNotification();
 
                 if (DEBUG) {
-                    Log.d(LOG_TAG,
-                            "User with alarm id   " + intent.getIntExtra(Intent.EXTRA_USER_ID,
-                                    -1) + "  current user id " + intent.getIntExtra(
-                                    EXTRA_CURRENT_USER_ID, -1));
+                    final int actionIndex = intent.getAction().lastIndexOf(".") + 1;
+                    final String action = intent.getAction().substring(actionIndex);
+                    Log.d(LOG_TAG, "Action requested: " + action + ", by userId "
+                            + ActivityManager.getCurrentUser() + " for alarm on user "
+                            + UserHandle.getUserHandleForUid(mNotificationClientUid));
                 }
-                mUserWithNotification = -1;
-                mNotificationManager.cancelAsUser(LOG_TAG, notificationId,
-                        UserHandle.of(intent.getIntExtra(EXTRA_CURRENT_USER_ID, -1)));
+
                 if (ACTION_MUTE_SOUND.equals(intent.getAction())) {
-                    final AudioManager audioManager =
-                            mSystemUserContext.getSystemService(AudioManager.class);
-                    if (audioManager != null) {
-                        for (AudioPlaybackConfiguration apc :
-                                audioManager.getActivePlaybackConfigurations()) {
-                            if (apc.getAudioAttributes().getUsage() == USAGE_ALARM) {
-                                if (apc.getPlayerProxy() != null) {
-                                    apc.getPlayerProxy().stop();
-                                }
-                            }
-                        }
-                    }
+                    muteAlarmSounds(mSystemUserContext);
                 } else if (ACTION_SWITCH_USER.equals(intent.getAction())) {
-                    service.switchUser(intent.getIntExtra(Intent.EXTRA_USER_ID, -1));
+                    activityManager.switchUser(UserHandle.getUserId(mNotificationClientUid));
                 }
+
+                mNotificationClientUid = -1;
             }
         };
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_MUTE_SOUND);
         filter.addAction(ACTION_SWITCH_USER);
+        filter.addAction(ACTION_DISMISS_NOTIFICATION);
         mSystemUserContext.registerReceiver(backgroundUserNotificationBroadcastReceiver, filter,
                 Context.RECEIVER_NOT_EXPORTED);
     }
 
     /**
+     * Stop player proxy for the ongoing alarm and drop focus for its AudioFocusInfo.
+     */
+    @VisibleForTesting
+    void muteAlarmSounds(Context context) {
+        AudioManager audioManager = context.getSystemService(AudioManager.class);
+        if (audioManager != null) {
+            for (AudioPlaybackConfiguration apc : audioManager.getActivePlaybackConfigurations()) {
+                if (apc.getClientUid() == mNotificationClientUid && apc.getPlayerProxy() != null) {
+                    apc.getPlayerProxy().stop();
+                }
+            }
+        }
+    }
+
+    /**
      * Check if sound is coming from background user and show notification is required.
      */
     @VisibleForTesting
-    void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context
-            foregroundContext) throws RemoteException {
+    void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context foregroundContext)
+            throws RemoteException {
         final int userId = UserHandle.getUserId(afi.getClientUid());
         final int usage = afi.getAttributes().getUsage();
         UserInfo userInfo = mUserManager.getUserInfo(userId);
-        if (userInfo != null && userId != foregroundContext.getUserId()) {
+        // Only show notification if the sound is coming from background user and the notification
+        // is not already shown.
+        if (userInfo != null && userId != foregroundContext.getUserId()
+                && mNotificationClientUid == -1) {
             //TODO: b/349138482 - Add handling of cases when usage == USAGE_NOTIFICATION_RINGTONE
             if (usage == USAGE_ALARM) {
-                Intent muteIntent = createIntent(ACTION_MUTE_SOUND, afi, foregroundContext, userId);
-                PendingIntent mutePI = PendingIntent.getBroadcast(mSystemUserContext, 0,
-                        muteIntent, PendingIntent.FLAG_UPDATE_CURRENT
-                                | PendingIntent.FLAG_IMMUTABLE);
-                Intent switchIntent = createIntent(ACTION_SWITCH_USER, afi, foregroundContext,
-                        userId);
-                PendingIntent switchPI = PendingIntent.getBroadcast(mSystemUserContext, 0,
-                        switchIntent, PendingIntent.FLAG_UPDATE_CURRENT
-                                | PendingIntent.FLAG_IMMUTABLE);
+                if (DEBUG) {
+                    Log.d(LOG_TAG, "Alarm ringing on background user " + userId
+                            + ", displaying notification for current user "
+                            + foregroundContext.getUserId());
+                }
 
-                mUserWithNotification = foregroundContext.getUserId();
-                mNotificationManager.notifyAsUser(LOG_TAG, afi.getClientUid(),
-                        createNotification(userInfo.name, mutePI, switchPI, foregroundContext),
+                mNotificationClientUid = afi.getClientUid();
+
+                mNotificationManager.notifyAsUser(LOG_TAG, mNotificationClientUid,
+                        createNotification(userInfo.name, foregroundContext),
                         foregroundContext.getUser());
             }
         }
     }
 
     /**
-     * If notification is present, dismisses it. To be called when the relevant sound loses focus.
+     * Dismisses notification if the associated focus has been removed from the focus stack.
+     * Notification remains if the focus is temporarily lost due to another client taking over the
+     * focus ownership.
      */
-    private void dismissNotificationIfNecessary(AudioFocusInfo afi) {
-        if (mUserWithNotification >= 0) {
-            mNotificationManager.cancelAsUser(LOG_TAG, afi.getClientUid(),
-                    UserHandle.of(mUserWithNotification));
+    @VisibleForTesting
+    void dismissNotificationIfNecessary() {
+        if (getAudioFocusInfoForNotification() == null && mNotificationClientUid >= 0) {
+            if (DEBUG) {
+                Log.d(LOG_TAG, "Alarm ringing on background user "
+                        + UserHandle.getUserHandleForUid(mNotificationClientUid).getIdentifier()
+                        + " left focus stack, dismissing notification");
+            }
+            dismissNotification();
+            mNotificationClientUid = -1;
         }
-        mUserWithNotification = -1;
     }
 
-    private Intent createIntent(String intentAction, AudioFocusInfo afi, Context fgUserContext,
-            int userId) {
+    /**
+     * Dismisses notification for all users in case user switch occurred after notification was
+     * shown.
+     */
+    @SuppressLint("MissingPermission")
+    private void dismissNotification() {
+        mNotificationManager.cancelAsUser(LOG_TAG, mNotificationClientUid, UserHandle.ALL);
+    }
+
+    /**
+     * Returns AudioFocusInfo associated with the current notification.
+     */
+    @SuppressLint("MissingPermission")
+    @VisibleForTesting
+    @Nullable
+    AudioFocusInfo getAudioFocusInfoForNotification() {
+        if (mNotificationClientUid >= 0) {
+            List<AudioFocusInfo> stack = mFocusControlAudioPolicy.getFocusStack();
+            for (int i = stack.size() - 1; i >= 0; i--) {
+                if (stack.get(i).getClientUid() == mNotificationClientUid) {
+                    return stack.get(i);
+                }
+            }
+        }
+        return null;
+    }
+
+    private PendingIntent createPendingIntent(String intentAction) {
         final Intent intent = new Intent(intentAction);
-        intent.putExtra(EXTRA_CURRENT_USER_ID, fgUserContext.getUserId());
-        intent.putExtra(EXTRA_NOTIFICATION_ID, afi.getClientUid());
-        intent.putExtra(Intent.EXTRA_USER_ID, userId);
-        return intent;
+        PendingIntent resultPI =  PendingIntent.getBroadcast(mSystemUserContext, 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        return resultPI;
     }
 
-    private Notification createNotification(String userName, PendingIntent muteIntent,
-            PendingIntent switchIntent, Context fgContext) {
+    @VisibleForTesting
+    Notification createNotification(String userName, Context fgContext) {
         final String title = fgContext.getString(R.string.bg_user_sound_notification_title_alarm,
                 userName);
         final int icon = R.drawable.ic_audio_alarm;
+
+        PendingIntent mutePI = createPendingIntent(ACTION_MUTE_SOUND);
+        PendingIntent switchPI = createPendingIntent(ACTION_SWITCH_USER);
+        PendingIntent dismissNotificationPI = createPendingIntent(ACTION_DISMISS_NOTIFICATION);
+
         final Notification.Action mute = new Notification.Action.Builder(null,
                 fgContext.getString(R.string.bg_user_sound_notification_button_mute),
-                muteIntent).build();
+                mutePI).build();
         final Notification.Action switchUser = new Notification.Action.Builder(null,
                 fgContext.getString(R.string.bg_user_sound_notification_button_switch_user),
-                switchIntent).build();
+                switchPI).build();
+
         Notification.Builder notificationBuilder = new Notification.Builder(mSystemUserContext,
                 BUSN_CHANNEL_ID)
                 .setSmallIcon(icon)
@@ -243,16 +311,18 @@
                 .setOngoing(true)
                 .setColor(fgContext.getColor(R.color.system_notification_accent_color))
                 .setContentTitle(title)
-                .setContentIntent(muteIntent)
+                .setContentIntent(mutePI)
                 .setAutoCancel(true)
+                .setDeleteIntent(dismissNotificationPI)
                 .setVisibility(Notification.VISIBILITY_PUBLIC);
+
         if (mUserManager.isUserSwitcherEnabled() && (mUserManager.getUserSwitchability(
-                UserHandle.of(fgContext.getUserId())) == UserManager.SWITCHABILITY_STATUS_OK)) {
+                fgContext.getUser()) == UserManager.SWITCHABILITY_STATUS_OK)) {
             notificationBuilder.setActions(mute, switchUser);
         } else {
             notificationBuilder.setActions(mute);
         }
+
         return notificationBuilder.build();
     }
 }
-
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index ee15bec..efd58ed 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -92,7 +92,6 @@
 import android.multiuser.Flags;
 import android.net.Uri;
 import android.os.Binder;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IInterface;
@@ -215,7 +214,7 @@
 
     @VisibleForTesting
     static class LauncherAppsImpl extends ILauncherApps.Stub {
-        private static final boolean DEBUG = Build.IS_DEBUGGABLE;
+        private static final boolean DEBUG = false;
         private static final String TAG = "LauncherAppsService";
         private static final String NAMESPACE_MULTIUSER = "multiuser";
         private static final String FLAG_NON_SYSTEM_ACCESS_TO_HIDDEN_PROFILES =
@@ -496,28 +495,8 @@
 
         private boolean canAccessProfile(int callingUid, int callingUserId, int callingPid,
                 int targetUserId, String message) {
-            if (DEBUG) {
-                final AndroidPackage callingPackage =
-                        mPackageManagerInternal.getPackage(callingUid);
-                final String callingPackageName = callingPackage == null
-                        ? null : callingPackage.getPackageName();
-                Slog.v(TAG, "canAccessProfile called by " + callingPackageName
-                        + " for user " + callingUserId
-                        + " requesting to access user "
-                        + targetUserId + " when invoking " + message);
-            }
-            if (targetUserId == callingUserId) {
-                if (DEBUG) {
-                    Slog.v(TAG, message + " passed canAccessProfile for targetuser"
-                        + targetUserId + " because it is the same as the calling user");
-                }
-                return true;
-            }
+            if (targetUserId == callingUserId) return true;
             if (injectHasInteractAcrossUsersFullPermission(callingPid, callingUid)) {
-              if (DEBUG) {
-                    Slog.v(TAG, message + " passed because calling process"
-                        + "has permission to interact across users");
-                }
                 return true;
             }
 
@@ -535,25 +514,11 @@
 
             if (isHiddenProfile(UserHandle.of(targetUserId))
                     && !canAccessHiddenProfile(callingUid, callingPid)) {
-                Slog.w(TAG, message + " for hidden profile user " + targetUserId
-                        + " from " + callingUserId + " not allowed");
-
                 return false;
             }
 
-            final boolean ret = mUserManagerInternal.isProfileAccessible(
-                    callingUserId, targetUserId, message, true);
-            if (DEBUG) {
-                final AndroidPackage callingPackage =
-                        mPackageManagerInternal.getPackage(callingUid);
-                final String callingPackageName = callingPackage == null
-                        ? null : callingPackage.getPackageName();
-                Slog.v(TAG, "canAccessProfile returned " + ret + " for " + callingPackageName
-                        + " for user " + callingUserId
-                        + " requesting to access user "
-                        + targetUserId + " when invoking " + message);
-            }
-            return ret;
+            return mUserManagerInternal.isProfileAccessible(callingUserId, targetUserId,
+                    message, true);
         }
 
         private boolean isHiddenProfile(UserHandle targetUser) {
@@ -1376,10 +1341,6 @@
         @Override
         public void pinShortcuts(String callingPackage, String packageName, List<String> ids,
                 UserHandle targetUser) {
-            if (DEBUG) {
-                Slog.v(TAG, "pinShortcuts: " + callingPackage + " is pinning shortcuts from "
-                        + packageName + " for user " + targetUser);
-            }
             if (!mShortcutServiceInternal
                     .areShortcutsSupportedOnHomeScreen(targetUser.getIdentifier())) {
                 // Requires strict ACCESS_SHORTCUTS permission for user-profiles with items
@@ -1390,11 +1351,6 @@
             }
             ensureShortcutPermission(callingPackage);
             if (!canAccessProfile(targetUser.getIdentifier(), "Cannot pin shortcuts")) {
-                if (DEBUG) {
-                    Slog.v(TAG, "pinShortcuts: " + callingPackage
-                            + " is pinning shortcuts from " + packageName
-                            + " for user " + targetUser + " but cannot access profile");
-                }
                 return;
             }
 
@@ -2451,7 +2407,7 @@
                 final int callbackUserId = callbackUser.getIdentifier();
                 final int shortcutUserId = shortcutUser.getIdentifier();
 
-                if (shortcutUser == callbackUser) return true;
+                if ((shortcutUser.equals(callbackUser))) return true;
                 return mUserManagerInternal.isProfileAccessible(callbackUserId, shortcutUserId,
                         null, false);
             }
@@ -2485,16 +2441,28 @@
                                 final BroadcastCookie cookie =
                                         (BroadcastCookie) mListeners.getBroadcastCookie(i);
                                 if (!isEnabledProfileOf(cookie, user, "onPackageRemoved")) {
+                                    // b/350144057
+                                    Slog.d(TAG, "onPackageRemoved: Skipping - profile not enabled"
+                                            + " or not accessible for user=" + user
+                                            + ", packageName=" + packageName);
                                     continue;
                                 }
                                 if (!isCallingAppIdAllowed(appIdAllowList, UserHandle.getAppId(
                                         cookie.callingUid))) {
+                                    // b/350144057
+                                    Slog.d(TAG, "onPackageRemoved: Skipping - appId not allowed"
+                                            + " for user=" + user
+                                            + ", packageName=" + packageName);
                                     continue;
                                 }
                                 try {
+                                    // b/350144057
+                                    Slog.d(TAG, "onPackageRemoved: triggering onPackageRemoved"
+                                            + " for user=" + user
+                                            + ", packageName=" + packageName);
                                     listener.onPackageRemoved(user, packageName);
                                 } catch (RemoteException re) {
-                                    Slog.d(TAG, "Callback failed ", re);
+                                    Slog.d(TAG, "onPackageRemoved: Callback failed ", re);
                                 }
                             }
                         } finally {
@@ -2524,15 +2492,27 @@
                         IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                         BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
                         if (!isEnabledProfileOf(cookie, user, "onPackageAdded")) {
+                            // b/350144057
+                            Slog.d(TAG, "onPackageAdded: Skipping - profile not enabled"
+                                    + " or not accessible for user=" + user
+                                    + ", packageName=" + packageName);
                             continue;
                         }
                         if (!isPackageVisibleToListener(packageName, cookie, user)) {
+                            // b/350144057
+                            Slog.d(TAG, "onPackageAdded: Skipping - package filtered"
+                                    + " for user=" + user
+                                    + ", packageName=" + packageName);
                             continue;
                         }
                         try {
+                            // b/350144057
+                            Slog.d(TAG, "onPackageAdded: triggering onPackageAdded"
+                                    + " for user=" + user
+                                    + ", packageName=" + packageName);
                             listener.onPackageAdded(user, packageName);
                         } catch (RemoteException re) {
-                            Slog.d(TAG, "Callback failed ", re);
+                            Slog.d(TAG, "onPackageAdded: Callback failed ", re);
                         }
                     }
                 } finally {
@@ -2566,7 +2546,7 @@
                         try {
                             listener.onPackageChanged(user, packageName);
                         } catch (RemoteException re) {
-                            Slog.d(TAG, "Callback failed ", re);
+                            Slog.d(TAG, "onPackageChanged: Callback failed ", re);
                         }
                     }
                 } finally {
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index d65e30b..045d4db 100644
--- a/services/core/java/com/android/server/pm/ShortcutLauncher.java
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -42,7 +42,6 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * Launcher information used by {@link ShortcutService}.
@@ -129,15 +128,9 @@
      */
     public void pinShortcuts(@UserIdInt int packageUserId,
             @NonNull String packageName, @NonNull List<String> ids, boolean forPinRequest) {
-        if (ShortcutService.DEBUG) {
-            Slog.v(TAG, "ShortcutLauncher#pinShortcuts: pin shortcuts from " + packageName
-                    + " with userId=" + packageUserId + " shortcutIds="
-                    + ids.stream().collect(Collectors.joining(", ", "[", "]")));
-        }
         final ShortcutPackage packageShortcuts =
                 mShortcutUser.getPackageShortcutsIfExists(packageName);
         if (packageShortcuts == null) {
-            Slog.w(TAG, "ShortcutLauncher#pinShortcuts packageShortcuts is null");
             return; // No need to instantiate.
         }
 
@@ -162,10 +155,6 @@
                 final String id = ids.get(i);
                 final ShortcutInfo si = packageShortcuts.findShortcutById(id);
                 if (si == null) {
-                    if (ShortcutService.DEBUG) {
-                        Slog.w(TAG, "ShortcutLauncher#pinShortcuts: cannot pin "
-                                + id + " because it does not exist");
-                    }
                     continue;
                 }
                 if (si.isDynamic() || si.isLongLived()
@@ -185,13 +174,6 @@
                         }
                     }
                 }
-                if (ShortcutService.DEBUG) {
-                    Slog.v(TAG, "ShortcutLauncher#pinShortcuts: "
-                            + " newSet: " + newSet.stream().collect(
-                                    Collectors.joining(", ", "[", "]"))
-                            + " floatingSet: " + floatingSet.stream().collect(
-                                    Collectors.joining(", ", "[", "]")));
-                }
                 mPinnedShortcuts.put(up, newSet);
             }
         }
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index c9ad498..60056eb 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -729,11 +729,6 @@
             }
             pinnedShortcuts.addAll(pinned);
         });
-        if (ShortcutService.DEBUG) {
-            Slog.v(TAG, "ShortcutPackage#refreshPinnedFlags: "
-                    + " pinnedShortcuts: " + pinnedShortcuts.stream().collect(
-                            Collectors.joining(", ", "[", "]")));
-        }
         // Secondly, update the pinned state if necessary.
         final List<ShortcutInfo> pinned = findAll(pinnedShortcuts);
         if (pinned != null) {
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index ea495c9..a3ff195 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -169,7 +169,7 @@
 public class ShortcutService extends IShortcutService.Stub {
     static final String TAG = "ShortcutService";
 
-    static final boolean DEBUG = Build.IS_DEBUGGABLE; // STOPSHIP if true
+    static final boolean DEBUG = false; // STOPSHIP if true
     static final boolean DEBUG_LOAD = false; // STOPSHIP if true
     static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true
     static final boolean DEBUG_REBOOT = Build.IS_DEBUGGABLE;
@@ -3206,11 +3206,6 @@
         public void pinShortcuts(int launcherUserId,
                 @NonNull String callingPackage, @NonNull String packageName,
                 @NonNull List<String> shortcutIds, int userId) {
-            if (DEBUG) {
-                Slog.v(TAG, "pinShortcuts: " + callingPackage + ", with userId=" + launcherUserId
-                        + ", is trying to pin shortcuts from " + packageName
-                        + " with userId=" + userId);
-            }
             // Calling permission must be checked by LauncherAppsImpl.
             Preconditions.checkStringNotEmpty(packageName, "packageName");
             Objects.requireNonNull(shortcutIds, "shortcutIds");
@@ -3235,11 +3230,6 @@
                                     && !si.isDeclaredInManifest(),
                             ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO,
                             callingPackage, launcherUserId, false);
-                } else {
-                    if (DEBUG) {
-                        Slog.w(TAG, "specified package " + packageName + ", with userId=" + userId
-                        + ", doesn't exist.");
-                    }
                 }
                 // Get list of shortcuts that will get unpinned.
                 ArraySet<String> oldPinnedIds = launcher.getPinnedShortcutIds(packageName, userId);
@@ -5458,17 +5448,6 @@
      */
     private List<ShortcutInfo> prepareChangedShortcuts(ArraySet<String> changedIds,
             ArraySet<String> newIds, List<ShortcutInfo> deletedList, final ShortcutPackage ps) {
-        if (DEBUG) {
-            Slog.v(TAG, "prepareChangedShortcuts: "
-                + " changedIds=" + (changedIds == null
-                        ? "n/a" : changedIds.stream().collect(Collectors.joining(", ", "[", "]")))
-                + " newIds=" + (newIds == null
-                        ? "n/a" : newIds.stream().collect(Collectors.joining(", ", "[", "]")))
-                + " deletedList=" + (deletedList == null
-                        ? "n/a" : deletedList.stream().map(ShortcutInfo::getId).collect(
-                                Collectors.joining(", ", "[", "]")))
-                + " ps=" + (ps == null ? "n/a" : ps.getPackageName()));
-        }
         if (ps == null) {
             // This can happen when package restore is not finished yet.
             return null;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index a683a8c..89417f3 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1090,6 +1090,21 @@
         mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null;
         mPrivateSpaceAutoLockSettingsObserver = new SettingsObserver(mHandler);
         emulateSystemUserModeIfNeeded();
+        initPropertyInvalidatedCaches();
+    }
+
+    /**
+     * This method is used to invalidate the caches at server statup,
+     * so that caches can start working.
+     */
+    private static final void initPropertyInvalidatedCaches() {
+        if (android.multiuser.Flags.cachesNotInvalidatedAtStartReadOnly()) {
+            UserManager.invalidateIsUserUnlockedCache();
+            UserManager.invalidateQuietModeEnabledCache();
+            UserManager.invalidateStaticUserProperties();
+            UserManager.invalidateUserPropertiesCache();
+            UserManager.invalidateUserSerialNumberCache();
+        }
     }
 
     private boolean doesDeviceHardwareSupportPrivateSpace() {
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index 953aae9..457196b 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -89,6 +89,7 @@
 import com.android.internal.widget.LockSettingsStateListener;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.servicewatcher.CurrentUserServiceSupplier;
 import com.android.server.servicewatcher.ServiceWatcher;
 import com.android.server.utils.Slogf;
@@ -170,6 +171,7 @@
     private final ActivityManager mActivityManager;
     private FingerprintManager mFingerprintManager;
     private FaceManager mFaceManager;
+    private UserManagerInternal mUserManagerInternal;
 
     private enum TrustState {
         // UNTRUSTED means that TrustManagerService is currently *not* giving permission for the
@@ -1064,6 +1066,8 @@
                     Log.w(TAG, "Unable to check keyguard lock state", e);
                 }
                 currentUserIsUnlocked = unlockedUser == id;
+            } else if (isVisibleBackgroundUser(id)) {
+                showingKeyguard = !mUserManager.isUserUnlocked(id);
             }
             final boolean deviceLocked = secure && showingKeyguard && !trusted
                     && !biometricAuthenticated;
@@ -1095,6 +1099,16 @@
         }
     }
 
+    private boolean isVisibleBackgroundUser(int userId) {
+        if (!mUserManager.isVisibleBackgroundUsersSupported()) {
+            return false;
+        }
+        if (mUserManagerInternal == null) {
+            mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+        }
+        return mUserManagerInternal.isVisibleBackgroundFullUser(userId);
+    }
+
     private void notifyTrustAgentsOfDeviceLockState(int userId, boolean isLocked) {
         for (int i = 0; i < mActiveAgents.size(); i++) {
             AgentInfo agent = mActiveAgents.valueAt(i);
diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
index 3be266e..f069dcd 100644
--- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
@@ -145,11 +145,13 @@
         }
     }
 
-    void updateSizeCompatScale(@NonNull Rect resolvedAppBounds, @NonNull Rect containerAppBounds) {
+    void updateSizeCompatScale(@NonNull Rect resolvedAppBounds, @NonNull Rect containerAppBounds,
+            @NonNull Configuration newParentConfig) {
         mSizeCompatScale = mActivityRecord.mAppCompatController.getTransparentPolicy()
                 .findOpaqueNotFinishingActivityBelow()
                 .map(activityRecord -> mSizeCompatScale)
-                .orElseGet(() -> calculateSizeCompatScale(resolvedAppBounds, containerAppBounds));
+                .orElseGet(() -> calculateSizeCompatScale(
+                        resolvedAppBounds, containerAppBounds, newParentConfig));
     }
 
     void clearSizeCompatModeAttributes() {
@@ -290,7 +292,7 @@
         // Calculates the scale the size compatibility bounds into the region which is available
         // to application.
         final float lastSizeCompatScale = mSizeCompatScale;
-        updateSizeCompatScale(resolvedAppBounds, containerAppBounds);
+        updateSizeCompatScale(resolvedAppBounds, containerAppBounds, newParentConfiguration);
 
         final int containerTopInset = containerAppBounds.top - containerBounds.top;
         final boolean topNotAligned =
@@ -423,7 +425,7 @@
     }
 
     private float calculateSizeCompatScale(@NonNull Rect resolvedAppBounds,
-            @NonNull Rect containerAppBounds) {
+            @NonNull Rect containerAppBounds, @NonNull Configuration newParentConfig) {
         final int contentW = resolvedAppBounds.width();
         final int contentH = resolvedAppBounds.height();
         final int viewportW = containerAppBounds.width();
@@ -432,7 +434,8 @@
         // original container or if it's a freeform window in desktop mode.
         boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH)
                 || (canEnterDesktopMode(mActivityRecord.mAtmService.mContext)
-                    && mActivityRecord.getWindowingMode() == WINDOWING_MODE_FREEFORM);
+                && newParentConfig.windowConfiguration.getWindowingMode()
+                    == WINDOWING_MODE_FREEFORM);
         return shouldAllowUpscaling ? Math.min(
                 (float) viewportW / contentW, (float) viewportH / contentH) : 1f;
     }
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 1994174..3a2cffb 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -422,6 +422,7 @@
                 || first.brightnessMaximum != second.brightnessMaximum
                 || first.brightnessDefault != second.brightnessDefault
                 || first.installOrientation != second.installOrientation
+                || first.isForceSdr != second.isForceSdr
                 || !Objects.equals(first.layoutLimitedRefreshRate, second.layoutLimitedRefreshRate)
                 || !BrightnessSynchronizer.floatEquals(first.hdrSdrRatio, second.hdrSdrRatio)
                 || !first.thermalRefreshRateThrottling.contentEquals(
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index 5514294e..e007b1d 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -181,22 +181,30 @@
         return true;
     }
 
-    boolean transferToHost(@NonNull InputTransferToken embeddedWindowToken,
+    boolean transferToHost(int callingUid, @NonNull InputTransferToken embeddedWindowToken,
             @NonNull WindowState transferToHostWindowState) {
         EmbeddedWindow ew = getByInputTransferToken(embeddedWindowToken);
         if (!isValidTouchGestureParams(transferToHostWindowState, ew)) {
             return false;
         }
+        if (callingUid != ew.mOwnerUid) {
+            throw new SecurityException(
+                    "Transfer request must originate from owner of transferFromToken");
+        }
         return mInputManagerService.transferTouchGesture(ew.getInputChannelToken(),
                 transferToHostWindowState.mInputChannelToken);
     }
 
-    boolean transferToEmbedded(WindowState hostWindowState,
+    boolean transferToEmbedded(int callingUid, WindowState hostWindowState,
             @NonNull InputTransferToken transferToToken) {
         final EmbeddedWindowController.EmbeddedWindow ew = getByInputTransferToken(transferToToken);
         if (!isValidTouchGestureParams(hostWindowState, ew)) {
             return false;
         }
+        if (callingUid != hostWindowState.mOwnerUid) {
+            throw new SecurityException(
+                    "Transfer request must originate from owner of transferFromToken");
+        }
         return mInputManagerService.transferTouchGesture(hostWindowState.mInputChannelToken,
                 ew.getInputChannelToken());
     }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 33f2dd1..b8f47cc 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9212,6 +9212,8 @@
         final InputApplicationHandle applicationHandle;
         final String name;
         Objects.requireNonNull(outInputChannel);
+        Objects.requireNonNull(inputTransferToken);
+
         synchronized (mGlobalLock) {
             WindowState hostWindowState = hostInputTransferToken != null
                     ? mInputToWindowMap.get(hostInputTransferToken.getToken()) : null;
@@ -9236,6 +9238,7 @@
         Objects.requireNonNull(transferFromToken);
         Objects.requireNonNull(transferToToken);
 
+        final int callingUid = Binder.getCallingUid();
         final long identity = Binder.clearCallingIdentity();
         boolean didTransfer;
         try {
@@ -9245,12 +9248,14 @@
                 // represents an embedded window so transfer from host to embedded.
                 WindowState windowStateTo = mInputToWindowMap.get(transferToToken.getToken());
                 if (windowStateTo != null) {
-                    didTransfer = mEmbeddedWindowController.transferToHost(transferFromToken,
+                    didTransfer = mEmbeddedWindowController.transferToHost(callingUid,
+                            transferFromToken,
                             windowStateTo);
                 } else {
                     WindowState windowStateFrom = mInputToWindowMap.get(
                             transferFromToken.getToken());
-                    didTransfer = mEmbeddedWindowController.transferToEmbedded(windowStateFrom,
+                    didTransfer = mEmbeddedWindowController.transferToEmbedded(callingUid,
+                            windowStateFrom,
                             transferToToken);
                 }
             }
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
index 63cf7bf..c05c381 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -139,16 +139,15 @@
         runtimeSearchSession.put(putDocumentsRequest).get()
         staticSearchSession.put(putDocumentsRequest).get()
         val metadataSyncAdapter =
-            MetadataSyncAdapter(
-                testExecutor,
-                runtimeSearchSession,
+            MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
+
+        val submitSyncRequest =
+            metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
                 staticSearchSession,
-                packageManager,
+                runtimeSearchSession,
             )
 
-        val submitSyncRequest = metadataSyncAdapter.submitSyncRequest()
-
-        assertThat(submitSyncRequest.get()).isTrue()
+        assertThat(submitSyncRequest).isInstanceOf(Unit::class.java)
     }
 
     @Test
@@ -182,16 +181,15 @@
             PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
         staticSearchSession.put(putDocumentsRequest).get()
         val metadataSyncAdapter =
-            MetadataSyncAdapter(
-                testExecutor,
-                runtimeSearchSession,
+            MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
+
+        val submitSyncRequest =
+            metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
                 staticSearchSession,
-                packageManager,
+                runtimeSearchSession,
             )
 
-        val submitSyncRequest = metadataSyncAdapter.submitSyncRequest()
-
-        assertThat(submitSyncRequest.get()).isTrue()
+        assertThat(submitSyncRequest).isInstanceOf(Unit::class.java)
     }
 
     @Test
@@ -239,16 +237,15 @@
             PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
         runtimeSearchSession.put(putDocumentsRequest).get()
         val metadataSyncAdapter =
-            MetadataSyncAdapter(
-                testExecutor,
-                runtimeSearchSession,
+            MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
+
+        val submitSyncRequest =
+            metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
                 staticSearchSession,
-                packageManager,
+                runtimeSearchSession,
             )
 
-        val submitSyncRequest = metadataSyncAdapter.submitSyncRequest()
-
-        assertThat(submitSyncRequest.get()).isTrue()
+        assertThat(submitSyncRequest).isInstanceOf(Unit::class.java)
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 8b80f85..255dcb0 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -27,6 +27,7 @@
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
+import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.server.display.ExternalDisplayPolicy.ENABLE_ON_CONNECT;
@@ -195,8 +196,8 @@
     private static final String VIRTUAL_DISPLAY_NAME = "Test Virtual Display";
     private static final String PACKAGE_NAME = "com.android.frameworks.displayservicetests";
     private static final long STANDARD_DISPLAY_EVENTS = DisplayManager.EVENT_FLAG_DISPLAY_ADDED
-                    | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
-                    | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
+            | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
+            | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
     private static final long STANDARD_AND_CONNECTION_DISPLAY_EVENTS =
             STANDARD_DISPLAY_EVENTS | DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED;
 
@@ -238,6 +239,8 @@
 
     private UserManager mUserManager;
 
+    private int[] mAllowedHdrOutputTypes;
+
     private final DisplayManagerService.Injector mShortMockedInjector =
             new DisplayManagerService.Injector() {
                 @Override
@@ -256,11 +259,12 @@
                             displayAdapterListener, flags,
                             mMockedDisplayNotificationManager,
                             new LocalDisplayAdapter.Injector() {
-                        @Override
-                        public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
-                            return mSurfaceControlProxy;
-                        }
-                    });
+                                @Override
+                                public LocalDisplayAdapter.SurfaceControlProxy
+                                        getSurfaceControlProxy() {
+                                    return mSurfaceControlProxy;
+                                }
+                            });
                 }
 
                 @Override
@@ -320,7 +324,7 @@
 
         @Override
         int setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
-                int[] autoHdrTypes) {
+                int[] allowedHdrOutputTypes) {
             mHdrConversionMode = conversionMode;
             mPreferredHdrOutputType = preferredHdrOutputType;
             return Display.HdrCapabilities.HDR_TYPE_INVALID;
@@ -1295,11 +1299,11 @@
                         .setUniqueId("uniqueId --- mirror display");
         assertThrows(SecurityException.class, () -> {
             localService.createVirtualDisplay(
-                            builder.build(),
-                            mMockAppToken /* callback */,
-                            null /* virtualDeviceToken */,
-                            mock(DisplayWindowPolicyController.class),
-                            PACKAGE_NAME);
+                    builder.build(),
+                    mMockAppToken /* callback */,
+                    null /* virtualDeviceToken */,
+                    mock(DisplayWindowPolicyController.class),
+                    PACKAGE_NAME);
         });
     }
 
@@ -1433,7 +1437,7 @@
 
         // The virtual display should not have FLAG_ALWAYS_UNLOCKED set.
         assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags
-                        & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED));
+                & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED));
     }
 
     /**
@@ -1466,7 +1470,7 @@
 
         // The virtual display should not have FLAG_PRESENTATION set.
         assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags
-                        & DisplayDeviceInfo.FLAG_PRESENTATION));
+                & DisplayDeviceInfo.FLAG_PRESENTATION));
     }
 
     @Test
@@ -2358,6 +2362,7 @@
                 HdrConversionMode.HDR_CONVERSION_FORCE,
                 Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION);
         displayManager.setHdrConversionModeInternal(mode);
+
         assertEquals(mode, displayManager.getHdrConversionModeSettingInternal());
         assertEquals(mode.getConversionMode(), mHdrConversionMode);
         assertEquals(mode.getPreferredHdrOutputType(), mPreferredHdrOutputType);
@@ -2402,6 +2407,86 @@
     }
 
     @Test
+    public void testSetAreUserDisabledHdrTypesAllowed_withFalse_whenHdrDisabled_stripsHdrType() {
+        DisplayManagerService displayManager = new DisplayManagerService(
+                mContext, new BasicInjector() {
+                    @Override
+                    int setHdrConversionMode(int conversionMode, int preferredHdrOutputType,
+                            int[] allowedTypes) {
+                        mAllowedHdrOutputTypes = allowedTypes;
+                        return Display.HdrCapabilities.HDR_TYPE_INVALID;
+                    }
+
+                    // Overriding this method to capture the allowed HDR type
+                    @Override
+                    int[] getSupportedHdrOutputTypes() {
+                        return new int[]{Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION};
+                    }
+                });
+
+        // Setup: no HDR types disabled, userDisabledTypes allowed, system conversion
+        displayManager.setUserDisabledHdrTypesInternal(new int [0]);
+        displayManager.setAreUserDisabledHdrTypesAllowedInternal(true);
+        displayManager.setHdrConversionModeInternal(
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+
+        assertEquals(1, mAllowedHdrOutputTypes.length);
+        assertTrue(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION == mAllowedHdrOutputTypes[0]);
+
+        // Action: disable Dolby Vision, set userDisabledTypes not allowed
+        displayManager.setUserDisabledHdrTypesInternal(
+                new int [] {Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION});
+        displayManager.setAreUserDisabledHdrTypesAllowedInternal(false);
+
+        assertEquals(0, mAllowedHdrOutputTypes.length);
+    }
+
+    @Test
+    public void testGetEnabledHdrTypesLocked_whenTypesDisabled_stripsDisabledTypes() {
+        DisplayManagerService displayManager = new DisplayManagerService(
+                mContext, new BasicInjector() {
+                    @Override
+                    int[] getSupportedHdrOutputTypes() {
+                        return new int[]{Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION};
+                    }
+                });
+
+        displayManager.setUserDisabledHdrTypesInternal(new int [0]);
+        displayManager.setAreUserDisabledHdrTypesAllowedInternal(true);
+        int [] enabledHdrOutputTypes = displayManager.getEnabledHdrOutputTypes();
+        assertEquals(1, enabledHdrOutputTypes.length);
+        assertTrue(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION == enabledHdrOutputTypes[0]);
+
+        displayManager.setAreUserDisabledHdrTypesAllowedInternal(false);
+        enabledHdrOutputTypes = displayManager.getEnabledHdrOutputTypes();
+        assertEquals(1, enabledHdrOutputTypes.length);
+        assertTrue(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION == enabledHdrOutputTypes[0]);
+
+        displayManager.setUserDisabledHdrTypesInternal(
+                new int [] {Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION});
+        enabledHdrOutputTypes = displayManager.getEnabledHdrOutputTypes();
+        assertEquals(0, enabledHdrOutputTypes.length);
+    }
+
+    @Test
+    public void testSetHdrConversionModeInternal_isForceSdrIsUpdated() {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
+        LogicalDisplay logicalDisplay =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+
+        displayManager.setHdrConversionModeInternal(
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, HDR_TYPE_INVALID));
+        assertTrue(logicalDisplay.getDisplayInfoLocked().isForceSdr);
+
+        displayManager.setHdrConversionModeInternal(
+                new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM));
+        assertFalse(logicalDisplay.getDisplayInfoLocked().isForceSdr);
+    }
+
+    @Test
     public void testReturnsRefreshRateForDisplayAndSensor_proximitySensorSet() {
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerInternal localService = displayManager.new LocalService();
@@ -3505,7 +3590,7 @@
     }
 
     private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
-                                                      Display.Mode[] modes) {
+            Display.Mode[] modes) {
         FakeDisplayDevice displayDevice = new FakeDisplayDevice();
         DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
         displayDeviceInfo.supportedModes = modes;
@@ -3761,9 +3846,9 @@
         public void setUserPreferredDisplayModeLocked(Display.Mode preferredMode) {
             for (Display.Mode mode : mDisplayDeviceInfo.supportedModes) {
                 if (mode.matchesIfValid(
-                          preferredMode.getPhysicalWidth(),
-                          preferredMode.getPhysicalHeight(),
-                          preferredMode.getRefreshRate())) {
+                        preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight(),
+                        preferredMode.getRefreshRate())) {
                     mPreferredMode = mode;
                     break;
                 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java
index a82658b..3062d51 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java
@@ -16,13 +16,19 @@
 
 package com.android.server.pm;
 
+import static android.media.AudioAttributes.USAGE_ALARM;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.AssertJUnit.assertEquals;
 
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -31,6 +37,9 @@
 import android.media.AudioAttributes;
 import android.media.AudioFocusInfo;
 import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
+import android.media.PlayerProxy;
+import android.media.audiopolicy.AudioPolicy;
 import android.os.Build;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -45,6 +54,10 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Stack;
+
 @RunWith(JUnit4.class)
 
 public class BackgroundUserSoundNotifierTest {
@@ -63,7 +76,10 @@
         MockitoAnnotations.initMocks(this);
         mSpiedContext = spy(mRealContext);
         mUsersToRemove = new ArraySet<>();
-        mUserManager = UserManager.get(mRealContext);
+
+        mUserManager = spy(mSpiedContext.getSystemService(UserManager.class));
+        doReturn(mUserManager)
+                .when(mSpiedContext).getSystemService(UserManager.class);
         doReturn(mNotificationManager)
                 .when(mSpiedContext).getSystemService(NotificationManager.class);
         mBackgroundUserSoundNotifier = new BackgroundUserSoundNotifier(mSpiedContext);
@@ -74,12 +90,9 @@
         mUsersToRemove.stream().toList().forEach(this::removeUser);
     }
     @Test
-    public void testAlarmOnBackgroundUser_ForegroundUserNotified() throws RemoteException {
-        AudioAttributes aa = new AudioAttributes.Builder()
-                .setUsage(AudioAttributes.USAGE_ALARM).build();
-        UserInfo user = createUser("User",
-                UserManager.USER_TYPE_FULL_SECONDARY,
-                0);
+    public void testAlarmOnBackgroundUser_foregroundUserNotified() throws RemoteException {
+        AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build();
+        UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0);
         final int fgUserId = mSpiedContext.getUserId();
         final int bgUserUid = user.id * 100000;
         doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser();
@@ -95,10 +108,9 @@
     }
 
     @Test
-    public void testMediaOnBackgroundUser_ForegroundUserNotNotified() throws RemoteException {
+    public void testMediaOnBackgroundUser_foregroundUserNotNotified() throws RemoteException {
         AudioAttributes aa = new AudioAttributes.Builder()
                 .setUsage(AudioAttributes.USAGE_MEDIA).build();
-        UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0);
         final int bgUserUid = mSpiedContext.getUserId() * 100000;
         AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "",
                 /* packageName= */ "com.android.car.audio", AudioManager.AUDIOFOCUS_GAIN,
@@ -109,9 +121,9 @@
     }
 
     @Test
-    public void testAlarmOnForegroundUser_ForegroundUserNotNotified() throws RemoteException {
+    public void testAlarmOnForegroundUser_foregroundUserNotNotified() throws RemoteException {
         AudioAttributes aa = new AudioAttributes.Builder()
-                .setUsage(AudioAttributes.USAGE_ALARM).build();
+                .setUsage(USAGE_ALARM).build();
         final int fgUserId = mSpiedContext.getUserId();
         final int fgUserUid = fgUserId * 100000;
         doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser();
@@ -123,6 +135,109 @@
         verifyZeroInteractions(mNotificationManager);
     }
 
+    @Test
+    public void testMuteAlarmSounds() {
+        final int fgUserId = mSpiedContext.getUserId();
+        int bgUserId = fgUserId + 1;
+        int bgUserUid = bgUserId * 100000;
+        mBackgroundUserSoundNotifier.mNotificationClientUid = bgUserUid;
+
+        AudioManager mockAudioManager = mock(AudioManager.class);
+        when(mSpiedContext.getSystemService(AudioManager.class)).thenReturn(mockAudioManager);
+
+        AudioPlaybackConfiguration apc1 = mock(AudioPlaybackConfiguration.class);
+        when(apc1.getClientUid()).thenReturn(bgUserUid);
+        when(apc1.getPlayerProxy()).thenReturn(mock(PlayerProxy.class));
+
+        AudioPlaybackConfiguration apc2 = mock(AudioPlaybackConfiguration.class);
+        when(apc2.getClientUid()).thenReturn(bgUserUid + 1);
+        when(apc2.getPlayerProxy()).thenReturn(mock(PlayerProxy.class));
+
+        List<AudioPlaybackConfiguration> configs = new ArrayList<>();
+        configs.add(apc1);
+        configs.add(apc2);
+        when(mockAudioManager.getActivePlaybackConfigurations()).thenReturn(configs);
+
+        AudioPolicy mockAudioPolicy = mock(AudioPolicy.class);
+
+        AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build();
+        AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", /* packageName= */ "",
+                AudioManager.AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_NONE, /* flags= */ 0,
+                Build.VERSION.SDK_INT);
+        Stack<AudioFocusInfo> focusStack = new Stack<>();
+        focusStack.add(afi);
+        doReturn(focusStack).when(mockAudioPolicy).getFocusStack();
+        mBackgroundUserSoundNotifier.mFocusControlAudioPolicy = mockAudioPolicy;
+
+        mBackgroundUserSoundNotifier.muteAlarmSounds(mSpiedContext);
+
+        verify(apc1.getPlayerProxy()).stop();
+        verify(apc2.getPlayerProxy(), never()).stop();
+    }
+
+    @Test
+    public void testOnAudioFocusGrant_alarmOnBackgroundUser_notifiesForegroundUser() {
+        final int fgUserId = mSpiedContext.getUserId();
+        UserInfo bgUser = createUser("Background User",  UserManager.USER_TYPE_FULL_SECONDARY, 0);
+        int bgUserUid = bgUser.id * 100000;
+
+        AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build();
+        AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", "",
+                AudioManager.AUDIOFOCUS_GAIN, 0, 0, Build.VERSION.SDK_INT);
+
+        mBackgroundUserSoundNotifier.getAudioPolicyFocusListener()
+                .onAudioFocusGrant(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+
+        verify(mNotificationManager)
+                .notifyAsUser(eq(BackgroundUserSoundNotifier.class.getSimpleName()),
+                        eq(afi.getClientUid()), any(Notification.class),
+                        eq(UserHandle.of(fgUserId)));
+    }
+
+
+    @Test
+    public void testCreateNotification_UserSwitcherEnabled_bothActionsAvailable() {
+        String userName = "BgUser";
+
+        doReturn(true).when(mUserManager).isUserSwitcherEnabled();
+        doReturn(UserManager.SWITCHABILITY_STATUS_OK)
+                .when(mUserManager).getUserSwitchability(any());
+
+        Notification notification = mBackgroundUserSoundNotifier.createNotification(userName,
+                mSpiedContext);
+
+        assertEquals("Alarm for BgUser", notification.extras.getString(
+                Notification.EXTRA_TITLE));
+        assertEquals(Notification.CATEGORY_REMINDER, notification.category);
+        assertEquals(Notification.VISIBILITY_PUBLIC, notification.visibility);
+        assertEquals(com.android.internal.R.drawable.ic_audio_alarm,
+                notification.getSmallIcon().getResId());
+
+        assertEquals(2, notification.actions.length);
+        assertEquals(mSpiedContext.getString(
+                com.android.internal.R.string.bg_user_sound_notification_button_mute),
+                notification.actions[0].title);
+        assertEquals(mSpiedContext.getString(
+                com.android.internal.R.string.bg_user_sound_notification_button_switch_user),
+                notification.actions[1].title);
+    }
+
+    @Test
+    public void testCreateNotification_UserSwitcherDisabled_onlyMuteActionAvailable() {
+        String userName = "BgUser";
+
+        doReturn(false).when(mUserManager).isUserSwitcherEnabled();
+        doReturn(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED)
+                .when(mUserManager).getUserSwitchability(any());
+
+        Notification notification = mBackgroundUserSoundNotifier.createNotification(userName,
+                mSpiedContext);
+
+        assertEquals(1, notification.actions.length);
+        assertEquals(mSpiedContext.getString(
+                com.android.internal.R.string.bg_user_sound_notification_button_mute),
+                notification.actions[0].title);
+    }
 
     private UserInfo createUser(String name, String userType, int flags) {
         UserInfo user = mUserManager.createUser(name, userType, flags);
diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
index 1a398c5..e0c393c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
@@ -100,6 +100,7 @@
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
+import com.android.server.pm.UserManagerInternal;
 
 import org.junit.After;
 import org.junit.Before;
@@ -145,6 +146,7 @@
 
     private static final String URI_SCHEME_PACKAGE = "package";
     private static final int TEST_USER_ID = 50;
+    private static final int TEST_VISIBLE_BACKGROUND_USER_ID = 51;
     private static final UserInfo TEST_USER =
             new UserInfo(TEST_USER_ID, "user", UserInfo.FLAG_FULL);
     private static final int PARENT_USER_ID = 60;
@@ -170,6 +172,7 @@
     private @Mock KeyStoreAuthorization mKeyStoreAuthorization;
     private @Mock LockPatternUtils mLockPatternUtils;
     private @Mock LockSettingsInternal mLockSettingsInternal;
+    private @Mock UserManagerInternal mUserManagerInternal;
     private @Mock PackageManager mPackageManager;
     private @Mock UserManager mUserManager;
     private @Mock IWindowManager mWindowManager;
@@ -224,6 +227,7 @@
         when(mUserManager.getAliveUsers()).thenReturn(List.of(TEST_USER));
         when(mUserManager.getEnabledProfileIds(TEST_USER_ID)).thenReturn(new int[0]);
         when(mUserManager.getUserInfo(TEST_USER_ID)).thenReturn(TEST_USER);
+        when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(false);
 
         when(mWindowManager.isKeyguardLocked()).thenReturn(true);
 
@@ -593,6 +597,54 @@
         verify(mTrustListener, never()).onTrustManagedChanged(anyBoolean(), anyInt());
     }
 
+    @Test
+    public void testDeviceLocked_visibleBackgroundUser_userLocked() throws RemoteException {
+        setupVisibleBackgroundUser(/* visible= */ true, /* unlocked= */ false);
+        mService.waitForIdle();
+        mTrustManager.reportEnabledTrustAgentsChanged(TEST_VISIBLE_BACKGROUND_USER_ID);
+        mService.waitForIdle();
+        assertThat(mService.isDeviceLockedInner(TEST_VISIBLE_BACKGROUND_USER_ID)).isTrue();
+    }
+
+    @Test
+    public void testDeviceLocked_visibleBackgroundUser_userUnlocked() throws RemoteException {
+        setupVisibleBackgroundUser(/* visible= */ true, /* unlocked= */ true);
+        mService.waitForIdle();
+        mTrustManager.reportEnabledTrustAgentsChanged(TEST_VISIBLE_BACKGROUND_USER_ID);
+        mService.waitForIdle();
+        assertThat(mService.isDeviceLockedInner(TEST_VISIBLE_BACKGROUND_USER_ID)).isFalse();
+    }
+
+    @Test
+    public void testDeviceLocked_invisibleBackgroundUser_userUnlocked() throws RemoteException {
+        setupVisibleBackgroundUser(/* visible= */ false, /* unlocked= */ true);
+        mService.waitForIdle();
+        mTrustManager.reportEnabledTrustAgentsChanged(TEST_VISIBLE_BACKGROUND_USER_ID);
+        mService.waitForIdle();
+        assertThat(mService.isDeviceLockedInner(TEST_VISIBLE_BACKGROUND_USER_ID)).isTrue();
+    }
+
+    private void setupVisibleBackgroundUser(boolean visible, boolean unlocked) {
+        UserInfo info = new UserInfo(TEST_VISIBLE_BACKGROUND_USER_ID, "visible bg user",
+                UserInfo.FLAG_FULL);
+
+        when(mActivityManager.isUserRunning(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(true);
+
+        when(mLockPatternUtils.isSecure(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(true);
+
+        when(mUserManager.getAliveUsers()).thenReturn(List.of(TEST_USER, info));
+        when(mUserManager.getEnabledProfileIds(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(
+                new int[0]);
+        when(mUserManager.getUserInfo(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(info);
+        when(mUserManager.isUserUnlocked(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(unlocked);
+        when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
+
+        LocalServices.removeServiceForTest(UserManagerInternal.class);
+        LocalServices.addService(UserManagerInternal.class, mUserManagerInternal);
+        when(mUserManagerInternal.isVisibleBackgroundFullUser(
+                TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(visible);
+    }
+
     private void setUpRenewableTrust(ITrustAgentService trustAgent) throws RemoteException {
         ITrustAgentServiceCallback callback = getCallback(trustAgent);
         callback.setManagingTrust(true);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 8ee7e03..84c4f62 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -32,6 +32,7 @@
 import static android.service.notification.Condition.STATE_TRUE;
 import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON;
 import static android.service.notification.ZenModeConfig.XML_VERSION_MODES_API;
+import static android.service.notification.ZenModeConfig.XML_VERSION_MODES_UI;
 import static android.service.notification.ZenModeConfig.ZEN_TAG;
 import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE;
 import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_NONE;
@@ -1169,6 +1170,23 @@
         assertThat(suppressedEffectsOf(result)).isEqualTo(suppressedEffectsOf(policy));
     }
 
+    @Test
+    public void readXml_fixesWronglyDisabledManualRule() throws Exception {
+        ZenModeConfig config = getCustomConfig();
+        if (!Flags.modesUi()) {
+            config.manualRule = new ZenModeConfig.ZenRule();
+            config.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+        }
+        config.manualRule.enabled = false;
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        writeConfigXml(config, XML_VERSION_MODES_UI, /* forBackup= */ false, baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        ZenModeConfig fromXml = readConfigXml(bais);
+
+        assertThat(fromXml.manualRule.enabled).isTrue();
+    }
+
     private static String suppressedEffectsOf(Policy policy) {
         return suppressedEffectsToString(policy.suppressedVisualEffects) + "("
                 + policy.suppressedVisualEffects + ")";
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index f743401..7bce828 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -1640,7 +1640,7 @@
                 .build();
         setUpApp(display);
         prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
-        mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
         assertFalse(mActivity.inSizeCompatMode());
 
         // Resize app to make original app bounds larger than parent bounds.
@@ -1667,7 +1667,7 @@
                 .build();
         setUpApp(display);
         prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
-        mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
         assertFalse(mActivity.inSizeCompatMode());
 
         // Resize app to make original app bounds smaller than parent bounds.
@@ -1692,7 +1692,7 @@
                 .build();
         setUpApp(display);
         prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
-        mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
         assertFalse(mActivity.inSizeCompatMode());
         final Rect originalAppBounds = mActivity.getBounds();
 
@@ -1705,6 +1705,38 @@
         assertEquals(originalAppBounds, mActivity.getBounds());
     }
 
+    /**
+     * Test that when desktop mode is enabled, a freeform unresizeable activity is not up-scaled
+     * when exiting freeform despite its larger parent bounds.
+     */
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void testCompatScaling_freeformUnresizeableApp_exitFreeform_notScaled() {
+        doReturn(true).when(() ->
+                DesktopModeHelper.canEnterDesktopMode(any()));
+        final int dw = 600;
+        final int dh = 800;
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM)
+                .build();
+        setUpApp(display);
+        prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
+        mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM);
+        final Rect originalAppBounds = mActivity.getBounds();
+
+        assertFalse(mActivity.inSizeCompatMode());
+
+        // Resize app to make original app bounds smaller than parent bounds.
+        mTask.getWindowConfiguration().setAppBounds(
+                new Rect(0, 0, dw + 300, dh + 400));
+        // Change windowing mode from freeform to fullscreen
+        mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        mActivity.onConfigurationChanged(mTask.getConfiguration());
+        // App should enter size compat mode but remain its original size.
+        assertTrue(mActivity.inSizeCompatMode());
+        assertEquals(originalAppBounds, mActivity.getBounds());
+    }
+
     @Test
     public void testGetLetterboxInnerBounds_noScalingApplied() {
         // Set up a display in portrait and ignoring orientation request.
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt
index 92b6b93..82e53c8 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt
@@ -54,7 +54,7 @@
         }
         transitions {
             device.sleep()
-            wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
+            wmHelper.StateSyncBuilder().withKeyguardShowing().waitForAndVerify()
             UnlockScreenRule.unlockScreen(device)
             wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify()
         }
diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
index aa73c39..c61a250 100644
--- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
+++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
@@ -36,7 +36,6 @@
 import com.android.cts.input.inputeventmatchers.withPressure
 import com.android.cts.input.inputeventmatchers.withRawCoords
 import com.android.cts.input.inputeventmatchers.withSource
-import java.io.InputStream
 import junit.framework.Assert.fail
 import org.hamcrest.Matchers.allOf
 import org.junit.Before
@@ -130,17 +129,18 @@
                         scenario.virtualDisplay.display.uniqueId!!,
                     )
 
-                    injectUinputEvents()
+                    injectUinputEvents().use {
+                        if (DEBUG_RECEIVED_EVENTS) {
+                            printReceivedEventsToLogcat(scenario.activity)
+                            fail("Test cannot pass in debug mode!")
+                        }
 
-                    if (DEBUG_RECEIVED_EVENTS) {
-                        printReceivedEventsToLogcat(scenario.activity)
-                        fail("Test cannot pass in debug mode!")
+                        val verifier = EventVerifier(
+                            BatchedEventSplitter { scenario.activity.getInputEvent() }
+                        )
+                        verifyEvents(verifier)
+                        scenario.activity.assertNoEvents()
                     }
-
-                    val verifier =
-                        EventVerifier(BatchedEventSplitter { scenario.activity.getInputEvent() })
-                    verifyEvents(verifier)
-                    scenario.activity.assertNoEvents()
                 } finally {
                     inputManager.removeUniqueIdAssociationByPort(inputPort)
                 }
@@ -162,14 +162,32 @@
         }
     }
 
-    private fun injectUinputEvents() {
+    /**
+     * Plays back the evemu recording associated with the current test case by injecting it via
+     * the `uinput` shell command in interactive mode. The recording playback will begin
+     * immediately, and the shell command (and the associated input device) will remain alive
+     * until the returned [AutoCloseable] is closed.
+     */
+    private fun injectUinputEvents(): AutoCloseable {
         val fds = instrumentation.uiAutomation!!.executeShellCommandRw("uinput -")
+        // We do not need to use stdout in this test.
+        fds[0].close()
 
-        ParcelFileDescriptor.AutoCloseOutputStream(fds[1]).use { stdIn ->
-            val inputStream: InputStream = instrumentation.context.resources.openRawResource(
+        return ParcelFileDescriptor.AutoCloseOutputStream(fds[1]).also { stdin ->
+            instrumentation.context.resources.openRawResource(
                 testData.uinputRecordingResource,
-            )
-            stdIn.write(inputStream.readBytes())
+            ).use { inputStream ->
+                stdin.write(inputStream.readBytes())
+
+                // TODO(b/367419268): Remove extra event injection when uinput parsing is fixed.
+                // Inject an extra sync event with an arbitrarily large timestamp, because the
+                // uinput command will not process the last event until either the next event is
+                // parsed, or fd is closed. Injecting this sync allows us complete injection of
+                // the evemu recording and extend the lifetime of the input device by keeping this
+                // fd open.
+                stdin.write("\nE: 9999.99 0 0 0\n".toByteArray())
+                stdin.flush()
+            }
         }
     }