Merge "Shift Key gestures handled by InputManager to IMS from PWM" 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/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/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/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 3c61f4f..3846972 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -84,17 +84,6 @@
 }
 
 flag {
-  name: "fix_font_update_failure"
-  namespace: "text"
-  description: "There was a bug of updating system font from Android 13 to 14. This flag for fixing the migration failure."
-  is_fixed_read_only: true
-  bug: "331717791"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
   name: "fix_misaligned_context_menu"
   namespace: "text"
   description: "Fix the context menu misalignment and incosistent icon size."
@@ -154,26 +143,6 @@
 }
 
 flag {
-  name: "portuguese_hyphenator"
-  namespace: "text"
-  description: "Portuguese taiored hyphenator"
-  bug: "344656282"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
-  name: "dont_break_email_in_nobreak_tag"
-  namespace: "text"
-  description: "Prevent line break inside email."
-  bug: "350691716"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
   name: "handwriting_gesture_with_transformation"
   namespace: "text"
   description: "Fix handwriting gesture is not working when view has transformation."
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 71ceaf7..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,
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/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/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index b9751c8..d90455a 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -920,7 +920,8 @@
             final Configuration.Builder builder = Configuration.Builder.withSurface(
                             cujType,
                             jankContext.getDisplayContext(),
-                            jankContext.getTargetSurfaceControl())
+                            jankContext.getTargetSurfaceControl(),
+                            jankContext.getDisplayContext().getMainThreadHandler())
                     .setTag(String.format(Locale.US, "%d@%d@%s", animType,
                             useSeparatedThread ? 0 : 1, jankContext.getHostPackageName()));
             InteractionJankMonitor.getInstance().begin(builder);
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index c7e1fba..ef08e49 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -330,9 +330,10 @@
      * @param cujType the specific {@link Cuj.CujType}.
      * @return boolean true if the tracker is started successfully, false otherwise.
      */
-    public boolean begin(SurfaceControl surface, Context context, @Cuj.CujType int cujType) {
+    public boolean begin(SurfaceControl surface, Context context, Handler handler,
+            @Cuj.CujType int cujType) {
         try {
-            return begin(Configuration.Builder.withSurface(cujType, context, surface));
+            return begin(Configuration.Builder.withSurface(cujType, context, surface, handler));
         } catch (IllegalArgumentException ex) {
             Log.d(TAG, "Build configuration failed!", ex);
             return false;
@@ -348,11 +349,12 @@
      * @param tag a tag containing extra information about the interaction.
      * @return boolean true if the tracker is started successfully, false otherwise.
      */
-    public boolean begin(SurfaceControl surface, Context context, @Cuj.CujType int cujType,
+    public boolean begin(SurfaceControl surface, Context context, Handler handler,
+            @Cuj.CujType int cujType,
             String tag) {
         try {
             final Configuration.Builder builder =
-                    Configuration.Builder.withSurface(cujType, context, surface);
+                    Configuration.Builder.withSurface(cujType, context, surface, handler);
             if (!TextUtils.isEmpty(tag)) {
                 builder.setTag(tag);
             }
@@ -689,20 +691,23 @@
             private SurfaceControl mAttrSurfaceControl;
             private final @Cuj.CujType int mAttrCujType;
             private boolean mAttrDeferMonitor = true;
+            private Handler mHandler = null;
 
             /**
              * Creates a builder which instruments only surface.
              * @param cuj The enum defined in {@link Cuj.CujType}.
              * @param context context
              * @param surfaceControl surface control
+             * @param uiThreadHandler UI thread for that surface
              * @return builder
              */
             public static Builder withSurface(@Cuj.CujType int cuj, @NonNull Context context,
-                    @NonNull SurfaceControl surfaceControl) {
+                    @NonNull SurfaceControl surfaceControl, @NonNull Handler uiThreadHandler) {
                 return new Builder(cuj)
                         .setContext(context)
                         .setSurfaceControl(surfaceControl)
-                        .setSurfaceOnly(true);
+                        .setSurfaceOnly(true)
+                        .setHandler(uiThreadHandler);
             }
 
             /**
@@ -722,6 +727,18 @@
             }
 
             /**
+             * Specifies the UI thread handler. If not provided, the View's one will be used.
+             * If only a surface is provided without handler, the app main thread will be used.
+             *
+             * @param uiThreadHandler handler associated to the cuj UI thread
+             * @return builder
+             */
+            public Builder setHandler(Handler uiThreadHandler) {
+                mHandler = uiThreadHandler;
+                return this;
+            }
+
+            /**
              * Specifies a view, must be set if {@link #setSurfaceOnly(boolean)} is set to false.
              * @param view an attached view
              * @return builder
@@ -798,13 +815,13 @@
                 return new Configuration(
                         mAttrCujType, mAttrView, mAttrTag, mAttrTimeout,
                         mAttrSurfaceOnly, mAttrContext, mAttrSurfaceControl,
-                        mAttrDeferMonitor);
+                        mAttrDeferMonitor, mHandler);
             }
         }
 
         private Configuration(@Cuj.CujType int cuj, View view, @NonNull String tag, long timeout,
                 boolean surfaceOnly, Context context, SurfaceControl surfaceControl,
-                boolean deferMonitor) {
+                boolean deferMonitor, Handler handler) {
             mCujType = cuj;
             mTag = tag;
             mSessionName = generateSessionName(Cuj.getNameOfCuj(cuj), tag);
@@ -816,8 +833,16 @@
                     : (view != null ? view.getContext().getApplicationContext() : null);
             mSurfaceControl = surfaceControl;
             mDeferMonitor = deferMonitor;
+            if (handler != null) {
+                mHandler = handler;
+            } else if (mSurfaceOnly) {
+                Log.w(TAG, "No UIThread provided for " + mSessionName
+                        + " (surface only). Defaulting to app main thread.");
+                mHandler = mContext.getMainThreadHandler();
+            } else {
+                mHandler = mView.getHandler();
+            }
             validate();
-            mHandler = mSurfaceOnly ? mContext.getMainThreadHandler() : mView.getHandler();
         }
 
         @VisibleForTesting
@@ -858,6 +883,12 @@
                     shouldThrow = true;
                     msg.append("Must pass in a valid surface control if only instrument surface; ");
                 }
+                if (mHandler == null) {
+                    shouldThrow = true;
+                    msg.append(
+                            "Must pass a UI thread handler when only a surface control is "
+                                    + "provided.");
+                }
             } else {
                 if (!hasValidView()) {
                     shouldThrow = true;
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/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
index 5af272c..e0823b8 100644
--- a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
@@ -24,6 +24,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -117,6 +118,7 @@
 
         // Simulate a trace session and see if begin / end are invoked.
         assertThat(monitor.begin(mSurfaceControl, mActivity.getApplicationContext(),
+                mActivity.getMainThreadHandler(),
                 Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
         verify(tracker).begin();
         assertThat(monitor.end(Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
@@ -188,6 +190,25 @@
         assertThat(generateSessionName(cujName, tooLongTag)).isEqualTo(expectedTrimmedName);
     }
 
+    @Test
+    public void validateConfiguration_surfaceOnlyAndNotDeferMonitor_throwsError() {
+        Configuration.Builder builder = Configuration.Builder.withSurface(1,
+                mActivity.getApplicationContext(), mSurfaceControl,
+                mActivity.getMainThreadHandler()).setDeferMonitorForAnimationStart(false);
+
+        assertThrows(IllegalStateException.class, builder::build);
+    }
+
+    @Test
+    public void validateConfiguration_surfaceOnlyAndDeferMonitor_doesNotThrowError() {
+        Configuration.Builder builder = Configuration.Builder.withSurface(1,
+                mActivity.getApplicationContext(),
+                mSurfaceControl, mActivity.getMainThreadHandler()).setDeferMonitorForAnimationStart(
+                true);
+
+        builder.build(); // no exception.
+    }
+
     private InteractionJankMonitor createMockedInteractionJankMonitor() {
         InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
         doReturn(true).when(monitor).shouldMonitor();
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/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index f478b44..3e5adf3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -117,6 +117,7 @@
      */
     private static final long MAX_ANIMATION_DURATION = 2000;
     private final LatencyTracker mLatencyTracker;
+    @ShellMainThread private final Handler mHandler;
 
     /** True when a back gesture is ongoing */
     private boolean mBackGestureStarted = false;
@@ -218,7 +219,8 @@
             @NonNull BackAnimationBackground backAnimationBackground,
             ShellBackAnimationRegistry shellBackAnimationRegistry,
             ShellCommandHandler shellCommandHandler,
-            Transitions transitions) {
+            Transitions transitions,
+            @ShellMainThread Handler handler) {
         this(
                 shellInit,
                 shellController,
@@ -230,7 +232,8 @@
                 backAnimationBackground,
                 shellBackAnimationRegistry,
                 shellCommandHandler,
-                transitions);
+                transitions,
+                handler);
     }
 
     @VisibleForTesting
@@ -245,7 +248,8 @@
             @NonNull BackAnimationBackground backAnimationBackground,
             ShellBackAnimationRegistry shellBackAnimationRegistry,
             ShellCommandHandler shellCommandHandler,
-            Transitions transitions) {
+            Transitions transitions,
+            @NonNull @ShellMainThread Handler handler) {
         mShellController = shellController;
         mShellExecutor = shellExecutor;
         mActivityTaskManager = activityTaskManager;
@@ -263,6 +267,7 @@
         mTransitions = transitions;
         mBackTransitionHandler = new BackTransitionHandler();
         mTransitions.addHandler(mBackTransitionHandler);
+        mHandler = handler;
         updateTouchableArea();
     }
 
@@ -399,7 +404,7 @@
         }
     }
 
-    private static class IBackAnimationImpl extends IBackAnimation.Stub
+    private class IBackAnimationImpl extends IBackAnimation.Stub
             implements ExternalInterfaceBinder {
         private BackAnimationController mController;
 
@@ -417,7 +422,8 @@
                                     callback,
                                     runner,
                                     controller.mContext,
-                                    CUJ_PREDICTIVE_BACK_HOME)));
+                                    CUJ_PREDICTIVE_BACK_HOME,
+                                    mHandler)));
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index e24df0b..9ca9b73 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.IRemoteAnimationFinishedCallback;
@@ -31,6 +32,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.jank.Cuj.CujType;
 import com.android.internal.jank.InteractionJankMonitor;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 /**
  * Used to register the animation callback and runner, it will trigger result if gesture was finish
@@ -45,6 +47,8 @@
     private final IRemoteAnimationRunner mRunner;
     private final @CujType int mCujType;
     private final Context mContext;
+    @ShellMainThread
+    private final Handler mHandler;
 
     // Whether we are waiting to receive onAnimationStart
     private boolean mWaitingAnimation;
@@ -56,18 +60,35 @@
             @NonNull IOnBackInvokedCallback callback,
             @NonNull IRemoteAnimationRunner runner,
             @NonNull Context context,
-            @CujType int cujType) {
+            @CujType int cujType,
+            @ShellMainThread Handler handler) {
         mCallback = callback;
         mRunner = runner;
         mCujType = cujType;
         mContext = context;
+        mHandler = handler;
     }
 
     public BackAnimationRunner(
             @NonNull IOnBackInvokedCallback callback,
             @NonNull IRemoteAnimationRunner runner,
-            @NonNull Context context) {
-        this(callback, runner, context, NO_CUJ);
+            @NonNull Context context,
+            @ShellMainThread Handler handler
+    ) {
+        this(callback, runner, context, NO_CUJ, handler);
+    }
+
+    /**
+     * @deprecated Use {@link BackAnimationRunner} constructor providing an handler for the ui
+     * thread of the animation.
+     */
+    @Deprecated
+    public BackAnimationRunner(
+            @NonNull IOnBackInvokedCallback callback,
+            @NonNull IRemoteAnimationRunner runner,
+            @NonNull Context context
+    ) {
+        this(callback, runner, context, NO_CUJ, context.getMainThreadHandler());
     }
 
     /** Returns the registered animation runner */
@@ -100,7 +121,7 @@
         mWaitingAnimation = false;
         if (shouldMonitorCUJ(apps)) {
             interactionJankMonitor.begin(
-                    apps[0].leash, mContext, mCujType);
+                    apps[0].leash, mContext, mHandler, mCujType);
         }
         try {
             getRunner().onAnimationStart(TRANSIT_OLD_UNSET, apps, wallpapers,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
index 32e809a..3733930 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -26,6 +26,7 @@
 import android.graphics.PointF
 import android.graphics.Rect
 import android.graphics.RectF
+import android.os.Handler
 import android.os.RemoteException
 import android.util.TimeUtils
 import android.view.Choreographer
@@ -53,6 +54,7 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.protolog.ShellProtoLogGroup
 import com.android.wm.shell.shared.animation.Interpolators
+import com.android.wm.shell.shared.annotations.ShellMainThread
 import kotlin.math.abs
 import kotlin.math.max
 import kotlin.math.min
@@ -61,7 +63,8 @@
     private val context: Context,
     private val background: BackAnimationBackground,
     private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
-    protected val transaction: SurfaceControl.Transaction
+    protected val transaction: SurfaceControl.Transaction,
+    @ShellMainThread handler: Handler,
 ) : ShellBackAnimation() {
 
     protected val startClosingRect = RectF()
@@ -80,7 +83,13 @@
     private var statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
 
     private val backAnimationRunner =
-        BackAnimationRunner(Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY)
+        BackAnimationRunner(
+            Callback(),
+            Runner(),
+            context,
+            Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY,
+            handler,
+        )
     private val initialTouchPos = PointF()
     private val transformMatrix = Matrix()
     private val tmpFloat9 = FloatArray(9)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index 3fcceca..7a56979 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -34,6 +34,7 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.view.Choreographer;
 import android.view.IRemoteAnimationFinishedCallback;
@@ -52,6 +53,7 @@
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 import javax.inject.Inject;
 
@@ -113,9 +115,10 @@
     private float mVerticalMargin;
 
     @Inject
-    public CrossTaskBackAnimation(Context context, BackAnimationBackground background) {
+    public CrossTaskBackAnimation(Context context, BackAnimationBackground background,
+            @ShellMainThread Handler handler) {
         mBackAnimationRunner = new BackAnimationRunner(
-                new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_TASK);
+                new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_TASK, handler);
         mBackground = background;
         mContext = context;
         loadResources();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
index b02f97b..2f7666b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
@@ -18,6 +18,7 @@
 import android.content.Context
 import android.graphics.Rect
 import android.graphics.RectF
+import android.os.Handler
 import android.util.MathUtils
 import android.view.SurfaceControl
 import android.view.animation.Animation
@@ -30,6 +31,7 @@
 import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.shared.annotations.ShellMainThread
 import javax.inject.Inject
 import kotlin.math.max
 import kotlin.math.min
@@ -40,13 +42,15 @@
     background: BackAnimationBackground,
     rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
     transaction: SurfaceControl.Transaction,
-    private val customAnimationLoader: CustomAnimationLoader
+    private val customAnimationLoader: CustomAnimationLoader,
+    @ShellMainThread handler: Handler,
 ) :
     CrossActivityBackAnimation(
         context,
         background,
         rootTaskDisplayAreaOrganizer,
-        transaction
+        transaction,
+        handler
     ) {
 
     private var enterAnimation: Animation? = null
@@ -59,7 +63,8 @@
     constructor(
         context: Context,
         background: BackAnimationBackground,
-        rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+        rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+        @ShellMainThread handler: Handler,
     ) : this(
         context,
         background,
@@ -67,7 +72,8 @@
         SurfaceControl.Transaction(),
         CustomAnimationLoader(
             TransitionAnimation(context, false /* debug */, "CustomCrossActivityBackAnimation")
-        )
+        ),
+        handler,
     )
 
     override fun preparePreCommitClosingRectMovement(swipeEdge: Int) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
index 66d8a5f..eecd769 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
@@ -16,11 +16,13 @@
 package com.android.wm.shell.back
 
 import android.content.Context
+import android.os.Handler
 import android.view.SurfaceControl
 import android.window.BackEvent
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.shared.animation.Interpolators
+import com.android.wm.shell.shared.annotations.ShellMainThread
 import javax.inject.Inject
 import kotlin.math.max
 
@@ -30,13 +32,15 @@
 constructor(
     context: Context,
     background: BackAnimationBackground,
-    rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+    rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+    @ShellMainThread handler: Handler,
 ) :
     CrossActivityBackAnimation(
         context,
         background,
         rootTaskDisplayAreaOrganizer,
-        SurfaceControl.Transaction()
+        SurfaceControl.Transaction(),
+        handler
     ) {
 
     private val postCommitInterpolator = Interpolators.EMPHASIZED
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index d253736..c545d73 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -2007,7 +2007,7 @@
         @Override
         public void selectionChanged(BubbleViewProvider selectedBubble) {
             // Only need to update the layer view if we're currently expanded for selection changes.
-            if (mLayerView != null && mLayerView.isExpanded()) {
+            if (mLayerView != null && isStackExpanded()) {
                 mLayerView.showExpandedView(selectedBubble);
             }
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 1367b7e..1c9c195 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -186,10 +186,6 @@
         if (expandedView == null) {
             return;
         }
-        if (mExpandedBubble != null && mIsExpanded && b.getKey().equals(mExpandedBubble.getKey())) {
-            // Already showing this bubble, skip animating
-            return;
-        }
         if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) {
             removeView(mExpandedView);
             mExpandedView = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index b8aa1b1..4b55fd0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -48,6 +48,7 @@
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.view.Display;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
@@ -75,6 +76,7 @@
 import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
 import com.android.wm.shell.shared.split.SplitScreenConstants.SnapPosition;
 import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition;
@@ -116,6 +118,8 @@
             new PathInterpolator(0.2f, 0f, 0f, 1f);
     private static final Interpolator GROW_INTERPOLATOR =
             new PathInterpolator(0.45f, 0f, 0.5f, 1f);
+    @ShellMainThread
+    private final Handler mHandler;
 
     private int mDividerWindowWidth;
     private int mDividerInsets;
@@ -166,7 +170,8 @@
             SplitLayoutHandler splitLayoutHandler,
             SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks,
             DisplayController displayController, DisplayImeController displayImeController,
-            ShellTaskOrganizer taskOrganizer, int parallaxType) {
+            ShellTaskOrganizer taskOrganizer, int parallaxType, @ShellMainThread Handler handler) {
+        mHandler = handler;
         mContext = context.createConfigurationContext(configuration);
         mOrientation = configuration.orientation;
         mRotation = configuration.windowConfiguration.getRotation();
@@ -598,7 +603,8 @@
     }
 
     void onStartDragging() {
-        mInteractionJankMonitor.begin(getDividerLeash(), mContext, CUJ_SPLIT_SCREEN_RESIZE);
+        mInteractionJankMonitor.begin(getDividerLeash(), mContext, mHandler,
+                CUJ_SPLIT_SCREEN_RESIZE);
     }
 
     void onDraggingCancelled() {
@@ -756,7 +762,7 @@
             @Override
             public void onAnimationStart(Animator animation) {
                 mInteractionJankMonitor.begin(getDividerLeash(),
-                        mContext, CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER);
+                        mContext, mHandler, CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER);
             }
 
             @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 4adea23..722fe1f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -444,7 +444,9 @@
             BackAnimationBackground backAnimationBackground,
             Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry,
             ShellCommandHandler shellCommandHandler,
-            Transitions transitions) {
+            Transitions transitions,
+            @ShellMainThread Handler handler
+    ) {
         if (BackAnimationController.IS_ENABLED) {
             return shellBackAnimationRegistry.map(
                     (animations) ->
@@ -457,7 +459,8 @@
                                     backAnimationBackground,
                                     animations,
                                     shellCommandHandler,
-                                    transitions));
+                                    transitions,
+                                    handler));
         }
         return Optional.empty();
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index b151c8b..b47adb4 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
@@ -355,7 +355,8 @@
             @ShellMainThread ShellExecutor mainExecutor,
             @ShellAnimationThread ShellExecutor animExecutor,
             @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
-            InteractionJankMonitor interactionJankMonitor) {
+            InteractionJankMonitor interactionJankMonitor,
+            @ShellMainThread Handler handler) {
         return new FreeformTaskTransitionHandler(
                 shellInit,
                 transitions,
@@ -365,7 +366,8 @@
                 mainExecutor,
                 animExecutor,
                 desktopModeTaskRepository,
-                interactionJankMonitor);
+                interactionJankMonitor,
+                handler);
     }
 
     @WMSingleton
@@ -487,10 +489,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);
     }
 
@@ -616,6 +619,7 @@
             RecentsTransitionHandler recentsTransitionHandler,
             MultiInstanceHelper multiInstanceHelper,
             @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler,
             Optional<DesktopTasksLimiter> desktopTasksLimiter,
             Optional<RecentTasksController> recentTasksController,
             InteractionJankMonitor interactionJankMonitor) {
@@ -628,7 +632,7 @@
                 dragToDesktopTransitionHandler, desktopModeTaskRepository,
                 desktopModeLoggerTransitionObserver, launchAdjacentController,
                 recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
-                recentTasksController.orElse(null), interactionJankMonitor);
+                recentTasksController.orElse(null), interactionJankMonitor, mainHandler);
     }
 
     @WMSingleton
@@ -638,7 +642,8 @@
             Transitions transitions,
             @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
             ShellTaskOrganizer shellTaskOrganizer,
-            InteractionJankMonitor interactionJankMonitor) {
+            InteractionJankMonitor interactionJankMonitor,
+            @ShellMainThread Handler handler) {
         int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context);
         if (!DesktopModeStatus.canEnterDesktopMode(context)
                 || !ENABLE_DESKTOP_WINDOWING_TASK_LIMIT.isEnabled(context)
@@ -652,7 +657,8 @@
                         shellTaskOrganizer,
                         maxTaskLimit,
                         interactionJankMonitor,
-                        context)
+                        context,
+                        handler)
         );
     }
 
@@ -699,9 +705,10 @@
     static ExitDesktopTaskTransitionHandler provideExitDesktopTaskTransitionHandler(
             Transitions transitions,
             Context context,
-            InteractionJankMonitor interactionJankMonitor) {
+            InteractionJankMonitor interactionJankMonitor,
+            @ShellMainThread Handler handler) {
         return new ExitDesktopTaskTransitionHandler(
-            transitions, context, interactionJankMonitor);
+            transitions, context, interactionJankMonitor, handler);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
index 037fbb2..3a4764d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
@@ -101,7 +101,8 @@
             DisplayInsetsController displayInsetsController,
             TabletopModeController pipTabletopController,
             Optional<OneHandedController> oneHandedController,
-            @ShellMainThread ShellExecutor mainExecutor) {
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler handler) {
         return Optional.ofNullable(PipController.create(
                 context, shellInit, shellCommandHandler, shellController,
                 displayController, pipAnimationController, pipAppOpsListener,
@@ -111,7 +112,7 @@
                 pipTransitionState, pipTouchHandler, pipTransitionController,
                 windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
                 displayInsetsController, pipTabletopController, oneHandedController,
-                mainExecutor));
+                mainExecutor, handler));
     }
 
     // Handler is used by Icon.loadDrawableAsync
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index c74f4a7..853284a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -35,6 +35,7 @@
 import android.graphics.Rect
 import android.graphics.Region
 import android.os.Binder
+import android.os.Handler
 import android.os.IBinder
 import android.os.SystemProperties
 import android.util.Size
@@ -136,7 +137,8 @@
     @ShellMainThread private val mainExecutor: ShellExecutor,
     private val desktopTasksLimiter: Optional<DesktopTasksLimiter>,
     private val recentTasksController: RecentTasksController?,
-    private val interactionJankMonitor: InteractionJankMonitor
+    private val interactionJankMonitor: InteractionJankMonitor,
+    @ShellMainThread private val handler: Handler,
 ) :
     RemoteCallable<DesktopTasksController>,
     Transitions.TransitionHandler,
@@ -387,7 +389,7 @@
         taskSurface: SurfaceControl,
     ) {
         logV("startDragToDesktop taskId=%d", taskInfo.taskId)
-        interactionJankMonitor.begin(taskSurface, context,
+        interactionJankMonitor.begin(taskSurface, context, handler,
             CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
         dragToDesktopTransitionHandler.startDragToDesktopTransition(
             taskInfo.taskId,
@@ -791,7 +793,7 @@
         releaseVisualIndicator()
         if (!taskInfo.isResizeable && DesktopModeFlags.DISABLE_SNAP_RESIZE.isEnabled(context)) {
             interactionJankMonitor.begin(
-                taskSurface, context, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_non_resizable"
+                taskSurface, context, handler, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_non_resizable"
             )
 
             // reposition non-resizable app back to its original position before being dragged
@@ -804,7 +806,7 @@
             )
         } else {
             interactionJankMonitor.begin(
-                taskSurface, context, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_resizable"
+                taskSurface, context, handler, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_resizable"
             )
             snapToHalfScreen(taskInfo, taskSurface, currentDragBounds, position)
         }
@@ -1604,7 +1606,7 @@
         when (indicatorType) {
             IndicatorType.TO_DESKTOP_INDICATOR -> {
                 // Start a new jank interaction for the drag release to desktop window animation.
-                interactionJankMonitor.begin(taskSurface, context,
+                interactionJankMonitor.begin(taskSurface, context, handler,
                     CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, "to_desktop")
                 finalizeDragToDesktop(taskInfo)
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index 597637d..dae37f4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -18,6 +18,7 @@
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
+import android.os.Handler
 import android.os.IBinder
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_TO_BACK
@@ -29,6 +30,7 @@
 import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.shared.annotations.ShellMainThread
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TransitionObserver
 
@@ -45,7 +47,8 @@
         private val shellTaskOrganizer: ShellTaskOrganizer,
         private val maxTasksLimit: Int,
         private val interactionJankMonitor: InteractionJankMonitor,
-        private val context: Context
+        private val context: Context,
+        @ShellMainThread private val handler: Handler,
 ) {
     private val minimizeTransitionObserver = MinimizeTransitionObserver()
     @VisibleForTesting
@@ -125,7 +128,7 @@
             if (mActiveTaskDetails != null && mActiveTaskDetails.transitionInfo != null) {
                 // Begin minimize window CUJ instrumentation.
                 interactionJankMonitor.begin(
-                    mActiveTaskDetails.transitionInfo?.rootLeash, context,
+                    mActiveTaskDetails.transitionInfo?.rootLeash, context, handler,
                     CUJ_DESKTOP_MODE_MINIMIZE_WINDOW
                 )
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
index e87be52..dedd44f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
@@ -29,6 +29,7 @@
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.os.IBinder;
 import android.util.DisplayMetrics;
 import android.view.SurfaceControl;
@@ -44,6 +45,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.jank.Cuj;
 import com.android.internal.jank.InteractionJankMonitor;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.transition.Transitions;
 
@@ -63,6 +65,8 @@
     private final Context mContext;
     private final Transitions mTransitions;
     private final InteractionJankMonitor mInteractionJankMonitor;
+    @ShellMainThread
+    private final Handler mHandler;
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
     private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback;
     private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
@@ -71,20 +75,24 @@
     public ExitDesktopTaskTransitionHandler(
             Transitions transitions,
             Context context,
-            InteractionJankMonitor interactionJankMonitor
-            ) {
-        this(transitions, SurfaceControl.Transaction::new, context, interactionJankMonitor);
+            InteractionJankMonitor interactionJankMonitor,
+            @ShellMainThread Handler handler
+    ) {
+        this(transitions, SurfaceControl.Transaction::new, context, interactionJankMonitor,
+                handler);
     }
 
     private ExitDesktopTaskTransitionHandler(
             Transitions transitions,
             Supplier<SurfaceControl.Transaction> supplier,
             Context context,
-            InteractionJankMonitor interactionJankMonitor) {
+            InteractionJankMonitor interactionJankMonitor,
+            @ShellMainThread Handler handler) {
         mTransitions = transitions;
         mTransactionSupplier = supplier;
         mContext = context;
         mInteractionJankMonitor = interactionJankMonitor;
+        mHandler = handler;
     }
 
     /**
@@ -154,7 +162,7 @@
             final SurfaceControl sc = change.getLeash();
             final Rect endBounds = change.getEndAbsBounds();
             mInteractionJankMonitor
-                .begin(sc, mContext, Cuj.CUJ_DESKTOP_MODE_EXIT_MODE);
+                    .begin(sc, mContext, mHandler, Cuj.CUJ_DESKTOP_MODE_EXIT_MODE);
             // Hide the first (fullscreen) frame because the animation will start from the freeform
             // size.
             startT.hide(sc)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index 832e2d2..517e209 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -28,6 +28,7 @@
 import android.app.WindowConfiguration;
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.os.IBinder;
 import android.util.ArrayMap;
 import android.view.SurfaceControl;
@@ -43,6 +44,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -65,6 +67,8 @@
     private final InteractionJankMonitor mInteractionJankMonitor;
     private final ShellExecutor mMainExecutor;
     private final ShellExecutor mAnimExecutor;
+    @ShellMainThread
+    private final Handler mHandler;
 
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
 
@@ -79,7 +83,8 @@
             ShellExecutor mainExecutor,
             ShellExecutor animExecutor,
             DesktopModeTaskRepository desktopModeTaskRepository,
-            InteractionJankMonitor interactionJankMonitor) {
+            InteractionJankMonitor interactionJankMonitor,
+            @ShellMainThread Handler handler) {
         mTransitions = transitions;
         mContext = context;
         mWindowDecorViewModel = windowDecorViewModel;
@@ -88,6 +93,7 @@
         mInteractionJankMonitor = interactionJankMonitor;
         mMainExecutor = mainExecutor;
         mAnimExecutor = animExecutor;
+        mHandler = handler;
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             shellInit.addInitCallback(this::onInit, this);
         }
@@ -267,7 +273,7 @@
                         change.getTaskInfo().displayId) == 1) {
             // Starting the jank trace if closing the last window in desktop mode.
             mInteractionJankMonitor.begin(
-                    sc, mContext, CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE);
+                    sc, mContext, mHandler, CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE);
         }
         animator.addListener(
                 new AnimatorListenerAdapter() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
index 1827923..15472eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -55,6 +55,7 @@
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.shared.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -203,7 +204,7 @@
             DisplayController displayController, DisplayLayout displayLayout,
             TaskStackListenerImpl taskStackListener,
             InteractionJankMonitor jankMonitor, UiEventLogger uiEventLogger,
-            ShellExecutor mainExecutor, Handler mainHandler) {
+            ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) {
         OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil();
         OneHandedAccessibilityUtil accessibilityUtil = new OneHandedAccessibilityUtil(context);
         OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor);
@@ -217,7 +218,7 @@
                 mainExecutor);
         OneHandedDisplayAreaOrganizer organizer = new OneHandedDisplayAreaOrganizer(
                 context, displayLayout, settingsUtil, animationController, tutorialHandler,
-                jankMonitor, mainExecutor);
+                jankMonitor, mainExecutor, mainHandler);
         OneHandedUiEventLogger oneHandedUiEventsLogger = new OneHandedUiEventLogger(uiEventLogger);
         return new OneHandedController(context, shellInit, shellCommandHandler, shellController,
                 displayController, organizer, touchHandler, tutorialHandler, settingsUtil,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
index d157ca8..95e633d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
@@ -23,6 +23,7 @@
 
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.os.SystemProperties;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -42,6 +43,7 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -70,6 +72,8 @@
     private final OneHandedSettingsUtil mOneHandedSettingsUtil;
     private final InteractionJankMonitor mJankMonitor;
     private final Context mContext;
+    @ShellMainThread
+    private final Handler mHandler;
 
     private boolean mIsReady;
     private float mLastVisualOffset = 0;
@@ -136,9 +140,11 @@
             OneHandedAnimationController animationController,
             OneHandedTutorialHandler tutorialHandler,
             InteractionJankMonitor jankMonitor,
-            ShellExecutor mainExecutor) {
+            ShellExecutor mainExecutor,
+            @ShellMainThread Handler handler) {
         super(mainExecutor);
         mContext = context;
+        mHandler = handler;
         setDisplayLayout(displayLayout);
         mOneHandedSettingsUtil = oneHandedSettingsUtil;
         mAnimationController = animationController;
@@ -333,7 +339,7 @@
                 getDisplayAreaTokenMap().entrySet().iterator().next();
         final InteractionJankMonitor.Configuration.Builder builder =
                 InteractionJankMonitor.Configuration.Builder.withSurface(
-                        cujType, mContext, firstEntry.getValue());
+                        cujType, mContext, firstEntry.getValue(), mHandler);
         if (!TextUtils.isEmpty(tag)) {
             builder.setTag(tag);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index deb7691..af68442 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -44,6 +44,7 @@
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.util.Pair;
@@ -93,6 +94,7 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -146,6 +148,8 @@
     private Optional<OneHandedController> mOneHandedController;
     private final ShellCommandHandler mShellCommandHandler;
     private final ShellController mShellController;
+    @ShellMainThread
+    private final Handler mHandler;
     protected final PipImpl mImpl;
 
     private final Rect mTmpInsetBounds = new Rect();
@@ -405,7 +409,8 @@
             DisplayInsetsController displayInsetsController,
             TabletopModeController pipTabletopController,
             Optional<OneHandedController> oneHandedController,
-            ShellExecutor mainExecutor) {
+            ShellExecutor mainExecutor,
+            Handler handler) {
         if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: Device doesn't support Pip feature", TAG);
@@ -418,7 +423,8 @@
                 pipDisplayLayoutState, pipMotionHelper, pipMediaController, phonePipMenuController,
                 pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController,
                 windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
-                displayInsetsController, pipTabletopController, oneHandedController, mainExecutor)
+                displayInsetsController, pipTabletopController, oneHandedController, mainExecutor,
+                handler)
                 .mImpl;
     }
 
@@ -446,11 +452,13 @@
             DisplayInsetsController displayInsetsController,
             TabletopModeController tabletopModeController,
             Optional<OneHandedController> oneHandedController,
-            ShellExecutor mainExecutor
+            ShellExecutor mainExecutor,
+            @ShellMainThread Handler handler
     ) {
         mContext = context;
         mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
+        mHandler = handler;
         mImpl = new PipImpl();
         mWindowManagerShellWrapper = windowManagerShellWrapper;
         mDisplayController = displayController;
@@ -1047,7 +1055,8 @@
         // Begin InteractionJankMonitor with PIP transition CUJs
         final InteractionJankMonitor.Configuration.Builder builder =
                 InteractionJankMonitor.Configuration.Builder.withSurface(
-                        CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl())
+                                CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl(),
+                                mHandler)
                 .setTag(getTransitionTag(direction))
                 .setTimeout(2000);
         InteractionJankMonitor.getInstance().begin(builder);
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/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 4ba84ee..5a905cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -1654,7 +1654,7 @@
             mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext,
                     mRootTaskInfo.configuration, this, mParentContainerCallbacks,
                     mDisplayController, mDisplayImeController, mTaskOrganizer,
-                    PARALLAX_ALIGN_CENTER /* parallaxType */);
+                    PARALLAX_ALIGN_CENTER /* parallaxType */, mMainHandler);
             mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
         }
 
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/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index b14283f..dc27c82 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -111,6 +111,7 @@
 import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
@@ -154,7 +155,7 @@
     private final ShellTaskOrganizer mTaskOrganizer;
     private final ShellController mShellController;
     private final Context mContext;
-    private final Handler mMainHandler;
+    private final @ShellMainThread Handler mMainHandler;
     private final @ShellBackgroundThread ShellExecutor mBgExecutor;
     private final Choreographer mMainChoreographer;
     private final DisplayController mDisplayController;
@@ -214,7 +215,7 @@
     public DesktopModeWindowDecorViewModel(
             Context context,
             ShellExecutor shellExecutor,
-            Handler mainHandler,
+            @ShellMainThread Handler mainHandler,
             Choreographer mainChoreographer,
             @ShellBackgroundThread ShellExecutor bgExecutor,
             ShellInit shellInit,
@@ -270,7 +271,7 @@
     DesktopModeWindowDecorViewModel(
             Context context,
             ShellExecutor shellExecutor,
-            Handler mainHandler,
+            @ShellMainThread Handler mainHandler,
             Choreographer mainChoreographer,
             @ShellBackgroundThread ShellExecutor bgExecutor,
             ShellInit shellInit,
@@ -495,7 +496,8 @@
             return;
         }
         mInteractionJankMonitor.begin(
-                decoration.mTaskSurface, mContext, Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, source);
+                decoration.mTaskSurface, mContext, mMainHandler,
+                Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, source);
         mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
         decoration.closeHandleMenu();
         decoration.closeMaximizeMenu();
@@ -512,7 +514,7 @@
             Toast.makeText(mContext,
                     R.string.desktop_mode_non_resizable_snap_text, Toast.LENGTH_SHORT).show();
         } else {
-            mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext,
+            mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, mMainHandler,
                     Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE, "maximize_menu_resizable");
             mDesktopTasksController.snapToHalfScreen(
                     decoration.mTaskInfo,
@@ -548,7 +550,7 @@
             return;
         }
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext,
+        mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, mMainHandler,
                 CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU);
         // App sometimes draws before the insets from WindowDecoration#relayout have
         // been added, so they must be added here
@@ -1378,7 +1380,8 @@
                 mDragStartListener,
                 mTransitions,
                 mInteractionJankMonitor,
-                mTransactionFactory);
+                mTransactionFactory,
+                mMainHandler);
         windowDecoration.setTaskDragResizer(taskPositioner);
 
         final DesktopModeTouchEventListener touchEventListener =
@@ -1602,7 +1605,8 @@
                 DragPositioningCallbackUtility.DragStartListener dragStartListener,
                 Transitions transitions,
                 InteractionJankMonitor interactionJankMonitor,
-                Supplier<SurfaceControl.Transaction> transactionFactory) {
+                Supplier<SurfaceControl.Transaction> transactionFactory,
+                Handler handler) {
             final TaskPositioner taskPositioner = DesktopModeStatus.isVeiledResizeEnabled()
                     ? new VeiledResizeTaskPositioner(
                             taskOrganizer,
@@ -1610,7 +1614,8 @@
                             displayController,
                             dragStartListener,
                             transitions,
-                            interactionJankMonitor)
+                            interactionJankMonitor,
+                            handler)
                     : new FluidResizeTaskPositioner(
                             taskOrganizer,
                             transitions,
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/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 5998155..6f3f411 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -24,6 +24,7 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.os.IBinder;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -37,6 +38,7 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.transition.Transitions;
 
 import java.util.function.Supplier;
@@ -63,14 +65,17 @@
     private int mCtrlType;
     private boolean mIsResizingOrAnimatingResize;
     @Surface.Rotation private int mRotation;
+    @ShellMainThread
+    private final Handler mHandler;
 
     public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
             DesktopModeWindowDecoration windowDecoration,
             DisplayController displayController,
             DragPositioningCallbackUtility.DragStartListener dragStartListener,
-            Transitions transitions, InteractionJankMonitor interactionJankMonitor) {
+            Transitions transitions, InteractionJankMonitor interactionJankMonitor,
+            @ShellMainThread Handler handler) {
         this(taskOrganizer, windowDecoration, displayController, dragStartListener,
-                SurfaceControl.Transaction::new, transitions, interactionJankMonitor);
+                SurfaceControl.Transaction::new, transitions, interactionJankMonitor, handler);
     }
 
     public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
@@ -78,7 +83,7 @@
             DisplayController displayController,
             DragPositioningCallbackUtility.DragStartListener dragStartListener,
             Supplier<SurfaceControl.Transaction> supplier, Transitions transitions,
-            InteractionJankMonitor interactionJankMonitor) {
+            InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler) {
         mDesktopWindowDecoration = windowDecoration;
         mTaskOrganizer = taskOrganizer;
         mDisplayController = displayController;
@@ -86,6 +91,7 @@
         mTransactionSupplier = supplier;
         mTransitions = transitions;
         mInteractionJankMonitor = interactionJankMonitor;
+        mHandler = handler;
     }
 
     @Override
@@ -97,7 +103,7 @@
         if (isResizing()) {
             // Capture CUJ for re-sizing window in DW mode.
             mInteractionJankMonitor.begin(mDesktopWindowDecoration.mTaskSurface,
-                    mDesktopWindowDecoration.mContext, CUJ_DESKTOP_MODE_RESIZE_WINDOW);
+                    mDesktopWindowDecoration.mContext, mHandler, CUJ_DESKTOP_MODE_RESIZE_WINDOW);
             if (!mDesktopWindowDecoration.mTaskInfo.isFocused) {
                 WindowContainerTransaction wct = new WindowContainerTransaction();
                 wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true);
@@ -131,7 +137,7 @@
         } else if (mCtrlType == CTRL_TYPE_UNDEFINED) {
             // Begin window drag CUJ instrumentation only when drag position moves.
             mInteractionJankMonitor.begin(mDesktopWindowDecoration.mTaskSurface,
-                    mDesktopWindowDecoration.mContext, CUJ_DESKTOP_MODE_DRAG_WINDOW);
+                    mDesktopWindowDecoration.mContext, mHandler, CUJ_DESKTOP_MODE_DRAG_WINDOW);
             final SurfaceControl.Transaction t = mTransactionSupplier.get();
             DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration,
                     mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, t, x, y);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index b53ea38..227060d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -137,6 +137,8 @@
     private Transitions mTransitions;
     @Mock
     private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+    @Mock
+    private Handler mHandler;
 
     private BackAnimationController mController;
     private TestableContentResolver mContentResolver;
@@ -161,13 +163,14 @@
         mTestableLooper = TestableLooper.get(this);
         mShellInit = spy(new ShellInit(mShellExecutor));
         mDefaultCrossActivityBackAnimation = new DefaultCrossActivityBackAnimation(mContext,
-                mAnimationBackground, mRootTaskDisplayAreaOrganizer);
-        mCrossTaskBackAnimation = new CrossTaskBackAnimation(mContext, mAnimationBackground);
+                mAnimationBackground, mRootTaskDisplayAreaOrganizer, mHandler);
+        mCrossTaskBackAnimation = new CrossTaskBackAnimation(mContext, mAnimationBackground,
+                mHandler);
         mShellBackAnimationRegistry =
                 new ShellBackAnimationRegistry(mDefaultCrossActivityBackAnimation,
                         mCrossTaskBackAnimation, /* dialogCloseAnimation= */ null,
                         new CustomCrossActivityBackAnimation(mContext, mAnimationBackground,
-                                mRootTaskDisplayAreaOrganizer),
+                                mRootTaskDisplayAreaOrganizer, mHandler),
                         /* defaultBackToHomeAnimation= */ null);
         mController =
                 new BackAnimationController(
@@ -181,7 +184,8 @@
                         mAnimationBackground,
                         mShellBackAnimationRegistry,
                         mShellCommandHandler,
-                        mTransitions);
+                        mTransitions,
+                        mHandler);
         mShellInit.init();
         mShellExecutor.flushAll();
         mTouchableRegion = new Rect(0, 0, 100, 100);
@@ -344,7 +348,8 @@
                         mAnimationBackground,
                         mShellBackAnimationRegistry,
                         mShellCommandHandler,
-                        mTransitions);
+                        mTransitions,
+                        mHandler);
         shellInit.init();
         registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
@@ -898,7 +903,8 @@
                 new BackAnimationRunner(
                         mAnimatorCallback,
                         mBackAnimationRunner,
-                        mContext));
+                        mContext,
+                        mHandler));
     }
 
     private void unregisterAnimation(int type) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt
index 080ad90..5b5ef6f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt
@@ -22,6 +22,7 @@
 import android.graphics.Color
 import android.graphics.Point
 import android.graphics.Rect
+import android.os.Handler
 import android.os.RemoteException
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
@@ -66,6 +67,7 @@
     @Mock private lateinit var transitionAnimation: TransitionAnimation
     @Mock private lateinit var appCompatTaskInfo: AppCompatTaskInfo
     @Mock private lateinit var transaction: Transaction
+    @Mock private lateinit var handler: Handler
 
     private lateinit var customCrossActivityBackAnimation: CustomCrossActivityBackAnimation
     private lateinit var customAnimationLoader: CustomAnimationLoader
@@ -80,7 +82,8 @@
                 backAnimationBackground,
                 rootTaskDisplayAreaOrganizer,
                 transaction,
-                customAnimationLoader
+                customAnimationLoader,
+                handler,
             )
 
         whenever(transitionAnimation.loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(OPEN_RES_ID)))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
index f5847cc..cf69704 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
@@ -25,6 +25,7 @@
 
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.os.SystemClock;
 import android.provider.DeviceConfig;
 import android.view.InputDevice;
@@ -55,6 +56,7 @@
     private @Mock DisplayController mDisplayController;
     private @Mock DisplayImeController mDisplayImeController;
     private @Mock ShellTaskOrganizer mTaskOrganizer;
+    private @Mock Handler mHandler;
     private SplitLayout mSplitLayout;
     private DividerView mDividerView;
 
@@ -65,7 +67,7 @@
         Configuration configuration = getConfiguration();
         mSplitLayout = new SplitLayout("TestSplitLayout", mContext, configuration,
                 mSplitLayoutHandler, mCallbacks, mDisplayController, mDisplayImeController,
-                mTaskOrganizer, SplitLayout.PARALLAX_NONE);
+                mTaskOrganizer, SplitLayout.PARALLAX_NONE, mHandler);
         SplitWindowManager splitWindowManager = new SplitWindowManager("TestSplitWindowManager",
                 mContext,
                 configuration, mCallbacks);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 82b3a7d..177e47a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -35,6 +35,7 @@
 import android.app.ActivityManager;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.window.WindowContainerTransaction;
 
 import androidx.test.annotation.UiThreadTest;
@@ -65,6 +66,7 @@
     @Mock DisplayImeController mDisplayImeController;
     @Mock ShellTaskOrganizer mTaskOrganizer;
     @Mock WindowContainerTransaction mWct;
+    @Mock Handler mHandler;
     @Captor ArgumentCaptor<Runnable> mRunnableCaptor;
     private SplitLayout mSplitLayout;
 
@@ -80,7 +82,8 @@
                 mDisplayController,
                 mDisplayImeController,
                 mTaskOrganizer,
-                SplitLayout.PARALLAX_NONE));
+                SplitLayout.PARALLAX_NONE,
+                mHandler));
     }
 
     @Test
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 b47201e..e610ebd 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
@@ -40,6 +40,7 @@
 import android.graphics.PointF
 import android.graphics.Rect
 import android.os.Binder
+import android.os.Handler
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
@@ -181,6 +182,7 @@
   private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
   @Mock private lateinit var mockSurface: SurfaceControl
   @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
+  @Mock private lateinit var mockHandler: Handler
 
   private lateinit var mockitoSession: StaticMockitoSession
   private lateinit var controller: DesktopTasksController
@@ -221,7 +223,8 @@
             shellTaskOrganizer,
             MAX_TASK_LIMIT,
             mockInteractionJankMonitor,
-            mContext)
+            mContext,
+            mockHandler)
 
     whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
     whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
@@ -274,7 +277,9 @@
         shellExecutor,
         Optional.of(desktopTasksLimiter),
         recentTasksController,
-        mockInteractionJankMonitor)
+        mockInteractionJankMonitor,
+        mockHandler,
+      )
   }
 
   @After
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 2d0e428..61d03ca 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -18,6 +18,7 @@
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.os.Binder
+import android.os.Handler
 import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
 import android.view.Display.DEFAULT_DISPLAY
@@ -70,6 +71,7 @@
     @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
     @Mock lateinit var transitions: Transitions
     @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
+    @Mock lateinit var handler: Handler
 
     private lateinit var mockitoSession: StaticMockitoSession
     private lateinit var desktopTasksLimiter: DesktopTasksLimiter
@@ -85,7 +87,7 @@
 
         desktopTasksLimiter =
             DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT,
-                interactionJankMonitor, mContext)
+                interactionJankMonitor, mContext, handler)
     }
 
     @After
@@ -97,7 +99,7 @@
     fun createDesktopTasksLimiter_withZeroLimit_shouldThrow() {
         assertFailsWith<IllegalArgumentException> {
             DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, 0,
-                interactionJankMonitor, mContext)
+                interactionJankMonitor, mContext, handler)
         }
     }
 
@@ -105,7 +107,7 @@
     fun createDesktopTasksLimiter_withNegativeLimit_shouldThrow() {
         assertFailsWith<IllegalArgumentException> {
             DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, -5,
-                interactionJankMonitor, mContext)
+                interactionJankMonitor, mContext, handler)
         }
     }
 
@@ -334,7 +336,7 @@
     fun getTaskToMinimizeIfNeeded_tasksAboveLimit_otherLimit_returnsBackTask() {
         desktopTasksLimiter =
             DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT2,
-                interactionJankMonitor, mContext)
+                interactionJankMonitor, mContext, handler)
         val tasks = (1..MAX_TASK_LIMIT2 + 1).map { setUpFreeformTask() }
 
         val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded(
@@ -375,6 +377,7 @@
         verify(interactionJankMonitor).begin(
             any(),
             eq(mContext),
+            eq(handler),
             eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
 
         desktopTasksLimiter.getTransitionObserver().onTransitionFinished(
@@ -403,7 +406,9 @@
         verify(interactionJankMonitor).begin(
             any(),
             eq(mContext),
-            eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
+            eq(handler),
+            eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW),
+        )
 
         desktopTasksLimiter.getTransitionObserver().onTransitionFinished(
             transition,
@@ -432,6 +437,7 @@
         verify(interactionJankMonitor).begin(
             any(),
             eq(mContext),
+            eq(handler),
             eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
 
         desktopTasksLimiter.getTransitionObserver().onTransitionMerged(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
index e0463b4..fefa933 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
@@ -34,6 +34,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Point;
+import android.os.Handler;
 import android.os.IBinder;
 import android.util.DisplayMetrics;
 import android.view.SurfaceControl;
@@ -81,6 +82,8 @@
     Transitions.TransitionFinishCallback mTransitionFinishCallback;
     @Mock
     ShellExecutor mExecutor;
+    @Mock
+    Handler mHandler;
 
     private Point mPoint;
     private ExitDesktopTaskTransitionHandler mExitDesktopTaskTransitionHandler;
@@ -97,7 +100,7 @@
                 .thenReturn(getContext().getResources().getDisplayMetrics());
 
         mExitDesktopTaskTransitionHandler = new ExitDesktopTaskTransitionHandler(mTransitions,
-                mContext, mInteractionJankMonitor);
+                mContext, mInteractionJankMonitor, mHandler);
         mPoint = new Point(0, 0);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java
index 9c7f723..9146906 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java
@@ -37,6 +37,7 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.Binder;
+import android.os.Handler;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.Display;
@@ -100,6 +101,8 @@
     OneHandedSettingsUtil mMockSettingsUitl;
     @Mock
     InteractionJankMonitor mJankMonitor;
+    @Mock
+    Handler mMockHandler;
 
     List<DisplayAreaAppearedInfo> mDisplayAreaAppearedInfoList = new ArrayList<>();
 
@@ -142,7 +145,8 @@
                 mMockAnimationController,
                 mTutorialHandler,
                 mJankMonitor,
-                mMockShellMainExecutor));
+                mMockShellMainExecutor,
+                mMockHandler));
 
         for (int i = 0; i < DISPLAYAREA_INFO_COUNT; i++) {
             mDisplayAreaAppearedInfoList.add(getDummyDisplayAreaInfo());
@@ -429,7 +433,8 @@
                         mMockAnimationController,
                         mTutorialHandler,
                         mJankMonitor,
-                        mMockShellMainExecutor));
+                        mMockShellMainExecutor,
+                        mMockHandler));
 
         assertThat(testSpiedDisplayAreaOrganizer.isReady()).isFalse();
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index f3944d5..9600351 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -40,6 +40,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -115,6 +116,7 @@
     @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder;
     @Mock private DisplayInsetsController mMockDisplayInsetsController;
     @Mock private TabletopModeController mMockTabletopModeController;
+    @Mock private Handler mMockHandler;
 
     @Mock private DisplayLayout mMockDisplayLayout1;
     @Mock private DisplayLayout mMockDisplayLayout2;
@@ -138,7 +140,7 @@
                 mMockPipTransitionController, mMockWindowManagerShellWrapper,
                 mMockTaskStackListener, mMockPipParamsChangedForwarder,
                 mMockDisplayInsetsController, mMockTabletopModeController,
-                mMockOneHandedController, mMockExecutor);
+                mMockOneHandedController, mMockExecutor, mMockHandler);
         mShellInit.init();
         when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm);
         when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper);
@@ -230,7 +232,7 @@
                 mMockPipTransitionController, mMockWindowManagerShellWrapper,
                 mMockTaskStackListener, mMockPipParamsChangedForwarder,
                 mMockDisplayInsetsController, mMockTabletopModeController,
-                mMockOneHandedController, mMockExecutor));
+                mMockOneHandedController, mMockExecutor, mMockHandler));
     }
 
     @Test
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/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index a18fbf0..85bc7cc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -247,7 +247,18 @@
         whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
         whenever(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS)
         whenever(mockInputMonitorFactory.create(any(), any())).thenReturn(mockInputMonitor)
-        whenever(mockTaskPositionerFactory.create(any(), any(), any(), any(), any(), any(), any()))
+        whenever(
+            mockTaskPositionerFactory.create(
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any()
+            )
+        )
             .thenReturn(mockTaskPositioner)
 
         doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 7784af6..ab41d9c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -21,6 +21,7 @@
 import android.content.res.Resources
 import android.graphics.Point
 import android.graphics.Rect
+import android.os.Handler
 import android.os.IBinder
 import android.testing.AndroidTestingRunner
 import android.view.Display
@@ -107,6 +108,8 @@
     private lateinit var mockResources: Resources
     @Mock
     private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
+    @Mock
+    private lateinit var mockHandler: Handler
 
     private lateinit var taskPositioner: VeiledResizeTaskPositioner
 
@@ -155,7 +158,8 @@
                         mockDragStartListener,
                         mockTransactionFactory,
                         mockTransitions,
-                        mockInteractionJankMonitor
+                        mockInteractionJankMonitor,
+                        mockHandler,
                 )
     }
 
diff --git a/libs/hwui/platform/host/thread/ThreadBase.h b/libs/hwui/platform/host/thread/ThreadBase.h
index d709430..b4e7bd3 100644
--- a/libs/hwui/platform/host/thread/ThreadBase.h
+++ b/libs/hwui/platform/host/thread/ThreadBase.h
@@ -48,7 +48,7 @@
         nsecs_t nextWakeup = mQueue.nextWakeup(lock);
         std::chrono::nanoseconds duration = std::chrono::nanoseconds::max();
         if (nextWakeup < std::numeric_limits<nsecs_t>::max()) {
-            int timeout = nextWakeup - WorkQueue::clock::now();
+            nsecs_t timeout = nextWakeup - WorkQueue::clock::now();
             if (timeout < 0) timeout = 0;
             duration = std::chrono::nanoseconds(timeout);
         }
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 3cc0ad2..31f8996 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -90,24 +90,24 @@
         mDisplayManager = displayManager;
     }
 
-  /**
-   * Register a listener to receive notifications about when the {@link MediaProjection} or captured
-   * content changes state.
-   *
-   * <p>The callback must be registered before invoking {@link #createVirtualDisplay(String, int,
-   * int, int, int, Surface, VirtualDisplay.Callback, Handler)} to ensure that any notifications on
-   * the callback are not missed. The client must implement {@link Callback#onStop()} and clean up
-   * any resources it is holding, e.g. the {@link VirtualDisplay} and {@link Surface}. This should
-   * also update any application UI indicating the MediaProjection status as MediaProjection has
-   * stopped.
-   *
-   * @param callback The callback to call.
-   * @param handler The handler on which the callback should be invoked, or null if the callback
-   *     should be invoked on the calling thread's looper.
-   * @throws NullPointerException If the given callback is null.
-   * @see #unregisterCallback
-   */
-  public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
+    /**
+     * Register a listener to receive notifications about when the {@link MediaProjection} or
+     * captured content changes state.
+     *
+     * <p>The callback must be registered before invoking {@link #createVirtualDisplay(String, int,
+     * int, int, int, Surface, VirtualDisplay.Callback, Handler)} to ensure that any notifications
+     * on the callback are not missed. The client must implement {@link Callback#onStop()} to
+     * properly handle MediaProjection clean up any resources it is holding, e.g. the {@link
+     * VirtualDisplay} and {@link Surface}. This should also update any application UI indicating
+     * the MediaProjection status as MediaProjection has stopped.
+     *
+     * @param callback The callback to call.
+     * @param handler The handler on which the callback should be invoked, or null if the callback
+     *     should be invoked on the calling thread's looper.
+     * @throws NullPointerException If the given callback is null.
+     * @see #unregisterCallback
+     */
+    public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
         try {
             final Callback c = Objects.requireNonNull(callback);
             if (handler == null) {
@@ -313,7 +313,7 @@
      */
     public abstract static class Callback {
         /**
-         * Called when the MediaProjection session is no longer valid.
+         * Called when the MediaProjection session has been stopped and is no longer valid.
          *
          * <p>Once a MediaProjection has been stopped, it's up to the application to release any
          * resources it may be holding (e.g. releasing the {@link VirtualDisplay} and {@link
@@ -321,9 +321,9 @@
          * it should be updated to indicate that MediaProjection is no longer active.
          *
          * <p>MediaProjection stopping can be a result of the system stopping the ongoing
-         * MediaProjection due to various reasons, such as another MediaProjection session starting.
-         * MediaProjection may also stop due to the user explicitly stopping ongoing MediaProjection
-         * via any available system-level UI.
+         * MediaProjection due to various reasons, such as another MediaProjection session starting,
+         * a user stopping the session via UI affordances in system-level UI, or the screen being
+         * locked.
          *
          * <p>After this callback any call to {@link MediaProjection#createVirtualDisplay} will
          * fail, even if no such {@link VirtualDisplay} was ever created for this MediaProjection
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 03fd2c6..dc55e41 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -60,13 +60,14 @@
  *   <li>Register a {@link MediaProjection.Callback} by calling {@link
  *       MediaProjection#registerCallback(MediaProjection.Callback, Handler)}. This is required to
  *       receive notifications about when the {@link MediaProjection} or captured content changes
- *       state. When receiving an `onStop()` callback, the client must clean up any resources it is
- *       holding, e.g. the {@link VirtualDisplay} and {@link Surface}. The MediaProjection may
- *       further no longer create any new {@link VirtualDisplay}s via {@link
- *       MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
- *       VirtualDisplay.Callback, Handler)}. Note that the `onStop()` callback can be a result of
- *       the system stopping MediaProjection due to various reasons or the user stopping the
- *       MediaProjection via UI affordances in system-level UI.
+ *       state. When receiving an `onStop()` callback the {@link MediaProjection} session has been
+ *       finished and the client must clean up any resources it is holding, e.g. the {@link
+ *       VirtualDisplay} and {@link Surface}. The MediaProjection may further no longer create any
+ *       new {@link VirtualDisplay}s via {@link MediaProjection#createVirtualDisplay(String, int,
+ *       int, int, int, Surface, VirtualDisplay.Callback, Handler)}. Note that the `onStop()`
+ *       callback can be a result of the system stopping MediaProjection due to various reasons.
+ *       This includes the user stopping the MediaProjection via UI affordances in system-level UI,
+ *       the screen being locked, or another {@link MediaProjection} session starting.
  *   <li>Start the screen capture session for media projection by calling {@link
  *       MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
  *       android.hardware.display.VirtualDisplay.Callback, Handler)}.
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/bluetooth/LocalBluetoothLeBroadcastAssistant.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
index 148e164..c9f9d1b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
@@ -151,7 +151,19 @@
             Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
             return;
         }
-        mService.addSource(sink, metadata, isGroupOp);
+        try {
+            mService.addSource(sink, metadata, isGroupOp);
+        } catch (IllegalStateException e) {
+            // BT will check callback registration before add source.
+            // If it throw callback exception when bt is disabled, then the failure is intended,
+            // just catch it here.
+            BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+            if (adapter != null && adapter.isEnabled()) {
+                throw e;
+            } else {
+                Log.d(TAG, "Catch addSource failure when bt is disabled: " + e);
+            }
+        }
     }
 
     /**
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/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
index afbe84c..fbce6ca 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
@@ -261,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/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 207c961..98c491b 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -538,6 +538,13 @@
 }
 
 flag {
+  name: "status_bar_connected_displays"
+  namespace: "systemui"
+  description: "Shows the status bar on connected displays"
+  bug: "362720336"
+}
+
+flag {
     name: "status_bar_switch_to_spn_from_data_spn"
     namespace: "systemui"
     description: "Fix usage of the SPN broadcast extras"
@@ -548,13 +555,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)."
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 163b355..8321238 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.bouncer.ui.composable
 
-import android.view.HapticFeedbackConstants
 import androidx.annotation.VisibleForTesting
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationVector1D
@@ -133,10 +132,7 @@
         // Perform haptic feedback, but only if the current dot is not null, so we don't perform it
         // when the UI first shows up or when the user lifts their pointer/finger.
         if (currentDot != null) {
-            view.performHapticFeedback(
-                HapticFeedbackConstants.VIRTUAL_KEY,
-                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
-            )
+            viewModel.performDotFeedback(view)
         }
 
         if (!isAnimationEnabled) {
@@ -206,10 +202,7 @@
     // Show the failure animation if the user entered the wrong input.
     LaunchedEffect(animateFailure) {
         if (animateFailure) {
-            showFailureAnimation(
-                dots = dots,
-                scalingAnimatables = dotScalingAnimatables,
-            )
+            showFailureAnimation(dots = dots, scalingAnimatables = dotScalingAnimatables)
             viewModel.onFailureAnimationShown()
         }
     }
@@ -358,15 +351,10 @@
                     (1 - checkNotNull(dotAppearMoveUpAnimatables[dot]).value) * initialOffset
                 drawCircle(
                     center =
-                        pixelOffset(
-                            dot,
-                            spacing,
-                            horizontalOffset,
-                            verticalOffset + appearOffset,
-                        ),
+                        pixelOffset(dot, spacing, horizontalOffset, verticalOffset + appearOffset),
                     color =
                         dotColor.copy(alpha = checkNotNull(dotAppearFadeInAnimatables[dot]).value),
-                    radius = dotRadius * checkNotNull(dotScalingAnimatables[dot]).value
+                    radius = dotRadius * checkNotNull(dotScalingAnimatables[dot]).value,
                 )
             }
         }
@@ -387,7 +375,7 @@
                             delayMillis = 33 * dot.y,
                             durationMillis = 450,
                             easing = Easings.LegacyDecelerate,
-                        )
+                        ),
                 )
             }
         }
@@ -400,7 +388,7 @@
                             delayMillis = 0,
                             durationMillis = 450 + (33 * dot.y),
                             easing = Easings.StandardDecelerate,
-                        )
+                        ),
                 )
             }
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 489e24e..0830c9b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.bouncer.ui.composable
 
-import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
+import android.view.View
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
@@ -72,11 +72,7 @@
 
 /** Renders the PIN button pad. */
 @Composable
-fun PinPad(
-    viewModel: PinBouncerViewModel,
-    verticalSpacing: Dp,
-    modifier: Modifier = Modifier,
-) {
+fun PinPad(viewModel: PinBouncerViewModel, verticalSpacing: Dp, modifier: Modifier = Modifier) {
     DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
 
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsStateWithLifecycle()
@@ -104,7 +100,7 @@
         columns = columns,
         verticalSpacing = verticalSpacing,
         horizontalSpacing = calculateHorizontalSpacingBetweenColumns(gridWidth = 300.dp),
-        modifier = modifier.focusRequester(focusRequester).sysuiResTag("pin_pad_grid")
+        modifier = modifier.focusRequester(focusRequester).sysuiResTag("pin_pad_grid"),
     ) {
         repeat(9) { index ->
             DigitButton(
@@ -126,10 +122,11 @@
                 ),
             isInputEnabled = isInputEnabled,
             onClicked = viewModel::onBackspaceButtonClicked,
+            onPointerDown = viewModel::onBackspaceButtonPressed,
             onLongPressed = viewModel::onBackspaceButtonLongPressed,
             appearance = backspaceButtonAppearance,
             scaling = buttonScaleAnimatables[9]::value,
-            elementId = "delete_button"
+            elementId = "delete_button",
         )
 
         DigitButton(
@@ -138,7 +135,7 @@
             onClicked = viewModel::onPinButtonClicked,
             scaling = buttonScaleAnimatables[10]::value,
             isAnimationEnabled = isDigitButtonAnimationEnabled,
-            onPointerDown = viewModel::onDigitButtonDown
+            onPointerDown = viewModel::onDigitButtonDown,
         )
 
         ActionButton(
@@ -152,7 +149,7 @@
             onClicked = viewModel::onAuthenticateButtonClicked,
             appearance = confirmButtonAppearance,
             scaling = buttonScaleAnimatables[11]::value,
-            elementId = "key_enter"
+            elementId = "key_enter",
         )
     }
 }
@@ -162,7 +159,7 @@
     digit: Int,
     isInputEnabled: Boolean,
     onClicked: (Int) -> Unit,
-    onPointerDown: () -> Unit,
+    onPointerDown: (View?) -> Unit,
     scaling: () -> Float,
     isAnimationEnabled: Boolean,
 ) {
@@ -178,7 +175,7 @@
                 val scale = if (isAnimationEnabled) scaling() else 1f
                 scaleX = scale
                 scaleY = scale
-            }
+            },
     ) { contentColor ->
         // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes
         // it into Text, use that here, to animate more efficiently.
@@ -197,6 +194,7 @@
     onClicked: () -> Unit,
     elementId: String,
     onLongPressed: (() -> Unit)? = null,
+    onPointerDown: ((View?) -> Unit)? = null,
     appearance: ActionButtonAppearance,
     scaling: () -> Float,
 ) {
@@ -222,18 +220,16 @@
         foregroundColor = foregroundColor,
         isAnimationEnabled = true,
         elementId = elementId,
+        onPointerDown = onPointerDown,
         modifier =
             Modifier.graphicsLayer {
                 alpha = hiddenAlpha
                 val scale = scaling()
                 scaleX = scale
                 scaleY = scale
-            }
+            },
     ) { contentColor ->
-        Icon(
-            icon = icon,
-            tint = contentColor(),
-        )
+        Icon(icon = icon, tint = contentColor())
     }
 }
 
@@ -247,22 +243,13 @@
     modifier: Modifier = Modifier,
     elementId: String? = null,
     onLongPressed: (() -> Unit)? = null,
-    onPointerDown: (() -> Unit)? = null,
+    onPointerDown: ((View?) -> Unit)? = null,
     content: @Composable (contentColor: () -> Color) -> Unit,
 ) {
     val interactionSource = remember { MutableInteractionSource() }
     val isPressed by interactionSource.collectIsPressedAsState()
     val indication = LocalIndication.current.takeUnless { isPressed }
-
     val view = LocalView.current
-    LaunchedEffect(isPressed) {
-        if (isPressed) {
-            view.performHapticFeedback(
-                HapticFeedbackConstants.VIRTUAL_KEY,
-                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
-            )
-        }
-    }
 
     // Pin button animation specification is asymmetric: fast animation to the pressed state, and a
     // slow animation upon release. Note that isPressed is guaranteed to be true for at least the
@@ -277,7 +264,7 @@
         animateDpAsState(
             if (isAnimationEnabled && isPressed) 24.dp else pinButtonMaxSize / 2,
             label = "PinButton round corners",
-            animationSpec = tween(animDurationMillis, easing = animEasing)
+            animationSpec = tween(animDurationMillis, easing = animEasing),
         )
     val colorAnimationSpec: AnimationSpec<Color> = tween(animDurationMillis, easing = animEasing)
     val containerColor: Color by
@@ -287,7 +274,7 @@
                 else -> backgroundColor
             },
             label = "Pin button container color",
-            animationSpec = colorAnimationSpec
+            animationSpec = colorAnimationSpec,
         )
     val contentColor =
         animateColorAsState(
@@ -296,7 +283,7 @@
                 else -> foregroundColor
             },
             label = "Pin button container color",
-            animationSpec = colorAnimationSpec
+            animationSpec = colorAnimationSpec,
         )
 
     Box(
@@ -319,11 +306,11 @@
                             interactionSource = interactionSource,
                             indication = indication,
                             onClick = onClicked,
-                            onLongClick = onLongPressed
+                            onLongClick = onLongPressed,
                         )
                         .pointerInteropFilter { motionEvent ->
                             if (motionEvent.action == MotionEvent.ACTION_DOWN) {
-                                onPointerDown?.let { it() }
+                                onPointerDown?.let { it(view) }
                             }
                             false
                         }
@@ -353,10 +340,7 @@
                 animatable.animateTo(
                     targetValue = 1f,
                     animationSpec =
-                        tween(
-                            durationMillis = pinButtonErrorRevertMs,
-                            easing = Easings.Legacy,
-                        ),
+                        tween(durationMillis = pinButtonErrorRevertMs, easing = Easings.Legacy),
                 )
             }
         }
@@ -364,9 +348,7 @@
 }
 
 /** Returns the amount of horizontal spacing between columns, in dips. */
-private fun calculateHorizontalSpacingBetweenColumns(
-    gridWidth: Dp,
-): Dp {
+private fun calculateHorizontalSpacingBetweenColumns(gridWidth: Dp): Dp {
     return (gridWidth - (pinButtonMaxSize * columns)) / (columns - 1)
 }
 
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/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index deef652..9552564 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -16,18 +16,26 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.keyguard.AuthInteractionProperties
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.testKosmos
+import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -39,11 +47,15 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
+    private val msdlPlayer = kosmos.fakeMSDLPlayer
+    private val bouncerHapticPlayer = kosmos.bouncerHapticPlayer
+    private val authInteractionProperties = AuthInteractionProperties()
     private val underTest =
         kosmos.pinBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true),
             onIntentionalUserInput = {},
             authenticationMethod = AuthenticationMethodModel.Pin,
+            bouncerHapticPlayer = bouncerHapticPlayer,
         )
 
     @Before
@@ -77,4 +89,42 @@
             underTest.onAuthenticateButtonClicked()
             assertThat(animateFailure).isFalse()
         }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onAuthenticationResult_playUnlockTokenIfSuccessful() =
+        testScope.runTest {
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+            // Correct PIN:
+            FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
+                underTest.onPinButtonClicked(digit)
+            }
+            underTest.onAuthenticateButtonClicked()
+            runCurrent()
+
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onAuthenticationResult_playFailureTokenIfFailure() =
+        testScope.runTest {
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+            // Wrong PIN:
+            FakeAuthenticationRepository.DEFAULT_PIN.drop(2).forEach { digit ->
+                underTest.onPinButtonClicked(digit)
+            }
+            underTest.onAuthenticateButtonClicked()
+            runCurrent()
+
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 7c773a9..c163c6f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -16,9 +16,11 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.authenticationRepository
@@ -27,12 +29,16 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.haptics.msdl.FakeMSDLPlayer
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
+import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -55,10 +61,13 @@
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val bouncerViewModel by lazy { kosmos.bouncerSceneContentViewModel }
+    private val msdlPlayer: FakeMSDLPlayer = kosmos.fakeMSDLPlayer
+    private val bouncerHapticHelper = kosmos.bouncerHapticPlayer
     private val underTest =
         kosmos.patternBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
             onIntentionalUserInput = {},
+            bouncerHapticPlayer = bouncerHapticHelper,
         )
 
     private val containerSize = 90 // px
@@ -115,10 +124,7 @@
                     .that(selectedDots)
                     .isEqualTo(
                         CORRECT_PATTERN.subList(0, index + 1).map {
-                            PatternDotViewModel(
-                                x = it.x,
-                                y = it.y,
-                            )
+                            PatternDotViewModel(x = it.x, y = it.y)
                         }
                     )
                 assertWithMessage("Wrong current dot for index $index")
@@ -174,7 +180,7 @@
                     listOf(
                         PatternDotViewModel(0, 0),
                         PatternDotViewModel(1, 0),
-                        PatternDotViewModel(2, 0)
+                        PatternDotViewModel(2, 0),
                     )
                 )
         }
@@ -200,7 +206,7 @@
                     listOf(
                         PatternDotViewModel(1, 0),
                         PatternDotViewModel(1, 1),
-                        PatternDotViewModel(1, 2)
+                        PatternDotViewModel(1, 2),
                     )
                 )
         }
@@ -228,7 +234,7 @@
                     listOf(
                         PatternDotViewModel(2, 0),
                         PatternDotViewModel(1, 1),
-                        PatternDotViewModel(0, 2)
+                        PatternDotViewModel(0, 2),
                     )
                 )
         }
@@ -300,10 +306,7 @@
             val attempts = FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT + 1
             repeat(attempts) { attempt ->
                 underTest.onDragStart()
-                CORRECT_PATTERN.subList(
-                        0,
-                        kosmos.authenticationRepository.minPatternLength - 1,
-                    )
+                CORRECT_PATTERN.subList(0, kosmos.authenticationRepository.minPatternLength - 1)
                     .forEach { coordinate ->
                         underTest.onDrag(
                             xPx = 30f * coordinate.x + 15,
@@ -341,6 +344,16 @@
             assertThat(authResult).isTrue()
         }
 
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun performDotFeedback_deliversDragToken() =
+        testScope.runTest {
+            underTest.performDotFeedback(null)
+
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+        }
+
     private fun dragOverCoordinates(vararg coordinatesDragged: Point) {
         underTest.onDragStart()
         coordinatesDragged.forEach(::dragToCoordinate)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 2ee4aee..af5f2ac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -27,6 +27,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
@@ -35,12 +36,15 @@
 import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository
 import com.android.systemui.classifier.fakeFalsingCollector
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
+import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
 import kotlin.random.Random
 import kotlin.random.nextInt
@@ -64,11 +68,14 @@
     private val testScope = kosmos.testScope
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+    private val msdlPlayer = kosmos.fakeMSDLPlayer
+    private val bouncerHapticPlayer = kosmos.bouncerHapticPlayer
     private val underTest by lazy {
         kosmos.pinBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true),
             onIntentionalUserInput = {},
             authenticationMethod = AuthenticationMethodModel.Pin,
+            bouncerHapticPlayer = bouncerHapticPlayer,
         )
     }
 
@@ -97,6 +104,7 @@
                     isInputEnabled = MutableStateFlow(true),
                     onIntentionalUserInput = {},
                     authenticationMethod = AuthenticationMethodModel.Sim,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
 
             assertThat(underTest.isSimAreaVisible).isTrue()
@@ -122,6 +130,7 @@
                     isInputEnabled = MutableStateFlow(true),
                     onIntentionalUserInput = {},
                     authenticationMethod = AuthenticationMethodModel.Pin,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
@@ -487,11 +496,39 @@
         testScope.runTest {
             lockDeviceAndOpenPinBouncer()
 
-            underTest.onDigitButtonDown()
+            underTest.onDigitButtonDown(null)
 
             assertTrue(kosmos.fakeFalsingCollector.wasLastGestureAvoided())
         }
 
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onDigiButtonDown_deliversKeyStandardToken() =
+        testScope.runTest {
+            underTest.onDigitButtonDown(null)
+
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.KEYPRESS_STANDARD)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onBackspaceButtonPressed_deliversKeyDeleteToken() {
+        underTest.onBackspaceButtonPressed(null)
+
+        assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.KEYPRESS_DELETE)
+        assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onBackspaceButtonLongPressed_deliversLongPressToken() {
+        underTest.onBackspaceButtonLongPressed()
+
+        assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.LONG_PRESS)
+        assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+    }
+
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.currentScene)
         val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
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/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/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt
index 0c716137..639737b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
 import com.android.systemui.inputdevice.tutorial.domain.interactor.KeyboardTouchpadConnectionInteractor
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD
@@ -53,6 +54,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
+import org.mockito.kotlin.mock
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -81,6 +83,7 @@
                 Optional.of(kosmos.touchpadGesturesInteractor),
                 KeyboardTouchpadConnectionInteractor(keyboardRepo, touchpadRepo),
                 hasTouchpadTutorialScreens,
+                mock<InputDeviceTutorialLogger>(),
                 SavedStateHandle(mapOf(INTENT_TUTORIAL_TYPE_KEY to startingPeripheral))
             )
         lifecycle.addObserver(viewModel)
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/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/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/multivalentTests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
index 21a45ec..9dcbe1b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -616,6 +616,71 @@
     }
 
     @Test
+    fun angleDecreaseAfterCancelAnimation_emitsStartClosingEvent() {
+        setFoldState(folded = true)
+        sendHingeAngleEvent(0)
+        foldUpdates.clear()
+
+        setFoldState(folded = false)
+        rotationListener.value.onRotationChanged(1)
+        sendHingeAngleEvent(180)
+        screenOnStatusProvider.notifyScreenTurningOn()
+        screenOnStatusProvider.notifyScreenTurnedOn()
+
+        // Start folding
+        (180 downTo 60).forEach {
+            sendHingeAngleEvent(it)
+        }
+        // Stopped folding and simulate timeout in this posture
+        simulateTimeout()
+
+        assertThat(foldUpdates)
+            .containsExactly(
+                FOLD_UPDATE_START_OPENING, // unfolded
+                FOLD_UPDATE_FINISH_HALF_OPEN, // force-finished the animation because of rotation
+                FOLD_UPDATE_START_CLOSING, // start closing the device
+                FOLD_UPDATE_FINISH_HALF_OPEN, // finished closing and timed-out in this state
+            )
+
+    }
+
+    @Test
+    fun angleIncreaseDecreaseAfterHalfUnfold_emitsStartClosingEvent() {
+        setFoldState(folded = true)
+        sendHingeAngleEvent(0)
+        foldUpdates.clear()
+
+        setFoldState(folded = false)
+        sendHingeAngleEvent(90)
+        screenOnStatusProvider.notifyScreenTurningOn()
+        screenOnStatusProvider.notifyScreenTurnedOn()
+
+        // Stopped folding and simulate timeout in this posture
+        simulateTimeout()
+
+        // Unfold further
+        (90 until 180).forEach {
+            sendHingeAngleEvent(it)
+        }
+        // Start folding
+        (180 downTo 90).forEach {
+            sendHingeAngleEvent(it)
+        }
+
+        // Stopped folding and simulate timeout in this posture
+        simulateTimeout()
+
+        assertThat(foldUpdates)
+            .containsExactly(
+                FOLD_UPDATE_START_OPENING, // unfolded
+                FOLD_UPDATE_FINISH_HALF_OPEN, // force-finished the animation because of rotation
+                FOLD_UPDATE_START_CLOSING, // start closing the device
+                FOLD_UPDATE_FINISH_HALF_OPEN, // finished closing and timed-out in this state
+            )
+
+    }
+
+    @Test
     fun onUnfold_onSmallScreen_emitsStartOpening() {
         // the new display state might arrive later, so it shouldn't be used to decide to send the
         // start opening event, but only for the closing.
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
index 19e7537..b8c30fe 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
@@ -76,11 +76,7 @@
     /** Deliver MSDL feedback when the delete key of the pin bouncer is pressed */
     fun playDeleteKeyPressFeedback() = msdlPlayer.get().playToken(MSDLToken.KEYPRESS_DELETE)
 
-    /**
-     * Deliver MSDL feedback when the delete key of the pin bouncer is long-pressed
-     *
-     * @return whether MSDL feedback is allowed to play.
-     */
+    /** Deliver MSDL feedback when the delete key of the pin bouncer is long-pressed. */
     fun playDeleteKeyLongPressedFeedback() = msdlPlayer.get().playToken(MSDLToken.LONG_PRESS)
 
     /** Deliver MSDL feedback when a numpad key is pressed on the pin bouncer */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index c67b354..873d1b3 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.channels.Channel
@@ -42,6 +43,7 @@
 
     /** Name to use for performance tracing purposes. */
     val traceName: String,
+    protected val bouncerHapticPlayer: BouncerHapticPlayer? = null,
 ) : ExclusiveActivatable() {
 
     private val _animateFailure = MutableStateFlow(false)
@@ -80,6 +82,8 @@
                 return@collectLatest
             }
 
+            performAuthenticationHapticFeedback(authenticationResult)
+
             _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
             clearInput()
         }
@@ -112,20 +116,23 @@
     /** Returns the input entered so far. */
     protected abstract fun getInput(): List<Any>
 
+    /** Perform authentication result haptics */
+    private fun performAuthenticationHapticFeedback(result: AuthenticationResult) {
+        if (result == AuthenticationResult.SKIPPED) return
+
+        bouncerHapticPlayer?.playAuthenticationFeedback(
+            authenticationSucceeded = result == AuthenticationResult.SUCCEEDED
+        )
+    }
+
     /**
      * Attempts to authenticate the user using the current input value.
      *
      * @see BouncerInteractor.authenticate
      */
-    protected fun tryAuthenticate(
-        input: List<Any> = getInput(),
-        useAutoConfirm: Boolean = false,
-    ) {
+    protected fun tryAuthenticate(input: List<Any> = getInput(), useAutoConfirm: Boolean = false) {
         authenticationRequests.trySend(AuthenticationRequest(input, useAutoConfirm))
     }
 
-    private data class AuthenticationRequest(
-        val input: List<Any>,
-        val useAutoConfirm: Boolean,
-    )
+    private data class AuthenticationRequest(val input: List<Any>, val useAutoConfirm: Boolean)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
index 0aada06..0bcb58d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.qualifiers.Application
@@ -61,6 +62,7 @@
     private val pinViewModelFactory: PinBouncerViewModel.Factory,
     private val patternViewModelFactory: PatternBouncerViewModel.Factory,
     private val passwordViewModelFactory: PasswordBouncerViewModel.Factory,
+    private val bouncerHapticPlayer: BouncerHapticPlayer,
 ) : ExclusiveActivatable() {
     private val _selectedUserImage = MutableStateFlow<Bitmap?>(null)
     val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow()
@@ -162,10 +164,7 @@
             }
 
             launch {
-                combine(
-                        userSwitcher.users,
-                        userSwitcher.menu,
-                    ) { users, actions ->
+                combine(userSwitcher.users, userSwitcher.menu) { users, actions ->
                         users.map { user ->
                             UserSwitcherDropdownItemViewModel(
                                 icon = Icon.Loaded(user.image, contentDescription = null),
@@ -178,7 +177,7 @@
                                     icon =
                                         Icon.Resource(
                                             action.iconResourceId,
-                                            contentDescription = null
+                                            contentDescription = null,
                                         ),
                                     text = Text.Resource(action.textResourceId),
                                     onClick = action.onClicked,
@@ -226,7 +225,7 @@
     }
 
     private fun getChildViewModel(
-        authenticationMethod: AuthenticationMethodModel,
+        authenticationMethod: AuthenticationMethodModel
     ): AuthMethodBouncerViewModel? {
         // If the current child view-model matches the authentication method, reuse it instead of
         // creating a new instance.
@@ -241,12 +240,14 @@
                     authenticationMethod = authenticationMethod,
                     onIntentionalUserInput = ::onIntentionalUserInput,
                     isInputEnabled = isInputEnabled,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
             is AuthenticationMethodModel.Sim ->
                 pinViewModelFactory.create(
                     authenticationMethod = authenticationMethod,
                     onIntentionalUserInput = ::onIntentionalUserInput,
                     isInputEnabled = isInputEnabled,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
             is AuthenticationMethodModel.Password ->
                 passwordViewModelFactory.create(
@@ -257,6 +258,7 @@
                 patternViewModelFactory.create(
                     onIntentionalUserInput = ::onIntentionalUserInput,
                     isInputEnabled = isInputEnabled,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
             else -> null
         }
@@ -317,10 +319,7 @@
         return when {
             // The wipe dialog takes priority over the lockout dialog.
             wipeText != null ->
-                DialogViewModel(
-                    text = wipeText,
-                    onDismiss = { wipeDialogMessage.value = null },
-                )
+                DialogViewModel(text = wipeText, onDismiss = { wipeDialogMessage.value = null })
             lockoutText != null ->
                 DialogViewModel(
                     text = lockoutText,
@@ -338,7 +337,7 @@
     fun onKeyEvent(keyEvent: KeyEvent): Boolean {
         return (authMethodViewModel.value as? PinBouncerViewModel)?.onKeyEvent(
             keyEvent.type,
-            keyEvent.nativeKeyEvent.keyCode
+            keyEvent.nativeKeyEvent.keyCode,
         ) ?: false
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 0a866b4..158f102 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -18,9 +18,11 @@
 
 import android.content.Context
 import android.util.TypedValue
+import android.view.View
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
 import com.android.systemui.res.R
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -35,7 +37,6 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the pattern bouncer UI. */
@@ -44,6 +45,7 @@
 constructor(
     private val applicationContext: Context,
     interactor: BouncerInteractor,
+    @Assisted bouncerHapticPlayer: BouncerHapticPlayer,
     @Assisted isInputEnabled: StateFlow<Boolean>,
     @Assisted private val onIntentionalUserInput: () -> Unit,
 ) :
@@ -51,6 +53,7 @@
         interactor = interactor,
         isInputEnabled = isInputEnabled,
         traceName = "PatternBouncerViewModel",
+        bouncerHapticPlayer = bouncerHapticPlayer,
     ) {
 
     /** The number of columns in the dot grid. */
@@ -190,14 +193,7 @@
     private fun defaultDots(): List<PatternDotViewModel> {
         return buildList {
             (0 until columnCount).forEach { x ->
-                (0 until rowCount).forEach { y ->
-                    add(
-                        PatternDotViewModel(
-                            x = x,
-                            y = y,
-                        )
-                    )
-                }
+                (0 until rowCount).forEach { y -> add(PatternDotViewModel(x = x, y = y)) }
             }
         }
     }
@@ -207,14 +203,17 @@
         applicationContext.resources.getValue(
             com.android.internal.R.dimen.lock_pattern_dot_hit_factor,
             outValue,
-            true
+            true,
         )
         max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR)
     }
 
+    fun performDotFeedback(view: View?) = bouncerHapticPlayer?.playPatternDotFeedback(view)
+
     @AssistedFactory
     interface Factory {
         fun create(
+            bouncerHapticPlayer: BouncerHapticPlayer,
             isInputEnabled: StateFlow<Boolean>,
             onIntentionalUserInput: () -> Unit,
         ): PatternBouncerViewModel
@@ -231,7 +230,7 @@
  */
 private fun PatternDotViewModel.isOnLineSegment(
     first: PatternDotViewModel,
-    second: PatternDotViewModel
+    second: PatternDotViewModel,
 ): Boolean {
     val anotherPoint = this
     // No need to consider any points outside the bounds of two end points
@@ -253,14 +252,8 @@
     return (this in a..b) || (this in b..a)
 }
 
-data class PatternDotViewModel(
-    val x: Int,
-    val y: Int,
-) {
+data class PatternDotViewModel(val x: Int, val y: Int) {
     fun toCoordinate(): AuthenticationPatternCoordinate {
-        return AuthenticationPatternCoordinate(
-            x = x,
-            y = y,
-        )
+        return AuthenticationPatternCoordinate(x = x, y = y)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index da29c62..0cb4260 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -19,12 +19,14 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.content.Context
+import android.view.HapticFeedbackConstants
 import android.view.KeyEvent.KEYCODE_0
 import android.view.KeyEvent.KEYCODE_9
 import android.view.KeyEvent.KEYCODE_DEL
 import android.view.KeyEvent.KEYCODE_NUMPAD_0
 import android.view.KeyEvent.KEYCODE_NUMPAD_9
 import android.view.KeyEvent.isConfirmKey
+import android.view.View
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyEventType
 import com.android.keyguard.PinShapeAdapter
@@ -32,6 +34,7 @@
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
 import com.android.systemui.res.R
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -56,6 +59,7 @@
     applicationContext: Context,
     interactor: BouncerInteractor,
     private val simBouncerInteractor: SimBouncerInteractor,
+    @Assisted bouncerHapticPlayer: BouncerHapticPlayer,
     @Assisted isInputEnabled: StateFlow<Boolean>,
     @Assisted private val onIntentionalUserInput: () -> Unit,
     @Assisted override val authenticationMethod: AuthenticationMethodModel,
@@ -64,6 +68,7 @@
         interactor = interactor,
         isInputEnabled = isInputEnabled,
         traceName = "PinBouncerViewModel",
+        bouncerHapticPlayer = bouncerHapticPlayer,
     ) {
     /**
      * Whether the sim-related UI in the pin view is showing.
@@ -126,10 +131,9 @@
                     .collect { _hintedPinLength.value = it }
             }
             launch {
-                combine(
-                        mutablePinInput,
-                        interactor.isAutoConfirmEnabled,
-                    ) { mutablePinEntries, isAutoConfirmEnabled ->
+                combine(mutablePinInput, interactor.isAutoConfirmEnabled) {
+                        mutablePinEntries,
+                        isAutoConfirmEnabled ->
                         computeBackspaceButtonAppearance(
                             pinInput = mutablePinEntries,
                             isAutoConfirmEnabled = isAutoConfirmEnabled,
@@ -183,8 +187,22 @@
         mutablePinInput.value = mutablePinInput.value.deleteLast()
     }
 
+    fun onBackspaceButtonPressed(view: View?) {
+        if (bouncerHapticPlayer?.isEnabled == true) {
+            bouncerHapticPlayer.playDeleteKeyPressFeedback()
+        } else {
+            view?.performHapticFeedback(
+                HapticFeedbackConstants.VIRTUAL_KEY,
+                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
+            )
+        }
+    }
+
     /** Notifies that the user long-pressed the backspace button. */
     fun onBackspaceButtonLongPressed() {
+        if (bouncerHapticPlayer?.isEnabled == true) {
+            bouncerHapticPlayer.playDeleteKeyLongPressedFeedback()
+        }
         clearInput()
     }
 
@@ -266,13 +284,24 @@
         }
     }
 
-    /** Notifies that the user has pressed down on a digit button. */
-    fun onDigitButtonDown() {
+    /**
+     * Notifies that the user has pressed down on a digit button. This function also performs haptic
+     * feedback on the view.
+     */
+    fun onDigitButtonDown(view: View?) {
         if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) {
             // Current PIN bouncer informs FalsingInteractor#avoidGesture() upon every Pin button
             // touch.
             super.onDown()
         }
+        if (bouncerHapticPlayer?.isEnabled == true) {
+            bouncerHapticPlayer.playNumpadKeyFeedback()
+        } else {
+            view?.performHapticFeedback(
+                HapticFeedbackConstants.VIRTUAL_KEY,
+                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
+            )
+        }
     }
 
     @AssistedFactory
@@ -281,6 +310,7 @@
             isInputEnabled: StateFlow<Boolean>,
             onIntentionalUserInput: () -> Unit,
             authenticationMethod: AuthenticationMethodModel,
+            bouncerHapticPlayer: BouncerHapticPlayer,
         ): PinBouncerViewModel
     }
 
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/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
index e50c05c..e09e198 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
@@ -44,12 +45,12 @@
  * @property[vibratorHelper] The [VibratorHelper] to deliver haptic effects.
  * @property[effectDuration] The duration of the effect in ms.
  */
-// TODO(b/332902869): In addition from being injectable, we can consider making it a singleton
 class QSLongPressEffect
 @Inject
 constructor(
     private val vibratorHelper: VibratorHelper?,
     private val keyguardStateController: KeyguardStateController,
+    private val falsingManager: FalsingManager,
     @QSLog private val logBuffer: LogBuffer,
 ) {
 
@@ -72,7 +73,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 +153,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 +249,7 @@
             LongPressHapticBuilder.createLongPressHint(
                 durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION,
                 durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION,
-                effectDuration
+                effectDuration,
             )
         setState(State.IDLE)
         return true
@@ -265,7 +278,7 @@
                 }
 
                 override fun dialogTransitionController(
-                    cuj: DialogCuj?,
+                    cuj: DialogCuj?
                 ): DialogTransitionAnimator.Controller? =
                     DialogTransitionAnimator.Controller.fromView(view, cuj)
             }
@@ -298,7 +311,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/inputdevice/tutorial/InputDeviceTutorialLogger.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialLogger.kt
index 9525174..48f5cb6 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialLogger.kt
@@ -16,27 +16,27 @@
 
 package com.android.systemui.inputdevice.tutorial
 
+import com.android.systemui.inputdevice.tutorial.domain.interactor.ConnectionState
+import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen as KeyboardTouchpadTutorialScreen
+import com.android.systemui.log.ConstantStringsLogger
+import com.android.systemui.log.ConstantStringsLoggerImpl
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.MessageInitializer
+import com.android.systemui.log.core.MessagePrinter
 import com.android.systemui.log.dagger.InputDeviceTutorialLog
-import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen
-import com.google.errorprone.annotations.CompileTimeConstant
+import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen as TouchpadTutorialScreen
 import javax.inject.Inject
 
 private const val TAG = "InputDeviceTutorial"
 
 class InputDeviceTutorialLogger
 @Inject
-constructor(@InputDeviceTutorialLog private val buffer: LogBuffer) {
+constructor(@InputDeviceTutorialLog private val buffer: LogBuffer) :
+    ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) {
 
-    fun log(@CompileTimeConstant s: String) {
-        buffer.log(TAG, LogLevel.INFO, message = s)
-    }
-
-    fun logGoingToScreen(screen: Screen, context: TutorialContext) {
-        buffer.log(
-            TAG,
-            LogLevel.INFO,
+    fun logGoingToScreen(screen: TouchpadTutorialScreen, context: TutorialContext) {
+        logInfo(
             {
                 str1 = screen.toString()
                 str2 = context.string
@@ -46,7 +46,58 @@
     }
 
     fun logCloseTutorial(context: TutorialContext) {
-        buffer.log(TAG, LogLevel.INFO, { str1 = context.string }, { "Closing $str1" })
+        logInfo({ str1 = context.string }, { "Closing $str1" })
+    }
+
+    fun logOpenTutorial(context: TutorialContext) {
+        logInfo({ str1 = context.string }, { "Opening $str1" })
+    }
+
+    fun logNextScreenMissingHardware(nextScreen: KeyboardTouchpadTutorialScreen) {
+        buffer.log(
+            TAG,
+            LogLevel.WARNING,
+            { str1 = nextScreen.toString() },
+            { "next screen should be $str1 but required hardware is missing" }
+        )
+    }
+
+    fun logNextScreen(nextScreen: KeyboardTouchpadTutorialScreen) {
+        logInfo({ str1 = nextScreen.toString() }, { "going to $str1 screen" })
+    }
+
+    fun logNewConnectionState(connectionState: ConnectionState) {
+        logInfo(
+            {
+                bool1 = connectionState.touchpadConnected
+                bool2 = connectionState.keyboardConnected
+            },
+            { "Received connection state: touchpad connected: $bool1 keyboard connected: $bool2" }
+        )
+    }
+
+    fun logMovingBetweenScreens(
+        previousScreen: KeyboardTouchpadTutorialScreen?,
+        currentScreen: KeyboardTouchpadTutorialScreen
+    ) {
+        logInfo(
+            {
+                str1 = previousScreen?.toString() ?: "NO_SCREEN"
+                str2 = currentScreen.toString()
+            },
+            { "Moving from $str1 screen to $str2 screen" }
+        )
+    }
+
+    fun logGoingBack(previousScreen: KeyboardTouchpadTutorialScreen) {
+        logInfo({ str1 = previousScreen.toString() }, { "Going back to $str1 screen" })
+    }
+
+    private inline fun logInfo(
+        messageInitializer: MessageInitializer,
+        noinline messagePrinter: MessagePrinter
+    ) {
+        buffer.log(TAG, LogLevel.INFO, messageInitializer, messagePrinter)
     }
 
     enum class TutorialContext(val string: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
index 1adc285..c130c6c 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
@@ -28,6 +28,8 @@
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.lifecycleScope
 import com.android.compose.theme.PlatformTheme
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext
 import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider
 import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen
 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel
@@ -48,6 +50,7 @@
 constructor(
     private val viewModelFactoryAssistedProvider: ViewModelFactoryAssistedProvider,
     private val touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>,
+    private val logger: InputDeviceTutorialLogger,
 ) : ComponentActivity() {
 
     companion object {
@@ -74,6 +77,7 @@
         lifecycleScope.launch {
             vm.closeActivity.collect { finish ->
                 if (finish) {
+                    logger.logCloseTutorial(TutorialContext.KEYBOARD_TOUCHPAD_TUTORIAL)
                     finish()
                 }
             }
@@ -81,6 +85,9 @@
         setContent {
             PlatformTheme { KeyboardTouchpadTutorialContainer(vm, touchpadTutorialScreensProvider) }
         }
+        if (savedInstanceState == null) {
+            logger.logOpenTutorial(TutorialContext.KEYBOARD_TOUCHPAD_TUTORIAL)
+        }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt
index 315c102..5cf1967 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt
@@ -22,6 +22,7 @@
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
+import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
 import com.android.systemui.inputdevice.tutorial.domain.interactor.ConnectionState
 import com.android.systemui.inputdevice.tutorial.domain.interactor.KeyboardTouchpadConnectionInteractor
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY
@@ -47,6 +48,7 @@
     private val gesturesInteractor: Optional<TouchpadGesturesInteractor>,
     private val keyboardTouchpadConnectionInteractor: KeyboardTouchpadConnectionInteractor,
     private val hasTouchpadTutorialScreens: Boolean,
+    private val logger: InputDeviceTutorialLogger,
     handle: SavedStateHandle
 ) : ViewModel(), DefaultLifecycleObserver {
 
@@ -68,7 +70,10 @@
 
     init {
         viewModelScope.launch {
-            keyboardTouchpadConnectionInteractor.connectionState.collect { connectionState = it }
+            keyboardTouchpadConnectionInteractor.connectionState.collect {
+                logger.logNewConnectionState(connectionState)
+                connectionState = it
+            }
         }
 
         viewModelScope.launch {
@@ -89,7 +94,14 @@
         viewModelScope.launch {
             // close activity if screen requires touchpad but we don't have it. This can only happen
             // when current sysui build doesn't contain touchpad module dependency
-            _screen.filterNot { it.canBeShown() }.collect { _closeActivity.value = true }
+            _screen
+                .filterNot { it.canBeShown() }
+                .collect {
+                    logger.e(
+                        "Touchpad is connected but touchpad module is missing, something went wrong"
+                    )
+                    _closeActivity.value = true
+                }
         }
     }
 
@@ -114,11 +126,14 @@
             if (requiredHardwarePresent(nextScreen)) {
                 break
             }
+            logger.logNextScreenMissingHardware(nextScreen)
             nextScreen = nextScreen.next()
         }
         if (nextScreen == null) {
+            logger.d("Final screen reached, closing tutorial")
             _closeActivity.value = true
         } else {
+            logger.logNextScreen(nextScreen)
             _screen.value = nextScreen
             screensBackStack.add(nextScreen)
         }
@@ -127,6 +142,7 @@
     private fun Screen.canBeShown() = requiredHardware != TOUCHPAD || hasTouchpadTutorialScreens
 
     private fun setupDeviceState(previousScreen: Screen?, currentScreen: Screen) {
+        logger.logMovingBetweenScreens(previousScreen, currentScreen)
         if (previousScreen?.requiredHardware == currentScreen.requiredHardware) return
         previousScreen?.let { clearDeviceStateForScreen(it) }
         when (currentScreen.requiredHardware) {
@@ -153,6 +169,7 @@
             _closeActivity.value = true
         } else {
             screensBackStack.removeLast()
+            logger.logGoingBack(screensBackStack.last())
             _screen.value = screensBackStack.last()
         }
     }
@@ -162,6 +179,7 @@
     constructor(
         private val gesturesInteractor: Optional<TouchpadGesturesInteractor>,
         private val keyboardTouchpadConnected: KeyboardTouchpadConnectionInteractor,
+        private val logger: InputDeviceTutorialLogger,
         @Assisted private val hasTouchpadTutorialScreens: Boolean,
     ) : AbstractSavedStateViewModelFactory() {
 
@@ -180,6 +198,7 @@
                 gesturesInteractor,
                 keyboardTouchpadConnected,
                 hasTouchpadTutorialScreens,
+                logger,
                 handle
             )
                 as T
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/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/statusbar/core/StatusBarInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
index 4056e7b..c6c303e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
@@ -16,9 +16,11 @@
 package com.android.systemui.statusbar.core
 
 import android.app.Fragment
-import com.android.systemui.res.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.fragments.FragmentHostManager
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
+import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewUpdatedListener
 import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions
 import com.android.systemui.statusbar.phone.PhoneStatusBarView
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
@@ -33,31 +35,62 @@
  * Responsible for creating the status bar window and initializing the root components of that
  * window (see [CollapsedStatusBarFragment])
  */
-@SysUISingleton
-class StatusBarInitializer @Inject constructor(
-    private val windowController: StatusBarWindowController,
-    private val collapsedStatusBarFragmentProvider: Provider<CollapsedStatusBarFragment>,
-    private val creationListeners: Set<@JvmSuppressWildcards OnStatusBarViewInitializedListener>,
-) {
+interface StatusBarInitializer {
 
-    var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? = null
+    var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener?
 
     /**
      * Creates the status bar window and root views, and initializes the component.
      *
      * TODO(b/277764509): Initialize the status bar via [CoreStartable#start].
      */
-    fun initializeStatusBar() {
-        windowController.fragmentHostManager.addTagListener(
+    fun initializeStatusBar()
+
+    interface OnStatusBarViewInitializedListener {
+
+        /**
+         * The status bar view has been initialized.
+         *
+         * @param component Dagger component that is created when the status bar view is created.
+         *   Can be used to retrieve dependencies from that scope, including the status bar root
+         *   view.
+         */
+        fun onStatusBarViewInitialized(component: StatusBarFragmentComponent)
+    }
+
+    interface OnStatusBarViewUpdatedListener {
+        fun onStatusBarViewUpdated(
+            statusBarView: PhoneStatusBarView,
+            statusBarViewController: PhoneStatusBarViewController,
+            statusBarTransitions: PhoneStatusBarTransitions,
+        )
+    }
+}
+
+@SysUISingleton
+class StatusBarInitializerImpl
+@Inject
+constructor(
+    private val windowController: StatusBarWindowController,
+    private val collapsedStatusBarFragmentProvider: Provider<CollapsedStatusBarFragment>,
+    private val creationListeners: Set<@JvmSuppressWildcards OnStatusBarViewInitializedListener>,
+) : StatusBarInitializer {
+
+    override var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? = null
+
+    override fun initializeStatusBar() {
+        windowController.fragmentHostManager
+            .addTagListener(
                 CollapsedStatusBarFragment.TAG,
                 object : FragmentHostManager.FragmentListener {
                     override fun onFragmentViewCreated(tag: String, fragment: Fragment) {
-                        val statusBarFragmentComponent = (fragment as CollapsedStatusBarFragment)
-                                .statusBarFragmentComponent ?: throw IllegalStateException()
+                        val statusBarFragmentComponent =
+                            (fragment as CollapsedStatusBarFragment).statusBarFragmentComponent
+                                ?: throw IllegalStateException()
                         statusBarViewUpdatedListener?.onStatusBarViewUpdated(
                             statusBarFragmentComponent.phoneStatusBarView,
                             statusBarFragmentComponent.phoneStatusBarViewController,
-                            statusBarFragmentComponent.phoneStatusBarTransitions
+                            statusBarFragmentComponent.phoneStatusBarTransitions,
                         )
                         creationListeners.forEach { listener ->
                             listener.onStatusBarViewInitialized(statusBarFragmentComponent)
@@ -67,33 +100,15 @@
                     override fun onFragmentViewDestroyed(tag: String?, fragment: Fragment?) {
                         // nop
                     }
-                }
-        ).fragmentManager
-                .beginTransaction()
-                .replace(
-                    R.id.status_bar_container,
-                    collapsedStatusBarFragmentProvider.get(),
-                    CollapsedStatusBarFragment.TAG
-                )
-                .commit()
-    }
-
-    interface OnStatusBarViewInitializedListener {
-
-        /**
-         * The status bar view has been initialized.
-         *
-         * @param component Dagger component that is created when the status bar view is created.
-         * Can be used to retrieve dependencies from that scope, including the status bar root view.
-         */
-        fun onStatusBarViewInitialized(component: StatusBarFragmentComponent)
-    }
-
-    interface OnStatusBarViewUpdatedListener {
-        fun onStatusBarViewUpdated(
-            statusBarView: PhoneStatusBarView,
-            statusBarViewController: PhoneStatusBarViewController,
-            statusBarTransitions: PhoneStatusBarTransitions
-        )
+                },
+            )
+            .fragmentManager
+            .beginTransaction()
+            .replace(
+                R.id.status_bar_container,
+                collapsedStatusBarFragmentProvider.get(),
+                CollapsedStatusBarFragment.TAG,
+            )
+            .commit()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 406a664..526c64c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -20,12 +20,16 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.statusbar.core.StatusBarInitializer
+import com.android.systemui.statusbar.core.StatusBarInitializerImpl
 import com.android.systemui.statusbar.data.StatusBarDataLayerModule
 import com.android.systemui.statusbar.phone.LightBarController
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
 import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.statusbar.window.StatusBarWindowControllerImpl
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -57,6 +61,13 @@
     @ClassKey(StatusBarSignalPolicy::class)
     abstract fun bindStatusBarSignalPolicy(impl: StatusBarSignalPolicy): CoreStartable
 
+    @Binds abstract fun statusBarInitializer(impl: StatusBarInitializerImpl): StatusBarInitializer
+
+    @Binds
+    abstract fun statusBarWindowController(
+        impl: StatusBarWindowControllerImpl
+    ): StatusBarWindowController
+
     companion object {
         @Provides
         @SysUISingleton
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/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
new file mode 100644
index 0000000..421e5c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.fragments.FragmentHostManager
+import java.util.Optional
+
+/** Encapsulates all logic for the status bar window state management. */
+interface StatusBarWindowController {
+    val statusBarHeight: Int
+
+    /** Rereads the status bar height and reapplies the current state if the height is different. */
+    fun refreshStatusBarHeight()
+
+    /** Adds the status bar view to the window manager. */
+    fun attach()
+
+    /** Adds the given view to the status bar window view. */
+    fun addViewToWindow(view: View, layoutParams: ViewGroup.LayoutParams)
+
+    /** Returns the status bar window's background view. */
+    val backgroundView: View
+
+    /** Returns a fragment host manager for the status bar window view. */
+    val fragmentHostManager: FragmentHostManager
+
+    /**
+     * Provides an updated animation controller if we're animating a view in the status bar.
+     *
+     * This is needed because we have to make sure that the status bar window matches the full
+     * screen during the animation and that we are expanding the view below the other status bar
+     * text.
+     *
+     * @param rootView the root view of the animation
+     * @param animationController the default animation controller to use
+     * @return If the animation is on a view in the status bar, returns an Optional containing an
+     *   updated animation controller that handles status-bar-related animation details. Returns an
+     *   empty optional if the animation is *not* on a view in the status bar.
+     */
+    fun wrapAnimationControllerIfInStatusBar(
+        rootView: View,
+        animationController: ActivityTransitionAnimator.Controller,
+    ): Optional<ActivityTransitionAnimator.Controller>
+
+    /** Set force status bar visible. */
+    fun setForceStatusBarVisible(forceStatusBarVisible: Boolean)
+
+    /**
+     * Sets whether an ongoing process requires the status bar to be forced visible.
+     *
+     * This method is separate from {@link this#setForceStatusBarVisible} because the ongoing
+     * process **takes priority**. For example, if {@link this#setForceStatusBarVisible} is set to
+     * false but this method is set to true, then the status bar **will** be visible.
+     *
+     * TODO(b/195839150): We should likely merge this method and {@link
+     *   this#setForceStatusBarVisible} together and use some sort of ranking system instead.
+     */
+    fun setOngoingProcessRequiresStatusBarVisible(visible: Boolean)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
similarity index 84%
rename from packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
index c30a6b7..1a0327c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
@@ -46,6 +46,8 @@
 import android.view.WindowInsets;
 import android.view.WindowManager;
 
+import androidx.annotation.NonNull;
+
 import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.animation.ActivityTransitionAnimator;
@@ -67,7 +69,7 @@
  * Encapsulates all logic for the status bar window state management.
  */
 @SysUISingleton
-public class StatusBarWindowController {
+public class StatusBarWindowControllerImpl implements StatusBarWindowController {
     private static final String TAG = "StatusBarWindowController";
     private static final boolean DEBUG = false;
 
@@ -89,7 +91,7 @@
     private final Binder mInsetsSourceOwner = new Binder();
 
     @Inject
-    public StatusBarWindowController(
+    public StatusBarWindowControllerImpl(
             Context context,
             @StatusBarWindowModule.InternalWindowView StatusBarWindowView statusBarWindowView,
             ViewCaptureAwareWindowManager viewCaptureAwareWindowManager,
@@ -117,14 +119,12 @@
                                 /* attachedViewProvider=*/ () -> mStatusBarWindowView)));
     }
 
+    @Override
     public int getStatusBarHeight() {
         return mBarHeight;
     }
 
-    /**
-     * Rereads the status bar height and reapplys the current state if the height
-     * is different.
-     */
+    @Override
     public void refreshStatusBarHeight() {
         Trace.beginSection("StatusBarWindowController#refreshStatusBarHeight");
         try {
@@ -141,9 +141,7 @@
         }
     }
 
-    /**
-     * Adds the status bar view to the window manager.
-     */
+    @Override
     public void attach() {
         // Now that the status bar window encompasses the sliding panel and its
         // translucent backdrop, the entire thing is made TRANSLUCENT and is
@@ -161,54 +159,47 @@
         apply(mCurrentState);
     }
 
-    /** Adds the given view to the status bar window view. */
-    public void addViewToWindow(View view, ViewGroup.LayoutParams layoutParams) {
+    @Override
+    public void addViewToWindow(@NonNull View view, @NonNull ViewGroup.LayoutParams layoutParams) {
         mStatusBarWindowView.addView(view, layoutParams);
     }
 
-    /** Returns the status bar window's background view. */
+    @NonNull
+    @Override
     public View getBackgroundView() {
         return mStatusBarWindowView.findViewById(R.id.status_bar_container);
     }
 
-    /** Returns a fragment host manager for the status bar window view. */
+    @NonNull
+    @Override
     public FragmentHostManager getFragmentHostManager() {
         return mFragmentService.getFragmentHostManager(mStatusBarWindowView);
     }
 
-    /**
-     * Provides an updated animation controller if we're animating a view in the status bar.
-     *
-     * This is needed because we have to make sure that the status bar window matches the full
-     * screen during the animation and that we are expanding the view below the other status bar
-     * text.
-     *
-     * @param rootView the root view of the animation
-     * @param animationController the default animation controller to use
-     * @return If the animation is on a view in the status bar, returns an Optional containing an
-     *   updated animation controller that handles status-bar-related animation details. Returns an
-     *   empty optional if the animation is *not* on a view in the status bar.
-     */
+    @NonNull
+    @Override
     public Optional<ActivityTransitionAnimator.Controller> wrapAnimationControllerIfInStatusBar(
-            View rootView, ActivityTransitionAnimator.Controller animationController) {
+            @NonNull View rootView,
+            @NonNull ActivityTransitionAnimator.Controller animationController) {
         if (rootView != mStatusBarWindowView) {
             return Optional.empty();
         }
 
         animationController.setTransitionContainer(mLaunchAnimationContainer);
-        return Optional.of(new DelegateTransitionAnimatorController(animationController) {
-            @Override
-            public void onTransitionAnimationStart(boolean isExpandingFullyAbove) {
-                getDelegate().onTransitionAnimationStart(isExpandingFullyAbove);
-                setLaunchAnimationRunning(true);
-            }
+        return Optional.of(
+                new DelegateTransitionAnimatorController(animationController) {
+                    @Override
+                    public void onTransitionAnimationStart(boolean isExpandingFullyAbove) {
+                        getDelegate().onTransitionAnimationStart(isExpandingFullyAbove);
+                        setLaunchAnimationRunning(true);
+                    }
 
-            @Override
-            public void onTransitionAnimationEnd(boolean isExpandingFullyAbove) {
-                getDelegate().onTransitionAnimationEnd(isExpandingFullyAbove);
-                setLaunchAnimationRunning(false);
-            }
-        });
+                    @Override
+                    public void onTransitionAnimationEnd(boolean isExpandingFullyAbove) {
+                        getDelegate().onTransitionAnimationEnd(isExpandingFullyAbove);
+                        setLaunchAnimationRunning(false);
+                    }
+                });
     }
 
     private WindowManager.LayoutParams getBarLayoutParams(int rotation) {
@@ -275,22 +266,13 @@
         }
     }
 
-    /** Set force status bar visible. */
+    @Override
     public void setForceStatusBarVisible(boolean forceStatusBarVisible) {
         mCurrentState.mForceStatusBarVisible = forceStatusBarVisible;
         apply(mCurrentState);
     }
 
-    /**
-     * Sets whether an ongoing process requires the status bar to be forced visible.
-     *
-     * This method is separate from {@link this#setForceStatusBarVisible} because the ongoing
-     * process **takes priority**. For example, if {@link this#setForceStatusBarVisible} is set to
-     * false but this method is set to true, then the status bar **will** be visible.
-     *
-     * TODO(b/195839150): We should likely merge this method and
-     * {@link this#setForceStatusBarVisible} together and use some sort of ranking system instead.
-     */
+    @Override
     public void setOngoingProcessRequiresStatusBarVisible(boolean visible) {
         mCurrentState.mOngoingProcessRequiresStatusBarVisible = visible;
         apply(mCurrentState);
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt
index 1a41987..80ea925 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt
@@ -30,12 +30,12 @@
     private val logger: InputDeviceTutorialLogger,
 ) {
     fun disableGestures() {
-        logger.log("Disabling touchpad gestures across the system")
+        logger.d("Disabling touchpad gestures across the system")
         setGesturesState(disabled = true)
     }
 
     fun enableGestures() {
-        logger.log("Enabling touchpad gestures across the system")
+        logger.d("Enabling touchpad gestures across the system")
         setGesturesState(disabled = false)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
index 6fe6547..d03b2e7 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
@@ -57,6 +57,7 @@
         }
         // required to handle 3+ fingers on touchpad
         window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+        logger.logOpenTutorial(TutorialContext.TOUCHPAD_TUTORIAL)
     }
 
     private fun finishTutorial() {
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/bouncer/ui/composable/PatternBouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
index 4b61a0d..088bb02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
@@ -25,6 +25,7 @@
 import androidx.test.filters.LargeTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.ui.viewmodel.patternBouncerViewModelFactory
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.motion.createSysUiComposeMotionTestRule
 import com.android.systemui.testKosmos
@@ -55,6 +56,7 @@
         kosmos.patternBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
             onIntentionalUserInput = {},
+            bouncerHapticPlayer = kosmos.bouncerHapticPlayer,
         )
 
     @Before
@@ -75,11 +77,11 @@
                     content = { play -> if (play) PatternBouncerUnderTest() },
                     ComposeRecordingSpec.until(
                         recordBefore = false,
-                        checkDone = { motionTestValueOfNode(MotionTestKeys.entryCompleted) }
+                        checkDone = { motionTestValueOfNode(MotionTestKeys.entryCompleted) },
                     ) {
                         feature(MotionTestKeys.dotAppearFadeIn, floatArray)
                         feature(MotionTestKeys.dotAppearMoveUp, floatArray)
-                    }
+                    },
                 )
 
             assertThat(motion).timeSeriesMatchesGolden()
@@ -100,7 +102,7 @@
                         viewModel.onDragEnd()
                         // Failure animation starts when animateFailure flips to true...
                         viewModel.animateFailure.takeWhile { !it }.collect {}
-                    }
+                    },
                 ) {
                     // ... and ends when the composable flips it back to false.
                     viewModel.animateFailure.takeWhile { it }.collect {}
@@ -111,7 +113,7 @@
                     content = { PatternBouncerUnderTest() },
                     ComposeRecordingSpec(failureAnimationMotionControl) {
                         feature(MotionTestKeys.dotScaling, floatArray)
-                    }
+                    },
                 )
             assertThat(motion).timeSeriesMatchesGolden()
         }
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/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index af04309..c710c56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -168,7 +168,7 @@
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
-import com.android.systemui.statusbar.core.StatusBarInitializer;
+import com.android.systemui.statusbar.core.StatusBarInitializerImpl;
 import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
@@ -504,7 +504,7 @@
                 mock(FragmentService.class),
                 mLightBarController,
                 mAutoHideController,
-                new StatusBarInitializer(
+                new StatusBarInitializerImpl(
                         mStatusBarWindowController,
                         mCollapsedStatusBarFragmentProvider,
                         emptySet()),
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/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
index 649e4e8..1b1d8c5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -25,6 +25,8 @@
 import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
 import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -34,9 +36,7 @@
 import kotlinx.coroutines.flow.StateFlow
 
 val Kosmos.bouncerUserActionsViewModel by Fixture {
-    BouncerUserActionsViewModel(
-        bouncerInteractor = bouncerInteractor,
-    )
+    BouncerUserActionsViewModel(bouncerInteractor = bouncerInteractor)
 }
 
 val Kosmos.bouncerUserActionsViewModelFactory by Fixture {
@@ -59,6 +59,7 @@
         pinViewModelFactory = pinBouncerViewModelFactory,
         patternViewModelFactory = patternBouncerViewModelFactory,
         passwordViewModelFactory = passwordBouncerViewModelFactory,
+        bouncerHapticPlayer = bouncerHapticPlayer,
     )
 }
 
@@ -76,6 +77,7 @@
             isInputEnabled: StateFlow<Boolean>,
             onIntentionalUserInput: () -> Unit,
             authenticationMethod: AuthenticationMethodModel,
+            bouncerHapticPlayer: BouncerHapticPlayer,
         ): PinBouncerViewModel {
             return PinBouncerViewModel(
                 applicationContext = applicationContext,
@@ -84,6 +86,7 @@
                 isInputEnabled = isInputEnabled,
                 onIntentionalUserInput = onIntentionalUserInput,
                 authenticationMethod = authenticationMethod,
+                bouncerHapticPlayer = bouncerHapticPlayer,
             )
         }
     }
@@ -92,6 +95,7 @@
 val Kosmos.patternBouncerViewModelFactory by Fixture {
     object : PatternBouncerViewModel.Factory {
         override fun create(
+            bouncerHapticPlayer: BouncerHapticPlayer,
             isInputEnabled: StateFlow<Boolean>,
             onIntentionalUserInput: () -> Unit,
         ): PatternBouncerViewModel {
@@ -100,6 +104,7 @@
                 interactor = bouncerInteractor,
                 isInputEnabled = isInputEnabled,
                 onIntentionalUserInput = onIntentionalUserInput,
+                bouncerHapticPlayer = bouncerHapticPlayer,
             )
         }
     }
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/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/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/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index a100974..57b58d8 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -128,7 +128,11 @@
 
         val currentDirection =
             if (angle < lastHingeAngle) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
-        if (isTransitionInProgress && currentDirection != lastFoldUpdate) {
+        val changedDirectionWhileInTransition =
+            isTransitionInProgress && currentDirection != lastFoldUpdate
+        val unfoldedPastThresholdSinceLastTransition =
+            angle - lastHingeAngleBeforeTransition > HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES
+        if (changedDirectionWhileInTransition || unfoldedPastThresholdSinceLastTransition) {
             lastHingeAngleBeforeTransition = lastHingeAngle
         }
 
@@ -153,7 +157,7 @@
                 isOnLargeScreen // Avoids sending closing event when on small screen.
         // Start event is sent regardless due to hall sensor.
         ) {
-            notifyFoldUpdate(transitionUpdate, lastHingeAngle)
+            notifyFoldUpdate(transitionUpdate, angle)
         }
 
         if (isTransitionInProgress) {
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/companion/java/com/android/server/companion/virtual/OWNERS b/services/companion/java/com/android/server/companion/virtual/OWNERS
index 4fe0592..4b732ac 100644
--- a/services/companion/java/com/android/server/companion/virtual/OWNERS
+++ b/services/companion/java/com/android/server/companion/virtual/OWNERS
@@ -2,7 +2,9 @@
 
 set noparent
 
-marvinramin@google.com
 vladokom@google.com
+marvinramin@google.com
+caen@google.com
+biswarupp@google.com
 ogunwale@google.com
 michaelwr@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index f0cc09f..22ec790 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -73,6 +73,7 @@
 import static android.media.audio.Flags.roForegroundAudioControl;
 import static android.os.Process.THREAD_GROUP_BACKGROUND;
 import static android.os.Process.THREAD_GROUP_DEFAULT;
+import static android.os.Process.THREAD_GROUP_FOREGROUND_WINDOW;
 import static android.os.Process.THREAD_GROUP_RESTRICTED;
 import static android.os.Process.THREAD_GROUP_TOP_APP;
 import static android.os.Process.THREAD_PRIORITY_DISPLAY;
@@ -116,6 +117,7 @@
 import static com.android.server.am.ProcessList.PREVIOUS_APP_ADJ;
 import static com.android.server.am.ProcessList.SCHED_GROUP_BACKGROUND;
 import static com.android.server.am.ProcessList.SCHED_GROUP_DEFAULT;
+import static com.android.server.am.ProcessList.SCHED_GROUP_FOREGROUND_WINDOW;
 import static com.android.server.am.ProcessList.SCHED_GROUP_RESTRICTED;
 import static com.android.server.am.ProcessList.SCHED_GROUP_TOP_APP;
 import static com.android.server.am.ProcessList.SCHED_GROUP_TOP_APP_BOUND;
@@ -1731,6 +1733,11 @@
                 // The recently used non-top visible freeform app.
                 schedGroup = SCHED_GROUP_TOP_APP;
                 mAdjType = "perceptible-freeform-activity";
+            } else if ((flags
+                    & WindowProcessController.ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE) != 0) {
+                // Currently the only case is from freeform apps which are not close to top.
+                schedGroup = SCHED_GROUP_FOREGROUND_WINDOW;
+                mAdjType = "vis-multi-window-activity";
             }
             foregroundActivities = true;
             mHasVisibleActivities = true;
@@ -3438,6 +3445,9 @@
                 case SCHED_GROUP_RESTRICTED:
                     processGroup = THREAD_GROUP_RESTRICTED;
                     break;
+                case SCHED_GROUP_FOREGROUND_WINDOW:
+                    processGroup = THREAD_GROUP_FOREGROUND_WINDOW;
+                    break;
                 default:
                     processGroup = THREAD_GROUP_DEFAULT;
                     break;
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index 21842db..fb1c2e9 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -331,7 +331,7 @@
         void forEachNewNode(int slot, @NonNull Consumer<OomAdjusterArgs> callback) {
             ProcessRecordNode node = mLastNode[slot].mNext;
             final ProcessRecordNode tail = mProcessRecordNodes[slot].TAIL;
-            while (node != tail) {
+            while (node != null && node != tail) {
                 mTmpOomAdjusterArgs.mApp = node.mApp;
                 if (node.mApp == null) {
                     // TODO(b/336178916) - Temporary logging for root causing b/336178916.
@@ -365,7 +365,9 @@
                 }
                 // Save the next before calling callback, since that may change the node.mNext.
                 final ProcessRecordNode next = node.mNext;
-                callback.accept(mTmpOomAdjusterArgs);
+                if (mTmpOomAdjusterArgs.mApp != null) {
+                    callback.accept(mTmpOomAdjusterArgs);
+                }
                 // There are couple of cases:
                 // a) The current node is moved to another slot
                 //    - for this case, we'd need to keep using the "next" node.
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index cb918a0..00250b4 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -303,6 +303,9 @@
     // Activity manager's version of Process.THREAD_GROUP_TOP_APP
     // Disambiguate between actual top app and processes bound to the top app
     static final int SCHED_GROUP_TOP_APP_BOUND = 4;
+    // Activity manager's version of Process.THREAD_GROUP_FOREGROUND_WINDOW
+    // The priority is like between default and top-app.
+    static final int SCHED_GROUP_FOREGROUND_WINDOW = 5;
 
     // The minimum number of cached apps we want to be able to keep around,
     // without empty apps being able to push them out of memory.
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/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
index ea240c7..9d04682 100644
--- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
+++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
@@ -196,12 +196,7 @@
                 File signatureFile = new File(dir, FONT_SIGNATURE_FILE);
                 if (!signatureFile.exists()) {
                     Slog.i(TAG, "The signature file is missing.");
-                    if (com.android.text.flags.Flags.fixFontUpdateFailure()) {
-                        return;
-                    } else {
-                        FileUtils.deleteContentsAndDir(dir);
-                        continue;
-                    }
+                    return;
                 }
                 byte[] signature;
                 try {
@@ -226,39 +221,33 @@
 
                 FontFileInfo fontFileInfo = validateFontFile(fontFile, signature);
                 if (fontConfig == null) {
-                    if (com.android.text.flags.Flags.fixFontUpdateFailure()) {
-                        // Use preinstalled font config for checking revision number.
-                        fontConfig = mConfigSupplier.apply(Collections.emptyMap());
-                    } else {
-                        fontConfig = getSystemFontConfig();
-                    }
+                    // Use preinstalled font config for checking revision number.
+                    fontConfig = mConfigSupplier.apply(Collections.emptyMap());
                 }
                 addFileToMapIfSameOrNewer(fontFileInfo, fontConfig, true /* deleteOldFile */);
             }
 
-            if (com.android.text.flags.Flags.fixFontUpdateFailure()) {
-                // Treat as error if post script name of font family was not installed.
-                for (int i = 0; i < config.fontFamilies.size(); ++i) {
-                    FontUpdateRequest.Family family = config.fontFamilies.get(i);
-                    for (int j = 0; j < family.getFonts().size(); ++j) {
-                        FontUpdateRequest.Font font = family.getFonts().get(j);
-                        if (mFontFileInfoMap.containsKey(font.getPostScriptName())) {
-                            continue;
-                        }
-
-                        if (fontConfig == null) {
-                            fontConfig = mConfigSupplier.apply(Collections.emptyMap());
-                        }
-
-                        if (getFontByPostScriptName(font.getPostScriptName(), fontConfig) != null) {
-                            continue;
-                        }
-
-                        Slog.e(TAG, "Unknown font that has PostScript name "
-                                + font.getPostScriptName() + " is requested in FontFamily "
-                                + family.getName());
-                        return;
+            // Treat as error if post script name of font family was not installed.
+            for (int i = 0; i < config.fontFamilies.size(); ++i) {
+                FontUpdateRequest.Family family = config.fontFamilies.get(i);
+                for (int j = 0; j < family.getFonts().size(); ++j) {
+                    FontUpdateRequest.Font font = family.getFonts().get(j);
+                    if (mFontFileInfoMap.containsKey(font.getPostScriptName())) {
+                        continue;
                     }
+
+                    if (fontConfig == null) {
+                        fontConfig = mConfigSupplier.apply(Collections.emptyMap());
+                    }
+
+                    if (getFontByPostScriptName(font.getPostScriptName(), fontConfig) != null) {
+                        continue;
+                    }
+
+                    Slog.e(TAG, "Unknown font that has PostScript name "
+                            + font.getPostScriptName() + " is requested in FontFamily "
+                            + family.getName());
+                    return;
                 }
             }
 
@@ -273,9 +262,7 @@
                 mFontFileInfoMap.clear();
                 mLastModifiedMillis = 0;
                 FileUtils.deleteContents(mFilesDir);
-                if (com.android.text.flags.Flags.fixFontUpdateFailure()) {
-                    mConfigFile.delete();
-                }
+                mConfigFile.delete();
             }
         }
     }
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/VibratorHelper.java b/services/core/java/com/android/server/notification/VibratorHelper.java
index fbe7772..f54c1f7 100644
--- a/services/core/java/com/android/server/notification/VibratorHelper.java
+++ b/services/core/java/com/android/server/notification/VibratorHelper.java
@@ -218,10 +218,16 @@
      * @param uri {@code Uri} an uri including query parameter "vibraiton_uri"
      */
     public @Nullable VibrationEffect createVibrationEffectFromSoundUri(Uri uri) {
-        if (uri == null) {
+        if (uri == null || uri.isOpaque()) {
             return null;
         }
-        return Utils.parseVibrationEffect(mVibrator, Utils.getVibrationUri(uri));
+
+        try {
+            return Utils.parseVibrationEffect(mVibrator, Utils.getVibrationUri(uri));
+        } catch (Exception e) {
+            Slog.e(TAG, "Failed to get vibration effect: ", e);
+        }
+        return null;
     }
 
     /** Returns if a given vibration can be played by the vibrator that does notification buzz. */
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/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/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 638e92f..42ea5a8 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -28,6 +28,7 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.Display.INVALID_DISPLAY;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
@@ -57,6 +58,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.internal.util.function.pooled.PooledPredicate;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.wm.LaunchParamsController.LaunchParams;
 
 import java.io.PrintWriter;
@@ -1761,10 +1763,10 @@
      * @return last reparented root task, or {@code null} if the root tasks had to be destroyed.
      */
     Task remove() {
+        final TaskDisplayArea toDisplayArea = getReparentToTaskDisplayArea(getFocusedRootTask());
         mPreferredTopFocusableRootTask = null;
         // TODO(b/153090332): Allow setting content removal mode per task display area
         final boolean destroyContentOnRemoval = mDisplayContent.shouldDestroyContentOnRemove();
-        final TaskDisplayArea toDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
         Task lastReparentedRootTask = null;
 
         // Root tasks could be reparented from the removed display area to other display area. After
@@ -1830,6 +1832,41 @@
         return lastReparentedRootTask;
     }
 
+    /**
+     * Returns the {@link TaskDisplayArea} to which root tasks should be reparented.
+     *
+     * <p>In the automotive multi-user multi-display environment where background users have
+     * UI access on their assigned displays (a.k.a. visible background users), it's not allowed to
+     * launch an activity on an unassigned display. If an activity is attempted to launch on an
+     * unassigned display, it throws an exception.
+     * <p>This method determines the appropriate {@link TaskDisplayArea} for reparenting root tasks
+     * when a display is removed, in order to avoid the exception. If the root task is null,
+     * the visible background user is not supported or the user associated with the root task is
+     * not a visible background user, it returns the default {@link TaskDisplayArea} of the default
+     * display. Otherwise, it returns the default {@link TaskDisplayArea} of the main display
+     * assigned to the user.
+     *
+     * @param rootTask The root task whose {@link TaskDisplayArea} needs to be determined.
+     * @return The {@link TaskDisplayArea} where the root tasks should be reparented to.
+     */
+    private TaskDisplayArea getReparentToTaskDisplayArea(Task rootTask) {
+        final TaskDisplayArea defaultTaskDisplayArea =
+                mRootWindowContainer.getDefaultTaskDisplayArea();
+        if (rootTask == null) {
+            return defaultTaskDisplayArea;
+        }
+        UserManagerInternal userManagerInternal = mAtmService.mWindowManager.mUmInternal;
+        if (!userManagerInternal.isVisibleBackgroundFullUser(rootTask.mUserId)) {
+            return defaultTaskDisplayArea;
+        }
+        int toDisplayId = userManagerInternal.getMainDisplayAssignedToUser(rootTask.mUserId);
+        if (toDisplayId == INVALID_DISPLAY) {
+            return defaultTaskDisplayArea;
+        }
+        DisplayContent dc = mRootWindowContainer.getDisplayContent(toDisplayId);
+        return dc != null ? dc.getDefaultTaskDisplayArea() : defaultTaskDisplayArea;
+    }
+
     /** Whether this task display area can request orientation. */
     boolean canSpecifyOrientation(@ScreenOrientation int orientation) {
         // Only allow to specify orientation if this TDA is the last focused one on this logical
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/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 976be4a..30d6f0a 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -326,6 +326,7 @@
     public static final int ACTIVITY_STATE_FLAG_HAS_ACTIVITY_IN_VISIBLE_TASK = 1 << 22;
     public static final int ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN = 1 << 23;
     public static final int ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM = 1 << 24;
+    public static final int ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE = 1 << 25;
     public static final int ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER = 0x0000ffff;
 
     /**
@@ -1293,8 +1294,12 @@
         if (hasResumedFreeform
                 && com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode()
                 // Exclude task layer 1 because it is already the top most.
-                && minTaskLayer > 1 && minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) {
-            stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM;
+                && minTaskLayer > 1) {
+            if (minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) {
+                stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM;
+            } else {
+                stateFlags |= ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE;
+            }
         }
         stateFlags |= minTaskLayer & ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER;
         if (visible) {
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/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 5ec5302..f6ad07d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -62,6 +62,7 @@
 import static com.android.server.am.ProcessList.PREVIOUS_APP_ADJ;
 import static com.android.server.am.ProcessList.SCHED_GROUP_BACKGROUND;
 import static com.android.server.am.ProcessList.SCHED_GROUP_DEFAULT;
+import static com.android.server.am.ProcessList.SCHED_GROUP_FOREGROUND_WINDOW;
 import static com.android.server.am.ProcessList.SCHED_GROUP_RESTRICTED;
 import static com.android.server.am.ProcessList.SCHED_GROUP_TOP_APP;
 import static com.android.server.am.ProcessList.SCHED_GROUP_TOP_APP_BOUND;
@@ -534,6 +535,14 @@
         updateOomAdj(app);
         assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP);
         assertEquals("perceptible-freeform-activity", app.mState.getAdjType());
+
+        doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_VISIBLE
+                | WindowProcessController.ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE)
+                .when(wpc).getActivityStateFlags();
+        updateOomAdj(app);
+        assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ,
+                SCHED_GROUP_FOREGROUND_WINDOW);
+        assertEquals("vis-multi-window-activity", app.mState.getAdjType());
     }
 
     @SuppressWarnings("GuardedBy")
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/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
index 44aa868..a55aa23 100644
--- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
@@ -30,7 +30,6 @@
 import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.system.Os;
@@ -41,8 +40,6 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.text.flags.Flags;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -1106,7 +1103,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void signatureMissingCase_fontFamilyInstalled_fontFamilyInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1126,7 +1122,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void signatureMissingCase_fontFamilyInstalled_fontInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1);
@@ -1146,7 +1141,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void signatureMissingCase_fontFileInstalled_fontFamilyInstallLater() {
         // Install font file, foo.ttf and bar.ttf
         installTestFontFile(2 /* numFonts */, 1 /* version */);
@@ -1166,7 +1160,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void signatureMissingCase_fontFileInstalled_fontFileInstallLater() {
         // Install font file, foo.ttf and bar.ttf
         installTestFontFile(2 /* numFonts */, 1 /* version */);
@@ -1186,7 +1179,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void signatureAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1206,7 +1198,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void signatureAllMissingCase_fontFamilyInstalled_fontInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1226,7 +1217,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void signatureAllMissingCase_fontFileInstalled_fontFamilyInstallLater() {
         // Install font file, foo.ttf
         installTestFontFile(1 /* numFonts */, 1 /* version */);
@@ -1246,7 +1236,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void signatureAllMissingCase_fontFileInstalled_fontFileInstallLater() {
         // Install font file, foo.ttf
         installTestFontFile(1 /* numFonts */, 1 /* version */);
@@ -1266,7 +1255,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontMissingCase_fontFamilyInstalled_fontFamilyInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1286,7 +1274,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontMissingCase_fontFamilyInstalled_fontInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1);
@@ -1306,7 +1293,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontMissingCase_fontFileInstalled_fontFamilyInstallLater() {
         // Install font file, foo.ttf and bar.ttf
         installTestFontFile(2 /* numFonts */, 1 /* version */);
@@ -1326,7 +1312,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontMissingCase_fontFileInstalled_fontFileInstallLater() {
         // Install font file, foo.ttf and bar.ttf
         installTestFontFile(2 /* numFonts */, 1 /* version */);
@@ -1346,7 +1331,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1366,7 +1350,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontAllMissingCase_fontFamilyInstalled_fontInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1386,7 +1369,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontAllMissingCase_fontFileInstalled_fontFamilyInstallLater() {
         // Install font file, foo.ttf
         installTestFontFile(1 /* numFonts */, 1 /* version */);
@@ -1406,7 +1388,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontAllMissingCase_fontFileInstalled_fontFileInstallLater() {
         // Install font file, foo.ttf
         installTestFontFile(1 /* numFonts */, 1 /* version */);
@@ -1426,7 +1407,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontDirAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1446,7 +1426,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontDirAllMissingCase_fontFamilyInstalled_fontInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1466,7 +1445,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontDirAllMissingCase_fontFileInstalled_fontFamilyInstallLater() {
         // Install font file, foo.ttf
         installTestFontFile(1 /* numFonts */, 1 /* version */);
@@ -1486,7 +1464,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void fontDirAllMissingCase_fontFileInstalled_fontFileInstallLater() {
         // Install font file, foo.ttf
         installTestFontFile(1 /* numFonts */, 1 /* version */);
@@ -1506,7 +1483,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void dirContentAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1527,7 +1503,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void dirContentAllMissingCase_fontFamilyInstalled_fontInstallLater() {
         // Install font families, foo.ttf, bar.ttf.
         installTestFontFamilies(1 /* version */);
@@ -1548,7 +1523,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void dirContentAllMissingCase_fontFileInstalled_fontFamilyInstallLater() {
         // Install font file, foo.ttf
         installTestFontFile(1 /* numFonts */, 1 /* version */);
@@ -1569,7 +1543,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE)
     public void dirContentAllMissingCase_fontFileInstalled_fontFileInstallLater() {
         // Install font file, foo.ttf
         installTestFontFile(1 /* numFonts */, 1 /* version */);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java
index 4d2396c..65b4ac1 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java
@@ -104,6 +104,12 @@
     }
 
     @Test
+    public void createVibrationEffectFromSoundUri_opaqueUri() {
+        Uri uri = Uri.parse("a:b#c");
+        assertNull(mVibratorHelper.createVibrationEffectFromSoundUri(uri));
+    }
+
+    @Test
     public void createVibrationEffectFromSoundUri_uriWithoutRequiredQueryParameter() {
         Uri uri = Settings.System.DEFAULT_NOTIFICATION_URI;
         assertNull(mVibratorHelper.createVibrationEffectFromSoundUri(uri));
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index 681b7f2..eac2e92 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -29,7 +29,9 @@
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
+import android.hardware.input.InputManager;
 import android.testing.TestableContext;
+import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -60,13 +62,15 @@
  */
 @RunWith(AndroidJUnit4.class)
 public class TouchpadDebugViewTest {
-    private static final int TOUCHPAD_DEVICE_ID = 6;
+    private static final int TOUCHPAD_DEVICE_ID = 60;
 
     private TouchpadDebugView mTouchpadDebugView;
     private WindowManager.LayoutParams mWindowLayoutParams;
 
     @Mock
     WindowManager mWindowManager;
+    @Mock
+    InputManager mInputManager;
 
     Rect mWindowBounds;
     WindowMetrics mWindowMetrics;
@@ -79,12 +83,21 @@
         mTestableContext = new TestableContext(context);
 
         mTestableContext.addMockSystemService(WindowManager.class, mWindowManager);
+        mTestableContext.addMockSystemService(InputManager.class, mInputManager);
 
         mWindowBounds = new Rect(0, 0, 2560, 1600);
         mWindowMetrics = new WindowMetrics(mWindowBounds, new WindowInsets(mWindowBounds), 1.0f);
 
         when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
 
+        InputDevice inputDevice = new InputDevice.Builder()
+                .setId(TOUCHPAD_DEVICE_ID)
+                .setSources(InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE)
+                .setName("Test Device " + TOUCHPAD_DEVICE_ID)
+                .build();
+
+        when(mInputManager.getInputDevice(TOUCHPAD_DEVICE_ID)).thenReturn(inputDevice);
+
         mTouchpadDebugView = new TouchpadDebugView(mTestableContext, TOUCHPAD_DEVICE_ID,
                 new TouchpadHardwareProperties.Builder(0f, 0f, 500f,
                         500f, 45f, 47f, -4f, 5f, (short) 10, true,