Merge "MotionEvent: Ensure addBatch works in the current coordinate space" into main
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb
index a40adac..82ef3e6 100644
--- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb
+++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb
@@ -1,6 +1,6 @@
 drops {
   android_build_drop {
-    build_id: "10093150"
+    build_id: "11947186"
     target: "CtsShim"
     source_file: "aosp_arm64/CtsShimPriv.apk"
   }
@@ -8,7 +8,7 @@
   version: ""
   version_group: ""
   git_project: "platform/frameworks/base"
-  git_branch: "udc-dev"
+  git_branch: "main"
   transform: TRANSFORM_NONE
   transform_options {
   }
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb
index 96444ba..7d0e5d7 100644
--- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb
+++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb
@@ -1,6 +1,6 @@
 drops {
   android_build_drop {
-    build_id: "10093150"
+    build_id: "11947186"
     target: "CtsShim"
     source_file: "aosp_arm64/CtsShim.apk"
   }
@@ -8,7 +8,7 @@
   version: ""
   version_group: ""
   git_project: "platform/frameworks/base"
-  git_branch: "udc-dev"
+  git_branch: "main"
   transform: TRANSFORM_NONE
   transform_options {
   }
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb
index 4d6f8ed..be32060 100644
--- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb
+++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb
@@ -1,6 +1,6 @@
 drops {
   android_build_drop {
-    build_id: "10093150"
+    build_id: "11947186"
     target: "CtsShim"
     source_file: "aosp_x86_64/CtsShimPriv.apk"
   }
@@ -8,7 +8,7 @@
   version: ""
   version_group: ""
   git_project: "platform/frameworks/base"
-  git_branch: "udc-dev"
+  git_branch: "main"
   transform: TRANSFORM_NONE
   transform_options {
   }
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb
index bfd6788..1a6448a 100644
--- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb
+++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb
@@ -1,6 +1,6 @@
 drops {
   android_build_drop {
-    build_id: "10093150"
+    build_id: "11947186"
     target: "CtsShim"
     source_file: "aosp_x86_64/CtsShim.apk"
   }
@@ -8,7 +8,7 @@
   version: ""
   version_group: ""
   git_project: "platform/frameworks/base"
-  git_branch: "udc-dev"
+  git_branch: "main"
   transform: TRANSFORM_NONE
   transform_options {
   }
diff --git a/core/java/android/app/wearable/WearableSensingManager.java b/core/java/android/app/wearable/WearableSensingManager.java
index b2b14ce..d28969d 100644
--- a/core/java/android/app/wearable/WearableSensingManager.java
+++ b/core/java/android/app/wearable/WearableSensingManager.java
@@ -449,7 +449,7 @@
      * Consumer<android.service.voice.HotwordAudioStream>}, the system will check whether the {@link
      * android.service.voice.VoiceInteractionService} at that time is {@code
      * targetVisComponentName}. If not, the system will call {@link
-     * WearableSensingService#onActiveHotwordAudioStopRequested()} and will not forward the audio
+     * WearableSensingService#onStopHotwordAudioStream()} and will not forward the audio
      * data to the current {@link android.service.voice.HotwordDetectionService} nor {@link
      * android.service.voice.VoiceInteractionService}. The system will not send a status code to
      * {@code statusConsumer} regarding the {@code targetVisComponentName} check. The caller is
@@ -464,9 +464,9 @@
      * continue to use the previous consumers after receiving a new one.
      *
      * <p>If the {@code statusConsumer} returns {@link STATUS_SUCCESS}, the caller should call
-     * {@link #stopListeningForHotword(Executor, Consumer)} when it wants the wearable to stop
+     * {@link #stopHotwordRecognition(Executor, Consumer)} when it wants the wearable to stop
      * listening for hotword. If the {@code statusConsumer} returns any other status code, a failure
-     * has occurred and calling {@link #stopListeningForHotword(Executor, Consumer)} is not
+     * has occurred and calling {@link #stopHotwordRecognition(Executor, Consumer)} is not
      * required. The system will not retry listening automatically. The caller should call this
      * method again if they want to retry.
      *
diff --git a/core/java/android/service/wearable/WearableSensingService.java b/core/java/android/service/wearable/WearableSensingService.java
index ac22e70..3735c43 100644
--- a/core/java/android/service/wearable/WearableSensingService.java
+++ b/core/java/android/service/wearable/WearableSensingService.java
@@ -398,8 +398,8 @@
     /**
      * Called when a data request observer is registered. Each request must not be larger than
      * {@link WearableSensingDataRequest#getMaxRequestSize()}. In addition, at most {@link
-     * WearableSensingDataRequester#getRateLimit()} requests can be sent every rolling {@link
-     * WearableSensingDataRequester#getRateLimitWindowSize()}. Requests that are too large or too
+     * WearableSensingDataRequest#getRateLimit()} requests can be sent every rolling {@link
+     * WearableSensingDataRequest#getRateLimitWindowSize()}. Requests that are too large or too
      * frequent will be dropped by the system. See {@link
      * WearableSensingDataRequester#requestData(WearableSensingDataRequest, Consumer)} for details
      * about the status code returned for each request.
@@ -442,7 +442,7 @@
      * @param packageName The package name of the app that will receive the requests sent to the
      *     dataRequester.
      * @param dataRequester A handle to the observer to be unregistered. It is the exact same
-     *     instance provided in a previous {@link #onDataRequestConsumerRegistered(int, String,
+     *     instance provided in a previous {@link #onDataRequestObserverRegistered(int, String,
      *     WearableSensingDataRequester, Consumer)} invocation.
      * @param statusConsumer the consumer for the status of the data request observer
      *     unregistration. This is different from the status for each data request.
@@ -469,7 +469,7 @@
      * in which case it should return the corresponding status code.
      *
      * <p>The implementation should also store the {@code statusConsumer}. If the wearable stops
-     * listening for hotword for any reason other than {@link #onStopListeningForHotword(Consumer)}
+     * listening for hotword for any reason other than {@link #onStopHotwordRecognition(Consumer)}
      * being invoked, it should send an appropriate status code listed in {@link
      * WearableSensingManager} to {@code statusConsumer}. If the error condition cannot be described
      * by any of those status codes, it should send a {@link WearableSensingManager#STATUS_UNKNOWN}.
@@ -514,11 +514,11 @@
 
     /**
      * Called when hotword audio data sent to the {@code hotwordAudioConsumer} in {@link
-     * #onStartListeningForHotword(Consumer, Consumer)} is accepted by the
+     * #onStartHotwordRecognition(Consumer, Consumer)} is accepted by the
      * {@link android.service.voice.HotwordDetectionService} as valid hotword.
      *
      * <p>After the implementation of this class sends the hotword audio data to the {@code
-     * hotwordAudioConsumer} in {@link #onStartListeningForHotword(Consumer,
+     * hotwordAudioConsumer} in {@link #onStartHotwordRecognition(Consumer,
      * Consumer)}, the system will forward the data into {@link
      * android.service.voice.HotwordDetectionService} (which runs in an isolated process) for
      * second-stage hotword detection. If accepted as valid hotword there, this method will be
@@ -545,7 +545,7 @@
      *
      * <p>This method is expected to be overridden by a derived class. The implementation should
      * stop sending hotword audio data to the {@code hotwordAudioConsumer} in {@link
-     * #onStartListeningForHotword(Consumer, Consumer)}
+     * #onStartHotwordRecognition(Consumer, Consumer)}
      */
     @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
     @BinderThread
diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java
index f24bc74..57bded7 100644
--- a/core/java/android/window/BackNavigationInfo.java
+++ b/core/java/android/window/BackNavigationInfo.java
@@ -23,6 +23,7 @@
 import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.graphics.Color;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -113,6 +114,8 @@
     private final CustomAnimationInfo mCustomAnimationInfo;
 
     private final int mLetterboxColor;
+    @NonNull
+    private final Rect mTouchableRegion;
 
     /**
      * Create a new {@link BackNavigationInfo} instance.
@@ -128,7 +131,8 @@
             boolean isPrepareRemoteAnimation,
             boolean isAnimationCallback,
             @Nullable CustomAnimationInfo customAnimationInfo,
-            int letterboxColor) {
+            int letterboxColor,
+            @Nullable Rect touchableRegion) {
         mType = type;
         mOnBackNavigationDone = onBackNavigationDone;
         mOnBackInvokedCallback = onBackInvokedCallback;
@@ -136,6 +140,7 @@
         mAnimationCallback = isAnimationCallback;
         mCustomAnimationInfo = customAnimationInfo;
         mLetterboxColor = letterboxColor;
+        mTouchableRegion = new Rect(touchableRegion);
     }
 
     private BackNavigationInfo(@NonNull Parcel in) {
@@ -146,6 +151,7 @@
         mAnimationCallback = in.readBoolean();
         mCustomAnimationInfo = in.readTypedObject(CustomAnimationInfo.CREATOR);
         mLetterboxColor = in.readInt();
+        mTouchableRegion = in.readTypedObject(Rect.CREATOR);
     }
 
     /** @hide */
@@ -158,6 +164,7 @@
         dest.writeBoolean(mAnimationCallback);
         dest.writeTypedObject(mCustomAnimationInfo, flags);
         dest.writeInt(mLetterboxColor);
+        dest.writeTypedObject(mTouchableRegion, flags);
     }
 
     /**
@@ -206,6 +213,16 @@
     public int getLetterboxColor() {
         return mLetterboxColor;
     }
+
+    /**
+     * @return The app window region where the client can handle touch event.
+     * @hide
+     */
+    @NonNull
+    public Rect getTouchableRegion() {
+        return mTouchableRegion;
+    }
+
     /**
      * Callback to be called when the back preview is finished in order to notify the server that
      * it can clean up the resources created for the animation.
@@ -402,6 +419,7 @@
         private boolean mAnimationCallback = false;
 
         private int mLetterboxColor = Color.TRANSPARENT;
+        private Rect mTouchableRegion;
 
         /**
          * @see BackNavigationInfo#getType()
@@ -478,6 +496,13 @@
         }
 
         /**
+         * @param rect Non-empty for frame of current focus window.
+         */
+        public Builder setTouchableRegion(Rect rect) {
+            mTouchableRegion = new Rect(rect);
+            return this;
+        }
+        /**
          * Builds and returns an instance of {@link BackNavigationInfo}
          */
         public BackNavigationInfo build() {
@@ -486,7 +511,8 @@
                     mPrepareRemoteAnimation,
                     mAnimationCallback,
                     mCustomAnimationInfo,
-                    mLetterboxColor);
+                    mLetterboxColor,
+                    mTouchableRegion);
         }
     }
 }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index b7f6f36..4ca64e7 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -435,7 +435,16 @@
         }
 
         @Override
-        public void onBackProgressed(BackMotionEvent backEvent) { }
+        public void onBackProgressed(BackMotionEvent backEvent) {
+            // This is only called in some special cases such as when activity embedding is active
+            // or when the activity is letterboxed. Otherwise mProgressAnimator#onBackProgressed is
+            // called from WindowOnBackInvokedDispatcher#onMotionEvent
+            mHandler.post(() -> {
+                if (getBackAnimationCallback() != null) {
+                    mProgressAnimator.onBackProgressed(backEvent);
+                }
+            });
+        }
 
         @Override
         public void onBackCancelled() {
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 0119289..0fd21f3 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
@@ -30,6 +30,7 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.database.ContentObserver;
+import android.graphics.Rect;
 import android.hardware.input.InputManager;
 import android.net.Uri;
 import android.os.Bundle;
@@ -47,6 +48,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
+import android.view.WindowManager;
 import android.window.BackAnimationAdapter;
 import android.window.BackEvent;
 import android.window.BackMotionEvent;
@@ -119,6 +121,9 @@
     private final ShellCommandHandler mShellCommandHandler;
     private final ShellExecutor mShellExecutor;
     private final Handler mBgHandler;
+    private final WindowManager mWindowManager;
+    @VisibleForTesting
+    final Rect mTouchableArea = new Rect();
 
     /**
      * Tracks the current user back gesture.
@@ -222,6 +227,8 @@
         mShellBackAnimationRegistry = shellBackAnimationRegistry;
         mLatencyTracker = LatencyTracker.getInstance(mContext);
         mShellCommandHandler = shellCommandHandler;
+        mWindowManager = context.getSystemService(WindowManager.class);
+        updateTouchableArea();
     }
 
     private void onInit() {
@@ -283,6 +290,11 @@
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         mShellBackAnimationRegistry.onConfigurationChanged(newConfig);
+        updateTouchableArea();
+    }
+
+    private void updateTouchableArea() {
+        mTouchableArea.set(mWindowManager.getCurrentWindowMetrics().getBounds());
     }
 
     @Override
@@ -416,11 +428,18 @@
         if (!shouldDispatchToAnimator && mActiveCallback != null) {
             mCurrentTracker.updateStartLocation();
             tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
+            if (mBackNavigationInfo != null && !isAppProgressGenerationAllowed()) {
+                tryPilferPointers();
+            }
         } else if (shouldDispatchToAnimator) {
             tryPilferPointers();
         }
     }
 
+    private boolean isAppProgressGenerationAllowed() {
+        return mBackNavigationInfo.getTouchableRegion().equals(mTouchableArea);
+    }
+
     /**
      * Called when a new motion event needs to be transferred to this
      * {@link BackAnimationController}
@@ -536,6 +555,9 @@
             // App is handling back animation. Cancel system animation latency tracking.
             cancelLatencyTracking();
             tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
+            if (!isAppProgressGenerationAllowed()) {
+                tryPilferPointers();
+            }
         }
     }
 
@@ -642,7 +664,8 @@
 
     private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
             BackMotionEvent backEvent) {
-        if (callback == null || !shouldDispatchToAnimator()) {
+        if (callback == null || (!shouldDispatchToAnimator() && mBackNavigationInfo != null
+                && isAppProgressGenerationAllowed())) {
             return;
         }
         try {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl
index b5f25433f..e779879 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl
@@ -53,9 +53,11 @@
      * @param destinationBounds the destination bounds the PiP window lands into
      * @param overlay an optional overlay to fade out after entering PiP
      * @param appBounds the bounds used to set the buffer size of the optional content overlay
+     * @param sourceRectHint the bounds to show in the transition to PiP
      */
     oneway void stopSwipePipToHome(int taskId, in ComponentName componentName,
-            in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds) = 2;
+            in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds,
+            in Rect sourceRectHint) = 2;
 
     /**
      * Notifies the swiping Activity to PiP onto home transition is aborted
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index 109868d..0713a79 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -204,7 +204,7 @@
                 FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__KEYBOARD_SHORTCUT_EXIT
             ),
             RETURN_HOME_OR_OVERVIEW(
-                FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME
+                FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__RETURN_HOME_OR_OVERVIEW
             ),
             TASK_FINISHED(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED),
             SCREEN_OFF(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 04dd0ef..8c9206c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -373,6 +373,10 @@
     @NonNull
     final Rect mAppBounds = new Rect();
 
+    /** The source rect hint from stopSwipePipToHome(). */
+    @Nullable
+    private Rect mSwipeSourceRectHint;
+
     public PipTaskOrganizer(Context context,
             @NonNull SyncTransactionQueue syncTransactionQueue,
             @NonNull PipTransitionState pipTransitionState,
@@ -504,7 +508,7 @@
      * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards.
      */
     public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
-            SurfaceControl overlay, Rect appBounds) {
+            SurfaceControl overlay, Rect appBounds, Rect sourceRectHint) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "stopSwipePipToHome: %s, stat=%s", componentName, mPipTransitionState);
         // do nothing if there is no startSwipePipToHome being called before
@@ -513,6 +517,7 @@
         }
         mPipBoundsState.setBounds(destinationBounds);
         setContentOverlay(overlay, appBounds);
+        mSwipeSourceRectHint = sourceRectHint;
         if (ENABLE_SHELL_TRANSITIONS && overlay != null) {
             // With Shell transition, the overlay was attached to the remote transition leash, which
             // will be removed when the current transition is finished, so we need to reparent it
@@ -529,6 +534,20 @@
         }
     }
 
+    /**
+     * Returns non-null Rect if the pip is entering from swipe-to-home with a specified source hint.
+     * This also consumes the rect hint.
+     */
+    @Nullable
+    Rect takeSwipeSourceRectHint() {
+        final Rect sourceRectHint = mSwipeSourceRectHint;
+        if (sourceRectHint == null || sourceRectHint.isEmpty()) {
+            return null;
+        }
+        mSwipeSourceRectHint = null;
+        return mPipTransitionState.getInSwipePipToHomeTransition() ? sourceRectHint : null;
+    }
+
     private void mayRemoveContentOverlay(SurfaceControl overlay) {
         final WeakReference<SurfaceControl> overlayRef = new WeakReference<>(overlay);
         final long timeoutDuration = (mEnterAnimationDuration
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 5ee6f6b..dbf18fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -1004,8 +1004,11 @@
         final Rect currentBounds = pipChange.getStartAbsBounds();
 
         int rotationDelta = deltaRotation(startRotation, endRotation);
-        Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect(
-                taskInfo.pictureInPictureParams, currentBounds, destinationBounds);
+        Rect sourceHintRect = mPipOrganizer.takeSwipeSourceRectHint();
+        if (sourceHintRect == null) {
+            sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect(
+                    taskInfo.pictureInPictureParams, currentBounds, destinationBounds);
+        }
         if (rotationDelta != Surface.ROTATION_0
                 && endRotation != mPipDisplayLayoutState.getRotation()) {
             // Computes the destination bounds in new rotation.
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 de105c0..8c4bf76 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
@@ -1001,9 +1001,9 @@
     }
 
     private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
-            SurfaceControl overlay, Rect appBounds) {
+            SurfaceControl overlay, Rect appBounds, Rect sourceRectHint) {
         mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay,
-                appBounds);
+                appBounds, sourceRectHint);
     }
 
     private void abortSwipePipToHome(int taskId, ComponentName componentName) {
@@ -1291,13 +1291,15 @@
 
         @Override
         public void stopSwipePipToHome(int taskId, ComponentName componentName,
-                Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
+                Rect destinationBounds, SurfaceControl overlay, Rect appBounds,
+                Rect sourceRectHint) {
             if (overlay != null) {
                 overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome");
             }
             executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome",
                     (controller) -> controller.stopSwipePipToHome(
-                            taskId, componentName, destinationBounds, overlay, appBounds));
+                            taskId, componentName, destinationBounds, overlay, appBounds,
+                            sourceRectHint));
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 1846720..fc0d36d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -285,7 +285,8 @@
     }
 
     private void onSwipePipToHomeAnimationStart(int taskId, ComponentName componentName,
-            Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
+            Rect destinationBounds, SurfaceControl overlay, Rect appBounds,
+            Rect sourceRectHint) {
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "onSwipePipToHomeAnimationStart: %s", componentName);
         Bundle extra = new Bundle();
@@ -409,13 +410,15 @@
 
         @Override
         public void stopSwipePipToHome(int taskId, ComponentName componentName,
-                Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
+                Rect destinationBounds, SurfaceControl overlay, Rect appBounds,
+                Rect sourceRectHint) {
             if (overlay != null) {
                 overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome");
             }
             executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome",
                     (controller) -> controller.onSwipePipToHomeAnimationStart(
-                            taskId, componentName, destinationBounds, overlay, appBounds));
+                            taskId, componentName, destinationBounds, overlay, appBounds,
+                            sourceRectHint));
         }
 
         @Override
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 a4ade1b..01a479f 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
@@ -233,32 +233,7 @@
         }
 
         if (oldRootView != mResult.mRootView) {
-            if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) {
-                mWindowDecorViewHolder = new AppHandleViewHolder(
-                        mResult.mRootView,
-                        mOnCaptionTouchListener,
-                        mOnCaptionButtonClickListener
-                );
-            } else if (mRelayoutParams.mLayoutResId
-                    == R.layout.desktop_mode_app_header) {
-                loadAppInfoIfNeeded();
-                mWindowDecorViewHolder = new AppHeaderViewHolder(
-                        mResult.mRootView,
-                        mOnCaptionTouchListener,
-                        mOnCaptionButtonClickListener,
-                        mOnCaptionLongClickListener,
-                        mOnCaptionGenericMotionListener,
-                        mAppName,
-                        mAppIconBitmap,
-                        () -> {
-                            if (!isMaximizeMenuActive()) {
-                                createMaximizeMenu();
-                            }
-                            return Unit.INSTANCE;
-                        });
-            } else {
-                throw new IllegalArgumentException("Unexpected layout resource id");
-            }
+            mWindowDecorViewHolder = createViewHolder();
         }
         Trace.beginSection("DesktopModeWindowDecoration#relayout-binding");
         mWindowDecorViewHolder.bindData(mTaskInfo);
@@ -269,16 +244,18 @@
             closeMaximizeMenu();
         }
 
-        final boolean isFreeform =
-                taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
-        final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
-        if (!isDragResizeable) {
+        updateDragResizeListener(oldDecorationSurface);
+        updateMaximizeMenu(startT);
+        Trace.endSection(); // DesktopModeWindowDecoration#relayout
+    }
+
+    private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
+        if (!isDragResizable(mTaskInfo)) {
             if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
                 // We still want to track caption bar's exclusion region on a non-resizeable task.
                 updateExclusionRegion();
             }
             closeDragResizeListener();
-            Trace.endSection(); // DesktopModeWindowDecoration#relayout
             return;
         }
 
@@ -312,15 +289,51 @@
                 || !mTaskInfo.positionInParent.equals(mPositionInParent)) {
             updateExclusionRegion();
         }
+    }
 
-        if (isMaximizeMenuActive()) {
-            if (!mTaskInfo.isVisible()) {
-                closeMaximizeMenu();
-            } else {
-                mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT);
-            }
+    private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+        final boolean isFreeform =
+                taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
+        return isFreeform && taskInfo.isResizeable;
+    }
+
+    private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
+        if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) {
+            return;
         }
-        Trace.endSection(); // DesktopModeWindowDecoration#relayout
+        if (!mTaskInfo.isVisible()) {
+            closeMaximizeMenu();
+        } else {
+            mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT);
+        }
+    }
+
+    private WindowDecorationViewHolder createViewHolder() {
+        if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) {
+            return new AppHandleViewHolder(
+                    mResult.mRootView,
+                    mOnCaptionTouchListener,
+                    mOnCaptionButtonClickListener
+            );
+        } else if (mRelayoutParams.mLayoutResId
+                == R.layout.desktop_mode_app_header) {
+            loadAppInfoIfNeeded();
+            return new AppHeaderViewHolder(
+                    mResult.mRootView,
+                    mOnCaptionTouchListener,
+                    mOnCaptionButtonClickListener,
+                    mOnCaptionLongClickListener,
+                    mOnCaptionGenericMotionListener,
+                    mAppName,
+                    mAppIconBitmap,
+                    () -> {
+                        if (!isMaximizeMenuActive()) {
+                            createMaximizeMenu();
+                        }
+                        return Unit.INSTANCE;
+                    });
+        }
+        throw new IllegalArgumentException("Unexpected layout resource id");
     }
 
     @VisibleForTesting
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index a08f97c..b9532dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -213,13 +213,39 @@
             return;
         }
 
+        inflateIfNeeded(params, wct, rootView, oldLayoutResId, outResult);
+        if (outResult.mRootView == null) {
+            // Didn't manage to create a root view, early out.
+            return;
+        }
+        rootView = null; // Clear it just in case we use it accidentally
+
+        updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId);
+
+        final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds();
+        outResult.mWidth = taskBounds.width();
+        outResult.mHeight = taskBounds.height();
+        outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
+        final Resources resources = mDecorWindowContext.getResources();
+        outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
+        outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL
+                ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width();
+        outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2;
+
+        updateDecorationContainerSurface(startT, outResult);
+        updateCaptionContainerSurface(startT, outResult);
+        updateCaptionInsets(params, wct, outResult, taskBounds);
+        updateTaskSurface(params, startT, finishT, outResult);
+        updateViewHost(params, startT, outResult);
+    }
+
+    private void inflateIfNeeded(RelayoutParams params, WindowContainerTransaction wct,
+            T rootView, int oldLayoutResId, RelayoutResult<T> outResult) {
         if (rootView == null && params.mLayoutResId == 0) {
             throw new IllegalArgumentException("layoutResId and rootView can't both be invalid.");
         }
 
         outResult.mRootView = rootView;
-        rootView = null; // Clear it just in case we use it accidentally
-
         final int oldDensityDpi = mWindowDecorConfig != null
                 ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED;
         final int oldNightMode =  mWindowDecorConfig != null
@@ -253,25 +279,17 @@
             outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
                     .inflate(params.mLayoutResId, null);
         }
+    }
 
-        updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId);
-
-        final Resources resources = mDecorWindowContext.getResources();
-        final Configuration taskConfig = mTaskInfo.getConfiguration();
-        final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
-        final boolean isFullscreen = taskConfig.windowConfiguration.getWindowingMode()
-                == WINDOWING_MODE_FULLSCREEN;
-        outResult.mWidth = taskBounds.width();
-        outResult.mHeight = taskBounds.height();
-
-        // DecorationContainerSurface
+    private void updateDecorationContainerSurface(
+            SurfaceControl.Transaction startT, RelayoutResult<T> outResult) {
         if (mDecorationContainerSurface == null) {
             final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
             mDecorationContainerSurface = builder
                     .setName("Decor container of Task=" + mTaskInfo.taskId)
                     .setContainerLayer()
                     .setParent(mTaskSurface)
-                    .setCallsite("WindowDecoration.relayout_1")
+                    .setCallsite("WindowDecoration.updateDecorationContainerSurface")
                     .build();
 
             startT.setTrustedOverlay(mDecorationContainerSurface, true)
@@ -281,101 +299,101 @@
 
         startT.setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight)
                 .show(mDecorationContainerSurface);
+    }
 
-        // CaptionContainerSurface, CaptionWindowManager
+    private void updateCaptionContainerSurface(
+            SurfaceControl.Transaction startT, RelayoutResult<T> outResult) {
         if (mCaptionContainerSurface == null) {
             final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
             mCaptionContainerSurface = builder
                     .setName("Caption container of Task=" + mTaskInfo.taskId)
                     .setContainerLayer()
                     .setParent(mDecorationContainerSurface)
-                    .setCallsite("WindowDecoration.relayout_2")
+                    .setCallsite("WindowDecoration.updateCaptionContainerSurface")
                     .build();
         }
 
-        outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
-        outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL
-                ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width();
-        outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2;
-
         startT.setWindowCrop(mCaptionContainerSurface, outResult.mCaptionWidth,
                         outResult.mCaptionHeight)
                 .setPosition(mCaptionContainerSurface, outResult.mCaptionX, 0 /* y */)
                 .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER)
                 .show(mCaptionContainerSurface);
+    }
 
-        outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
-
-        // Caption insets
-        if (mIsCaptionVisible) {
-            // Caption inset is the full width of the task with the |captionHeight| and
-            // positioned at the top of the task bounds, also in absolute coordinates.
-            // So just reuse the task bounds and adjust the bottom coordinate.
-            final Rect captionInsetsRect = new Rect(taskBounds);
-            captionInsetsRect.bottom = captionInsetsRect.top + outResult.mCaptionHeight;
-
-            // Caption bounding rectangles: these are optional, and are used to present finer
-            // insets than traditional |Insets| to apps about where their content is occluded.
-            // These are also in absolute coordinates.
-            final Rect[] boundingRects;
-            final int numOfElements = params.mOccludingCaptionElements.size();
-            if (numOfElements == 0) {
-                boundingRects = null;
-            } else {
-                // The customizable region can at most be equal to the caption bar.
-                if (params.hasInputFeatureSpy()) {
-                    outResult.mCustomizableCaptionRegion.set(captionInsetsRect);
-                }
-                boundingRects = new Rect[numOfElements];
-                for (int i = 0; i < numOfElements; i++) {
-                    final OccludingCaptionElement element =
-                            params.mOccludingCaptionElements.get(i);
-                    final int elementWidthPx =
-                            resources.getDimensionPixelSize(element.mWidthResId);
-                    boundingRects[i] =
-                            calculateBoundingRect(element, elementWidthPx, captionInsetsRect);
-                    // Subtract the regions used by the caption elements, the rest is
-                    // customizable.
-                    if (params.hasInputFeatureSpy()) {
-                        outResult.mCustomizableCaptionRegion.op(boundingRects[i],
-                                Region.Op.DIFFERENCE);
-                    }
-                }
-            }
-
-            final WindowDecorationInsets newInsets = new WindowDecorationInsets(
-                    mTaskInfo.token, mOwner, captionInsetsRect, boundingRects);
-            if (!newInsets.equals(mWindowDecorationInsets)) {
-                // Add or update this caption as an insets source.
-                mWindowDecorationInsets = newInsets;
-                mWindowDecorationInsets.addOrUpdate(wct);
-            }
-        } else {
+    private void updateCaptionInsets(RelayoutParams params, WindowContainerTransaction wct,
+            RelayoutResult<T> outResult, Rect taskBounds) {
+        if (!mIsCaptionVisible) {
             if (mWindowDecorationInsets != null) {
                 mWindowDecorationInsets.remove(wct);
                 mWindowDecorationInsets = null;
             }
+            return;
         }
+        // Caption inset is the full width of the task with the |captionHeight| and
+        // positioned at the top of the task bounds, also in absolute coordinates.
+        // So just reuse the task bounds and adjust the bottom coordinate.
+        final Rect captionInsetsRect = new Rect(taskBounds);
+        captionInsetsRect.bottom = captionInsetsRect.top + outResult.mCaptionHeight;
 
-        // Task surface itself
-        float shadowRadius;
-        final Point taskPosition = mTaskInfo.positionInParent;
-        if (isFullscreen) {
-            // Shadow is not needed for fullscreen tasks
-            shadowRadius = 0;
+        // Caption bounding rectangles: these are optional, and are used to present finer
+        // insets than traditional |Insets| to apps about where their content is occluded.
+        // These are also in absolute coordinates.
+        final Rect[] boundingRects;
+        final int numOfElements = params.mOccludingCaptionElements.size();
+        if (numOfElements == 0) {
+            boundingRects = null;
         } else {
-            shadowRadius = loadDimension(resources, params.mShadowRadiusId);
+            // The customizable region can at most be equal to the caption bar.
+            if (params.hasInputFeatureSpy()) {
+                outResult.mCustomizableCaptionRegion.set(captionInsetsRect);
+            }
+            final Resources resources = mDecorWindowContext.getResources();
+            boundingRects = new Rect[numOfElements];
+            for (int i = 0; i < numOfElements; i++) {
+                final OccludingCaptionElement element =
+                        params.mOccludingCaptionElements.get(i);
+                final int elementWidthPx =
+                        resources.getDimensionPixelSize(element.mWidthResId);
+                boundingRects[i] =
+                        calculateBoundingRect(element, elementWidthPx, captionInsetsRect);
+                // Subtract the regions used by the caption elements, the rest is
+                // customizable.
+                if (params.hasInputFeatureSpy()) {
+                    outResult.mCustomizableCaptionRegion.op(boundingRects[i],
+                            Region.Op.DIFFERENCE);
+                }
+            }
         }
 
+        final WindowDecorationInsets newInsets = new WindowDecorationInsets(
+                mTaskInfo.token, mOwner, captionInsetsRect, boundingRects);
+        if (!newInsets.equals(mWindowDecorationInsets)) {
+            // Add or update this caption as an insets source.
+            mWindowDecorationInsets = newInsets;
+            mWindowDecorationInsets.addOrUpdate(wct);
+        }
+    }
+
+    private void updateTaskSurface(RelayoutParams params, SurfaceControl.Transaction startT,
+            SurfaceControl.Transaction finishT, RelayoutResult<T> outResult) {
         if (params.mSetTaskPositionAndCrop) {
+            final Point taskPosition = mTaskInfo.positionInParent;
             startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight);
             finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight)
                     .setPosition(mTaskSurface, taskPosition.x, taskPosition.y);
         }
 
-        startT.setShadowRadius(mTaskSurface, shadowRadius)
-                .show(mTaskSurface);
+        float shadowRadius;
+        if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+            // Shadow is not needed for fullscreen tasks
+            shadowRadius = 0;
+        } else {
+            shadowRadius =
+                    loadDimension(mDecorWindowContext.getResources(), params.mShadowRadiusId);
+        }
+        startT.setShadowRadius(mTaskSurface, shadowRadius).show(mTaskSurface);
         finishT.setShadowRadius(mTaskSurface, shadowRadius);
+
         if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
             if (!DesktopModeStatus.isVeiledResizeEnabled()) {
                 // When fluid resize is enabled, add a background to freeform tasks
@@ -390,7 +408,10 @@
         } else if (!DesktopModeStatus.isVeiledResizeEnabled()) {
             startT.unsetColor(mTaskSurface);
         }
+    }
 
+    private void updateViewHost(RelayoutParams params, SurfaceControl.Transaction onDrawTransaction,
+            RelayoutResult<T> outResult) {
         Trace.beginSection("CaptionViewHostLayout");
         if (mCaptionWindowManager == null) {
             // Put caption under a container surface because ViewRootImpl sets the destination frame
@@ -399,9 +420,7 @@
                     mTaskInfo.getConfiguration(), mCaptionContainerSurface,
                     null /* hostInputToken */);
         }
-
-        // Caption view
-        mCaptionWindowManager.setConfiguration(taskConfig);
+        mCaptionWindowManager.setConfiguration(mTaskInfo.getConfiguration());
         final WindowManager.LayoutParams lp =
                 new WindowManager.LayoutParams(outResult.mCaptionWidth, outResult.mCaptionHeight,
                         TYPE_APPLICATION,
@@ -414,14 +433,14 @@
             mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay,
                     mCaptionWindowManager);
             if (params.mApplyStartTransactionOnDraw) {
-                mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT);
+                mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction);
             }
             mViewHost.setView(outResult.mRootView, lp);
             Trace.endSection();
         } else {
             Trace.beginSection("CaptionViewHostLayout-relayout");
             if (params.mApplyStartTransactionOnDraw) {
-                mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT);
+                mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction);
             }
             mViewHost.relayout(lp);
             Trace.endSection();
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 f6f3aa4..1903586 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
@@ -123,6 +123,7 @@
     private DefaultCrossActivityBackAnimation mDefaultCrossActivityBackAnimation;
     private CrossTaskBackAnimation mCrossTaskBackAnimation;
     private ShellBackAnimationRegistry mShellBackAnimationRegistry;
+    private Rect mTouchableRegion;
 
     @Before
     public void setUp() throws Exception {
@@ -158,6 +159,8 @@
                         mShellCommandHandler);
         mShellInit.init();
         mShellExecutor.flushAll();
+        mTouchableRegion = new Rect(0, 0, 100, 100);
+        mController.mTouchableArea.set(mTouchableRegion);
     }
 
     private void createNavigationInfo(int backType,
@@ -169,7 +172,8 @@
                         .setOnBackNavigationDone(new RemoteCallback((bundle) -> {}))
                         .setOnBackInvokedCallback(mAppCallback)
                         .setPrepareRemoteAnimation(enableAnimation)
-                        .setAnimationCallback(isAnimationCallback);
+                        .setAnimationCallback(isAnimationCallback)
+                        .setTouchableRegion(mTouchableRegion);
 
         createNavigationInfo(builder);
     }
@@ -234,7 +238,8 @@
                     .setType(type)
                     .setOnBackInvokedCallback(mAppCallback)
                     .setPrepareRemoteAnimation(true)
-                    .setOnBackNavigationDone(new RemoteCallback(result)));
+                    .setOnBackNavigationDone(new RemoteCallback(result))
+                    .setTouchableRegion(mTouchableRegion));
             triggerBackGesture();
             simulateRemoteAnimationStart();
             mShellExecutor.flushAll();
@@ -512,7 +517,8 @@
                     .setType(type)
                     .setOnBackInvokedCallback(mAppCallback)
                     .setPrepareRemoteAnimation(true)
-                    .setOnBackNavigationDone(new RemoteCallback(result)));
+                    .setOnBackNavigationDone(new RemoteCallback(result))
+                    .setTouchableRegion(mTouchableRegion));
             triggerBackGesture();
             simulateRemoteAnimationStart();
             mShellExecutor.flushAll();
@@ -543,7 +549,8 @@
         createNavigationInfo(new BackNavigationInfo.Builder()
                 .setType(type)
                 .setOnBackInvokedCallback(mAppCallback)
-                .setOnBackNavigationDone(new RemoteCallback(result)));
+                .setOnBackNavigationDone(new RemoteCallback(result))
+                .setTouchableRegion(mTouchableRegion));
         triggerBackGesture();
         mShellExecutor.flushAll();
         releaseBackGesture();
@@ -570,7 +577,8 @@
         createNavigationInfo(new BackNavigationInfo.Builder()
                 .setType(type)
                 .setOnBackInvokedCallback(mAppCallback)
-                .setOnBackNavigationDone(new RemoteCallback(result)));
+                .setOnBackNavigationDone(new RemoteCallback(result))
+                .setTouchableRegion(mTouchableRegion));
         doMotionEvent(MotionEvent.ACTION_CANCEL, 0);
         mShellExecutor.flushAll();
 
diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp
index 8ea4632..746c280 100644
--- a/native/graphics/jni/Android.bp
+++ b/native/graphics/jni/Android.bp
@@ -111,6 +111,7 @@
             "allocator_may_return_null = 1",
         ],
     },
+    dictionary: "fuzz/imagedecoder_fuzzer.dict",
     host_supported: true,
 }
 
diff --git a/native/graphics/jni/fuzz/fuzz_imagedecoder.cpp b/native/graphics/jni/fuzz/fuzz_imagedecoder.cpp
index 838bf3f..6743997 100644
--- a/native/graphics/jni/fuzz/fuzz_imagedecoder.cpp
+++ b/native/graphics/jni/fuzz/fuzz_imagedecoder.cpp
@@ -15,32 +15,15 @@
  */
 
 #include <android/imagedecoder.h>
-
 #include <binder/IPCThreadState.h>
-#include <stddef.h>
-#include <stdint.h>
-#include <cstdlib>
-#include <memory>
+#include <fuzzer/FuzzedDataProvider.h>
 
 #ifdef PNG_MUTATOR_DEFINE_LIBFUZZER_CUSTOM_MUTATOR
 #include <fuzz/png_mutator.h>
 #endif
 
-struct DecoderDeleter {
-    void operator()(AImageDecoder* decoder) const { AImageDecoder_delete(decoder); }
-};
-
-using DecoderPointer = std::unique_ptr<AImageDecoder, DecoderDeleter>;
-
-static DecoderPointer makeDecoder(const uint8_t* data, size_t size) {
-    AImageDecoder* decoder = nullptr;
-    int result = AImageDecoder_createFromBuffer(data, size, &decoder);
-    if (result != ANDROID_IMAGE_DECODER_SUCCESS) {
-        // This was not a valid image.
-        return nullptr;
-    }
-    return DecoderPointer(decoder);
-}
+constexpr int32_t kMaxDimension = 5000;
+constexpr int32_t kMinDimension = 0;
 
 struct PixelFreer {
     void operator()(void* pixels) const { std::free(pixels); }
@@ -48,41 +31,113 @@
 
 using PixelPointer = std::unique_ptr<void, PixelFreer>;
 
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
-    // Without this call, decoding HEIF may time out on binder IPC calls.
-    android::ProcessState::self()->startThreadPool();
+AImageDecoder* init(const uint8_t* data, size_t size, bool useFileDescriptor) {
+    AImageDecoder* decoder = nullptr;
+    if (useFileDescriptor) {
+        constexpr char testFd[] = "tempFd";
+        int32_t fileDesc = open(testFd, O_RDWR | O_CREAT | O_TRUNC);
+        write(fileDesc, data, size);
+        AImageDecoder_createFromFd(fileDesc, &decoder);
+        close(fileDesc);
+    } else {
+        AImageDecoder_createFromBuffer(data, size, &decoder);
+    }
+    return decoder;
+}
 
-    DecoderPointer decoder = makeDecoder(data, size);
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+    FuzzedDataProvider dataProvider = FuzzedDataProvider(data, size);
+    /**
+     * Use maximum of 80% of buffer for creating decoder and save at least
+     * 20% buffer for fuzzing other APIs
+     */
+    const int32_t dataSize = dataProvider.ConsumeIntegralInRange<int32_t>(0, (size * 80) / 100);
+    std::vector<uint8_t> inputBuffer = dataProvider.ConsumeBytes<uint8_t>(dataSize);
+    AImageDecoder* decoder =
+            init(inputBuffer.data(), inputBuffer.size(), dataProvider.ConsumeBool());
     if (!decoder) {
         return 0;
     }
-
-    const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder.get());
-    int32_t width = AImageDecoderHeaderInfo_getWidth(info);
-    int32_t height = AImageDecoderHeaderInfo_getHeight(info);
-
-    // Set an arbitrary limit on the size of an image. The fuzzer runs with a
-    // limited amount of memory, and keeping this allocation small allows the
-    // fuzzer to continue running to try to find more serious problems. This
-    // size is large enough to hold a photo taken by a current gen phone.
-    constexpr int32_t kMaxDimension = 5000;
-    if (width > kMaxDimension || height > kMaxDimension) {
-        return 0;
+    const AImageDecoderHeaderInfo* headerInfo = AImageDecoder_getHeaderInfo(decoder);
+    AImageDecoderFrameInfo* frameInfo = AImageDecoderFrameInfo_create();
+    int32_t height = AImageDecoderHeaderInfo_getHeight(headerInfo);
+    int32_t width = AImageDecoderHeaderInfo_getWidth(headerInfo);
+    while (dataProvider.remaining_bytes()) {
+        auto invokeImageApi = dataProvider.PickValueInArray<const std::function<void()>>({
+                [&]() {
+                    int32_t testHeight =
+                            dataProvider.ConsumeIntegralInRange<int32_t>(kMinDimension,
+                                                                         kMaxDimension);
+                    int32_t testWidth = dataProvider.ConsumeIntegralInRange<int32_t>(kMinDimension,
+                                                                                     kMaxDimension);
+                    int32_t result = AImageDecoder_setTargetSize(decoder, testHeight, testWidth);
+                    if (result == ANDROID_IMAGE_DECODER_SUCCESS) {
+                        height = testHeight;
+                        width = testWidth;
+                    }
+                },
+                [&]() {
+                    const bool required = dataProvider.ConsumeBool();
+                    AImageDecoder_setUnpremultipliedRequired(decoder, required);
+                },
+                [&]() {
+                    AImageDecoder_setAndroidBitmapFormat(
+                            decoder,
+                            dataProvider.ConsumeIntegralInRange<
+                                    int32_t>(ANDROID_BITMAP_FORMAT_NONE,
+                                             ANDROID_BITMAP_FORMAT_RGBA_1010102) /* format */);
+                },
+                [&]() {
+                    AImageDecoder_setDataSpace(decoder,
+                                               dataProvider
+                                                       .ConsumeIntegral<int32_t>() /* dataspace */);
+                },
+                [&]() {
+                    ARect rect{dataProvider.ConsumeIntegral<int32_t>() /* left */,
+                               dataProvider.ConsumeIntegral<int32_t>() /* top */,
+                               dataProvider.ConsumeIntegral<int32_t>() /* right */,
+                               dataProvider.ConsumeIntegral<int32_t>() /* bottom */};
+                    AImageDecoder_setCrop(decoder, rect);
+                },
+                [&]() { AImageDecoderHeaderInfo_getWidth(headerInfo); },
+                [&]() { AImageDecoderHeaderInfo_getMimeType(headerInfo); },
+                [&]() { AImageDecoderHeaderInfo_getAlphaFlags(headerInfo); },
+                [&]() { AImageDecoderHeaderInfo_getAndroidBitmapFormat(headerInfo); },
+                [&]() {
+                    int32_t tempHeight;
+                    int32_t tempWidth;
+                    AImageDecoder_computeSampledSize(decoder,
+                                                     dataProvider.ConsumeIntegral<
+                                                             int>() /* sampleSize */,
+                                                     &tempWidth, &tempHeight);
+                },
+                [&]() { AImageDecoderHeaderInfo_getDataSpace(headerInfo); },
+                [&]() { AImageDecoder_getRepeatCount(decoder); },
+                [&]() { AImageDecoder_getFrameInfo(decoder, frameInfo); },
+                [&]() { AImageDecoderFrameInfo_getDuration(frameInfo); },
+                [&]() { AImageDecoderFrameInfo_hasAlphaWithinBounds(frameInfo); },
+                [&]() { AImageDecoderFrameInfo_getDisposeOp(frameInfo); },
+                [&]() { AImageDecoderFrameInfo_getBlendOp(frameInfo); },
+                [&]() {
+                    AImageDecoder_setInternallyHandleDisposePrevious(
+                            decoder, dataProvider.ConsumeBool() /* handle */);
+                },
+                [&]() { AImageDecoder_rewind(decoder); },
+                [&]() { AImageDecoder_advanceFrame(decoder); },
+                [&]() {
+                    size_t stride = AImageDecoder_getMinimumStride(decoder);
+                    size_t pixelSize = height * stride;
+                    auto pixels = PixelPointer(std::malloc(pixelSize));
+                    if (!pixels.get()) {
+                        return;
+                    }
+                    AImageDecoder_decodeImage(decoder, pixels.get(), stride, pixelSize);
+                },
+        });
+        invokeImageApi();
     }
 
-    size_t stride = AImageDecoder_getMinimumStride(decoder.get());
-    size_t pixelSize = height * stride;
-    auto pixels = PixelPointer(std::malloc(pixelSize));
-    if (!pixels.get()) {
-        return 0;
-    }
-
-    while (true) {
-        int result = AImageDecoder_decodeImage(decoder.get(), pixels.get(), stride, pixelSize);
-        if (result != ANDROID_IMAGE_DECODER_SUCCESS) break;
-
-        result = AImageDecoder_advanceFrame(decoder.get());
-        if (result != ANDROID_IMAGE_DECODER_SUCCESS) break;
-    }
+    AImageDecoderFrameInfo_delete(frameInfo);
+    AImageDecoder_delete(decoder);
     return 0;
 }
diff --git a/native/graphics/jni/fuzz/imagedecoder_fuzzer.dict b/native/graphics/jni/fuzz/imagedecoder_fuzzer.dict
new file mode 100644
index 0000000..5b54a0e
--- /dev/null
+++ b/native/graphics/jni/fuzz/imagedecoder_fuzzer.dict
@@ -0,0 +1,7 @@
+kw1="\x89\x50\x4E\x47"
+kw2="\xff\xD8\xFF"
+kw4="\x52\x49\x46\x46"
+kw5="\x00\x00\x01\x00"
+kw6="\x47\x49\x46\x08"
+kw7="ftyp"
+kw8="\x04\x00\x00\x00"
diff --git a/packages/CtsShim/apk/arm/CtsShim.apk b/packages/CtsShim/apk/arm/CtsShim.apk
index 38a6d13..5ab190d 100644
--- a/packages/CtsShim/apk/arm/CtsShim.apk
+++ b/packages/CtsShim/apk/arm/CtsShim.apk
Binary files differ
diff --git a/packages/CtsShim/apk/arm/CtsShimPriv.apk b/packages/CtsShim/apk/arm/CtsShimPriv.apk
index bcd9836..51a8c46 100644
--- a/packages/CtsShim/apk/arm/CtsShimPriv.apk
+++ b/packages/CtsShim/apk/arm/CtsShimPriv.apk
Binary files differ
diff --git a/packages/CtsShim/apk/x86/CtsShim.apk b/packages/CtsShim/apk/x86/CtsShim.apk
index 38a6d13..5ab190d 100644
--- a/packages/CtsShim/apk/x86/CtsShim.apk
+++ b/packages/CtsShim/apk/x86/CtsShim.apk
Binary files differ
diff --git a/packages/CtsShim/apk/x86/CtsShimPriv.apk b/packages/CtsShim/apk/x86/CtsShimPriv.apk
index f778904..fcd0273 100644
--- a/packages/CtsShim/apk/x86/CtsShimPriv.apk
+++ b/packages/CtsShim/apk/x86/CtsShimPriv.apk
Binary files differ
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java
index f3ff0fe..717a8ee 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java
@@ -34,6 +34,7 @@
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
 import android.media.MediaRoute2Info;
+import android.os.SystemProperties;
 import android.util.SparseIntArray;
 
 import androidx.annotation.NonNull;
@@ -42,6 +43,7 @@
 import com.android.settingslib.R;
 import com.android.settingslib.media.flags.Flags;
 
+import java.util.Arrays;
 import java.util.Objects;
 
 /** A util class to get the appropriate icon for different device types. */
@@ -50,18 +52,23 @@
     private static final SparseIntArray AUDIO_DEVICE_TO_MEDIA_ROUTE_TYPE = new SparseIntArray();
 
     private final boolean mIsTv;
+    private final boolean mIsTablet;
     private final Context mContext;
     public DeviceIconUtil(@NonNull Context context) {
         mContext = Objects.requireNonNull(context);
         mIsTv =
                 mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)
                         && Flags.enableTvMediaOutputDialog();
+        mIsTablet =
+                Arrays.asList(SystemProperties.get("ro.build.characteristics").split(","))
+                        .contains("tablet");
     }
 
     @VisibleForTesting
     /* package */ DeviceIconUtil(boolean isTv) {
         mContext = null;
         mIsTv = isTv;
+        mIsTablet = false;
     }
 
     /** Returns a drawable for an icon representing the given audioDeviceType. */
@@ -80,12 +87,17 @@
     /** Returns a drawable res ID for an icon representing the given mediaRouteType. */
     @DrawableRes
     public int getIconResIdFromMediaRouteType(@MediaRoute2Info.Type int type) {
-        return mIsTv ? getIconResourceIdForTv(type) : getIconResourceIdForPhone(type);
+        return mIsTv
+                ? getIconResourceIdForTv(type)
+                : getIconResourceIdForPhoneOrTablet(type, mIsTablet);
     }
 
     @SuppressLint("SwitchIntDef")
     @DrawableRes
-    private static int getIconResourceIdForPhone(@MediaRoute2Info.Type int type) {
+    private static int getIconResourceIdForPhoneOrTablet(
+            @MediaRoute2Info.Type int type, boolean isTablet) {
+        int defaultResId = isTablet ? R.drawable.ic_media_tablet : R.drawable.ic_smartphone;
+
         return switch (type) {
             case MediaRoute2Info.TYPE_USB_DEVICE,
                             MediaRoute2Info.TYPE_USB_HEADSET,
@@ -98,7 +110,7 @@
                             MediaRoute2Info.TYPE_HDMI_ARC,
                             MediaRoute2Info.TYPE_HDMI_EARC ->
                     R.drawable.ic_external_display;
-            default -> R.drawable.ic_smartphone; // Includes TYPE_BUILTIN_SPEAKER.
+            default -> defaultResId; // Includes TYPE_BUILTIN_SPEAKER.
         };
     }
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java
index 8edda1a..883640d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.content.Context;
 import android.media.AudioDeviceInfo;
 import android.media.MediaRoute2Info;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -30,6 +31,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowSystemProperties;
 
 @RunWith(RobolectricTestRunner.class)
 public class DeviceIconUtilTest {
@@ -37,9 +40,12 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
+    private Context mContext;
+
     @Before
     public void setup() {
         mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TV_MEDIA_OUTPUT_DIALOG);
+        mContext = RuntimeEnvironment.getApplication();
     }
 
     @Test
@@ -171,6 +177,14 @@
     }
 
     @Test
+    public void getIconResIdFromMediaRouteType_onTablet_builtinSpeaker_isTablet() {
+        ShadowSystemProperties.override("ro.build.characteristics", "tablet");
+        assertThat(new DeviceIconUtil(mContext)
+                .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER))
+                .isEqualTo(R.drawable.ic_media_tablet);
+    }
+
+    @Test
     public void getIconResIdFromMediaRouteType_unsupportedType_isSmartphone() {
         assertThat(new DeviceIconUtil(/* isTv */ false)
                 .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_UNKNOWN))
@@ -178,6 +192,14 @@
     }
 
     @Test
+    public void getIconResIdFromMediaRouteType_onTablet_unsupportedType_isTablet() {
+        ShadowSystemProperties.override("ro.build.characteristics", "tablet");
+        assertThat(new DeviceIconUtil(mContext)
+                .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_UNKNOWN))
+                .isEqualTo(R.drawable.ic_media_tablet);
+    }
+
+    @Test
     public void getIconResIdFromMediaRouteType_tv_unsupportedType_isSpeaker() {
         assertThat(new DeviceIconUtil(/* isTv */ true)
                 .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_UNKNOWN))
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 3cc8431..6001f1f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -19,8 +19,6 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
 import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
-import androidx.compose.foundation.gestures.horizontalDrag
-import androidx.compose.foundation.gestures.verticalDrag
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
@@ -32,7 +30,9 @@
 import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
 import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
+import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
 import androidx.compose.ui.input.pointer.positionChange
+import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.input.pointer.util.addPointerInputChange
 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
@@ -46,6 +46,8 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.util.fastAll
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
 import kotlin.coroutines.cancellation.CancellationException
 import kotlin.math.sign
@@ -236,8 +238,23 @@
         onDragCancel: (controller: DragController) -> Unit,
         swipeDetector: SwipeDetector,
     ) {
-        // Wait for a consumable event in [PointerEventPass.Main] pass
-        val consumablePointer = awaitConsumableEvent().changes.first()
+        val consumablePointer =
+            awaitConsumableEvent {
+                    // We are searching for an event that can be used as the starting point for the
+                    // drag gesture. Our options are:
+                    // - Initial: These events should never be consumed by the MultiPointerDraggable
+                    //   since our ancestors can consume the gesture, but we would eliminate this
+                    //   possibility for our descendants.
+                    // - Main: These events are consumed during the drag gesture, and they are a
+                    //   good place to start if the previous event has not been consumed.
+                    // - Final: If the previous event has been consumed, we can wait for the Main
+                    //   pass to finish. If none of our ancestors were interested in the event, we
+                    //   can wait for an unconsumed event in the Final pass.
+                    val previousConsumed = currentEvent.changes.fastAny { it.isConsumed }
+                    if (previousConsumed) PointerEventPass.Final else PointerEventPass.Main
+                }
+                .changes
+                .first()
 
         var overSlop = 0f
         val drag =
@@ -297,18 +314,22 @@
                 onDrag(controller, drag, overSlop)
 
                 successful =
-                    when (orientation) {
-                        Orientation.Horizontal ->
-                            horizontalDrag(drag.id) {
-                                onDrag(controller, it, it.positionChange().toFloat())
-                                it.consume()
-                            }
-                        Orientation.Vertical ->
-                            verticalDrag(drag.id) {
-                                onDrag(controller, it, it.positionChange().toFloat())
-                                it.consume()
-                            }
-                    }
+                    drag(
+                        initialPointerId = drag.id,
+                        hasDragged = { it.positionChangeIgnoreConsumed().toFloat() != 0f },
+                        onDrag = {
+                            onDrag(controller, it, it.positionChange().toFloat())
+                            it.consume()
+                        },
+                        onIgnoredEvent = {
+                            // We are still dragging an object, but this event is not of interest to
+                            // the caller.
+                            // This event will not trigger the onDrag event, but we will consume the
+                            // event to prevent another pointerInput from interrupting the current
+                            // gesture just because the event was ignored.
+                            it.consume()
+                        },
+                    )
             } catch (t: Throwable) {
                 onDragCancel(controller)
                 throw t
@@ -322,7 +343,9 @@
         }
     }
 
-    private suspend fun AwaitPointerEventScope.awaitConsumableEvent(): PointerEvent {
+    private suspend fun AwaitPointerEventScope.awaitConsumableEvent(
+        pass: () -> PointerEventPass,
+    ): PointerEvent {
         fun canBeConsumed(changes: List<PointerInputChange>): Boolean {
             // All pointers must be:
             return changes.fastAll {
@@ -337,9 +360,7 @@
 
         var event: PointerEvent
         do {
-            // To allow the descendants with the opportunity to consume the event, we wait for it in
-            // the Main pass.
-            event = awaitPointerEvent()
+            event = awaitPointerEvent(pass = pass())
         } while (!canBeConsumed(event.changes))
 
         // We found a consumable event in the Main pass
@@ -352,4 +373,82 @@
             Orientation.Horizontal -> x
         }
     }
+
+    /**
+     * Continues to read drag events until all pointers are up or the drag event is canceled. The
+     * initial pointer to use for driving the drag is [initialPointerId]. [hasDragged] passes the
+     * result whether a change was detected from the drag function or not.
+     *
+     * Whenever the pointer moves, if [hasDragged] returns true, [onDrag] is called; otherwise,
+     * [onIgnoredEvent] is called.
+     *
+     * @return true when gesture ended with all pointers up and false when the gesture was canceled.
+     *
+     * Note: Inspired by DragGestureDetector.kt
+     */
+    private suspend inline fun AwaitPointerEventScope.drag(
+        initialPointerId: PointerId,
+        hasDragged: (PointerInputChange) -> Boolean,
+        onDrag: (PointerInputChange) -> Unit,
+        onIgnoredEvent: (PointerInputChange) -> Unit,
+    ): Boolean {
+        val pointer = currentEvent.changes.fastFirstOrNull { it.id == initialPointerId }
+        val isPointerUp = pointer?.pressed != true
+        if (isPointerUp) {
+            return false // The pointer has already been lifted, so the gesture is canceled
+        }
+        var pointerId = initialPointerId
+        while (true) {
+            val change = awaitDragOrUp(pointerId, hasDragged, onIgnoredEvent) ?: return false
+
+            if (change.isConsumed) {
+                return false
+            }
+
+            if (change.changedToUpIgnoreConsumed()) {
+                return true
+            }
+
+            onDrag(change)
+            pointerId = change.id
+        }
+    }
+
+    /**
+     * Waits for a single drag in one axis, final pointer up, or all pointers are up. When
+     * [initialPointerId] has lifted, another pointer that is down is chosen to be the finger
+     * governing the drag. When the final pointer is lifted, that [PointerInputChange] is returned.
+     * When a drag is detected, that [PointerInputChange] is returned. A drag is only detected when
+     * [hasDragged] returns `true`. Events that should not be captured are passed to
+     * [onIgnoredEvent].
+     *
+     * `null` is returned if there was an error in the pointer input stream and the pointer that was
+     * down was dropped before the 'up' was received.
+     *
+     * Note: Inspired by DragGestureDetector.kt
+     */
+    private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
+        initialPointerId: PointerId,
+        hasDragged: (PointerInputChange) -> Boolean,
+        onIgnoredEvent: (PointerInputChange) -> Unit,
+    ): PointerInputChange? {
+        var pointerId = initialPointerId
+        while (true) {
+            val event = awaitPointerEvent()
+            val dragEvent = event.changes.fastFirstOrNull { it.id == pointerId } ?: return null
+            if (dragEvent.changedToUpIgnoreConsumed()) {
+                val otherDown = event.changes.fastFirstOrNull { it.pressed }
+                if (otherDown == null) {
+                    // This is the last "up"
+                    return dragEvent
+                } else {
+                    pointerId = otherDown.id
+                }
+            } else if (hasDragged(dragEvent)) {
+                return dragEvent
+            } else {
+                onIgnoredEvent(dragEvent)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
index 4bb643f..1a0740b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
@@ -349,6 +349,121 @@
     }
 
     @Test
+    fun multiPointerDuringAnotherGestureWaitAConsumableEventAfterMainPass() {
+        val size = 200f
+        val middle = Offset(size / 2f, size / 2f)
+
+        var verticalStarted = false
+        var verticalDragged = false
+        var verticalStopped = false
+        var horizontalStarted = false
+        var horizontalDragged = false
+        var horizontalStopped = false
+
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            Box(
+                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
+                    .multiPointerDraggable(
+                        orientation = Orientation.Vertical,
+                        enabled = { true },
+                        startDragImmediately = { false },
+                        onDragStarted = { _, _, _ ->
+                            verticalStarted = true
+                            object : DragController {
+                                override fun onDrag(delta: Float) {
+                                    verticalDragged = true
+                                }
+
+                                override fun onStop(velocity: Float, canChangeScene: Boolean) {
+                                    verticalStopped = true
+                                }
+                            }
+                        },
+                    )
+                    .multiPointerDraggable(
+                        orientation = Orientation.Horizontal,
+                        enabled = { true },
+                        startDragImmediately = { false },
+                        onDragStarted = { _, _, _ ->
+                            horizontalStarted = true
+                            object : DragController {
+                                override fun onDrag(delta: Float) {
+                                    horizontalDragged = true
+                                }
+
+                                override fun onStop(velocity: Float, canChangeScene: Boolean) {
+                                    horizontalStopped = true
+                                }
+                            }
+                        },
+                    )
+            )
+        }
+
+        fun startDraggingDown() {
+            rule.onRoot().performTouchInput {
+                down(middle)
+                moveBy(Offset(0f, touchSlop))
+            }
+        }
+
+        fun startDraggingRight() {
+            rule.onRoot().performTouchInput {
+                down(middle)
+                moveBy(Offset(touchSlop, 0f))
+            }
+        }
+
+        fun stopDragging() {
+            rule.onRoot().performTouchInput { up() }
+        }
+
+        fun continueDown() {
+            rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) }
+        }
+
+        fun continueRight() {
+            rule.onRoot().performTouchInput { moveBy(Offset(touchSlop, 0f)) }
+        }
+
+        startDraggingDown()
+        assertThat(verticalStarted).isTrue()
+        assertThat(verticalDragged).isTrue()
+        assertThat(verticalStopped).isFalse()
+
+        // Ignore right swipe, do not interrupt the dragging gesture.
+        continueRight()
+        assertThat(horizontalStarted).isFalse()
+        assertThat(horizontalDragged).isFalse()
+        assertThat(horizontalStopped).isFalse()
+        assertThat(verticalStopped).isFalse()
+
+        stopDragging()
+        assertThat(verticalStopped).isTrue()
+
+        verticalStarted = false
+        verticalDragged = false
+        verticalStopped = false
+
+        startDraggingRight()
+        assertThat(horizontalStarted).isTrue()
+        assertThat(horizontalDragged).isTrue()
+        assertThat(horizontalStopped).isFalse()
+
+        // Ignore down swipe, do not interrupt the dragging gesture.
+        continueDown()
+        assertThat(verticalStarted).isFalse()
+        assertThat(verticalDragged).isFalse()
+        assertThat(verticalStopped).isFalse()
+        assertThat(horizontalStopped).isFalse()
+
+        stopDragging()
+        assertThat(horizontalStopped).isTrue()
+    }
+
+    @Test
     fun multiPointerSwipeDetectorInteraction() {
         val size = 200f
         val middle = Offset(size / 2f, size / 2f)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index f46ca00..61d8216 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -50,6 +50,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.pow
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runTest
 import org.junit.BeforeClass
 import org.junit.Test
@@ -205,8 +206,13 @@
                         pointerCount = if (downWithTwoPointers) 2 else 1,
                     )
                 )
-
-            assertThat(downDestination?.toScene)
+            val downScene by
+                collectLastValue(
+                    downDestination?.let {
+                        kosmos.sceneInteractor.resolveSceneFamily(downDestination.toScene)
+                    } ?: flowOf(null)
+                )
+            assertThat(downScene)
                 .isEqualTo(
                     expectedDownDestination(
                         downFromEdge = downFromEdge,
@@ -223,7 +229,14 @@
                     )
                 )
 
-            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
+            val upScene by
+                collectLastValue(
+                    destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene?.let { scene ->
+                        kosmos.sceneInteractor.resolveSceneFamily(scene)
+                    } ?: flowOf(null)
+                )
+
+            assertThat(upScene)
                 .isEqualTo(
                     expectedUpDestination(
                         canSwipeToEnter = canSwipeToEnter,
@@ -231,7 +244,14 @@
                     )
                 )
 
-            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Left))?.toScene)
+            val leftScene by
+                collectLastValue(
+                    destinationScenes?.get(Swipe(SwipeDirection.Left))?.toScene?.let { scene ->
+                        kosmos.sceneInteractor.resolveSceneFamily(scene)
+                    } ?: flowOf(null)
+                )
+
+            assertThat(leftScene)
                 .isEqualTo(
                     expectedLeftDestination(
                         isCommunalAvailable = isCommunalAvailable,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
index 6b1794e2..cb4e2d3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
@@ -30,8 +30,8 @@
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.scene.domain.interactor.homeSceneFamilyResolver
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 7ee20e5..5b6fea5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -41,10 +41,10 @@
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
 import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.homeSceneFamilyResolver
 import com.android.systemui.scene.domain.interactor.sceneBackInteractor
 import com.android.systemui.scene.domain.interactor.sceneContainerStartable
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
index f28ddeb..ac67ac8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
@@ -30,8 +30,8 @@
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.scene.domain.interactor.homeSceneFamilyResolver
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.ui.viewmodel.quickSettingsShadeSceneViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index f8a62cb..4d5d22c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -58,9 +58,9 @@
 import com.android.systemui.qs.footerActionsController
 import com.android.systemui.qs.footerActionsViewModelFactory
 import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
-import com.android.systemui.scene.domain.interactor.homeSceneFamilyResolver
 import com.android.systemui.scene.domain.interactor.sceneContainerStartable
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 92e6b16..ec7150b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.scene.data.repository.Transition
 import com.android.systemui.scene.data.repository.sceneContainerRepository
 import com.android.systemui.scene.data.repository.setSceneTransition
+import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.sceneContainerConfig
 import com.android.systemui.scene.sceneKeys
 import com.android.systemui.scene.shared.model.SceneFamilies
@@ -41,6 +42,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.test.runCurrent
@@ -399,8 +401,8 @@
     @Test
     fun resolveSceneFamily_home() =
         testScope.runTest {
-            assertThat(underTest.resolveSceneFamily(SceneFamilies.Home))
-                .isEqualTo(kosmos.homeSceneFamilyResolver.resolvedScene)
+            assertThat(underTest.resolveSceneFamily(SceneFamilies.Home).first())
+                .isEqualTo(kosmos.homeSceneFamilyResolver.resolvedScene.value)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt
index 3a5ff00..fa4da42 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt
@@ -29,8 +29,8 @@
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.scene.domain.interactor.homeSceneFamilyResolver
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shared.recents.utilities.Utilities
 import com.android.systemui.testKosmos
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index f88d102..c53cdf8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -19,6 +19,8 @@
 import android.testing.TestableLooper
 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.SceneKey
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.SwipeDirection
 import com.android.systemui.SysuiTestCase
@@ -27,6 +29,7 @@
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
@@ -39,8 +42,8 @@
 import com.android.systemui.qs.footerActionsViewModelFactory
 import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
 import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.homeSceneFamilyResolver
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
@@ -57,7 +60,9 @@
 import com.google.common.truth.Truth.assertThat
 import java.util.Locale
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -126,9 +131,7 @@
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pin
             )
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
+            setDeviceEntered(true)
 
             assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
                 .isEqualTo(SceneFamilies.Home)
@@ -196,9 +199,7 @@
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pin
             )
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
+            setDeviceEntered(true)
             runCurrent()
 
             assertThat(isClickable).isFalse()
@@ -345,6 +346,32 @@
         return maxTranslation
     }
 
+    private fun TestScope.setDeviceEntered(isEntered: Boolean) {
+        if (isEntered) {
+            // Unlock the device marking the device has entered.
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+        }
+        setScene(
+            if (isEntered) {
+                Scenes.Gone
+            } else {
+                Scenes.Lockscreen
+            }
+        )
+        assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
+    }
+
+    private fun TestScope.setScene(key: SceneKey) {
+        sceneInteractor.changeScene(key, "test")
+        sceneInteractor.setTransitionState(
+            MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+        )
+        runCurrent()
+    }
+
     private data class Translations(
         val start: Float,
         val end: Float,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
index 10a4eb7..7385a47 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
@@ -71,6 +71,7 @@
                 addOverride(R.drawable.ic_headphone, testIcon)
                 addOverride(R.drawable.ic_smartphone, testIcon)
                 addOverride(R.drawable.ic_media_speaker_device, testIcon)
+                addOverride(R.drawable.ic_media_tablet, testIcon)
 
                 addOverride(com.android.internal.R.drawable.ic_bt_hearing_aid, testIcon)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
index 8921a23..0f56d0b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
@@ -65,6 +65,7 @@
 
             with(context.orCreateTestableResources) {
                 addOverride(R.drawable.ic_smartphone, testIcon)
+                addOverride(R.drawable.ic_media_tablet, testIcon)
 
                 addOverride(R.string.media_transfer_this_device_name_tv, builtInDeviceName)
                 addOverride(R.string.media_transfer_this_device_name_tablet, builtInDeviceName)
diff --git a/packages/SystemUI/res/drawable/ic_bt_le_audio_sharing_18dp.xml b/packages/SystemUI/res/drawable/ic_bt_le_audio_sharing_18dp.xml
new file mode 100644
index 0000000..dd3d9e3
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_bt_le_audio_sharing_18dp.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item
+        android:drawable="@drawable/ic_bt_le_audio_sharing"
+        android:width="18dp"
+        android:height="18dp" />
+</layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
index a598007..27b8006 100644
--- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
@@ -268,6 +268,12 @@
                 android:ellipsize="end"
                 android:maxLines="1"
                 android:text="@string/quick_settings_bluetooth_audio_sharing_button"
+                android:drawableStart="@drawable/ic_bt_le_audio_sharing_18dp"
+                android:drawablePadding="10dp"
+                android:drawableTint="?android:attr/textColorPrimary"
+                app:layout_constrainedWidth="true"
+                app:layout_constraintHorizontal_bias="0"
+                app:layout_constraintEnd_toStartOf="@+id/done_button"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/barrier"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c2ca4da..0017db6 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -699,9 +699,9 @@
     <!-- QuickSettings: Bluetooth auto on info text when enabled [CHAR LIMIT=NONE]-->
     <string name="turn_on_bluetooth_auto_info_enabled">Bluetooth will turn on tomorrow morning</string>
     <!-- QuickSettings: Bluetooth dialog audio sharing button text [CHAR LIMIT=50]-->
-    <string name="quick_settings_bluetooth_audio_sharing_button">Audio Sharing</string>
+    <string name="quick_settings_bluetooth_audio_sharing_button">Share audio</string>
     <!-- QuickSettings: Bluetooth dialog audio sharing button text when sharing audio [CHAR LIMIT=50]-->
-    <string name="quick_settings_bluetooth_audio_sharing_button_sharing">Sharing Audio</string>
+    <string name="quick_settings_bluetooth_audio_sharing_button_sharing">Sharing audio</string>
 
     <!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]-->
     <string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string>
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
index 0bd6d6e..3c4c003 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
@@ -30,7 +30,12 @@
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.os.Handler;
+import android.util.Log;
 import android.view.AttachedSurfaceControl;
+import android.view.Display;
+import android.view.IRotationWatcher;
+import android.view.IWindowManager;
 import android.view.LayoutInflater;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
@@ -46,15 +51,18 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.res.R;
+import com.android.systemui.util.leak.RotationUtils;
 
 import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
 class FullscreenMagnificationController implements ComponentCallbacks {
 
+    private static final String TAG = "FullscreenMagnificationController";
     private final Context mContext;
     private final AccessibilityManager mAccessibilityManager;
     private final WindowManager mWindowManager;
+    private final IWindowManager mIWindowManager;
     private Supplier<SurfaceControlViewHost> mScvhSupplier;
     private SurfaceControlViewHost mSurfaceControlViewHost = null;
     private SurfaceControl mBorderSurfaceControl = null;
@@ -65,33 +73,50 @@
     private final int mDisplayId;
     private static final Region sEmptyRegion = new Region();
     private ValueAnimator mShowHideBorderAnimator;
+    private Handler mHandler;
     private Executor mExecutor;
     private boolean mFullscreenMagnificationActivated = false;
     private final Configuration mConfiguration;
+    private final Runnable mShowBorderRunnable = this::showBorderWithNullCheck;
+    private int mRotation;
+    private final IRotationWatcher mRotationWatcher = new IRotationWatcher.Stub() {
+        @Override
+        public void onRotationChanged(final int rotation) {
+            handleScreenRotation();
+        }
+    };
+    private final long mLongAnimationTimeMs;
 
     FullscreenMagnificationController(
             @UiContext Context context,
-            Executor executor,
+            @Main Handler handler,
+            @Main Executor executor,
             AccessibilityManager accessibilityManager,
             WindowManager windowManager,
+            IWindowManager iWindowManager,
             Supplier<SurfaceControlViewHost> scvhSupplier) {
-        this(context, executor, accessibilityManager, windowManager, scvhSupplier,
-                new SurfaceControl.Transaction(), createNullTargetObjectAnimator(context));
+        this(context, handler, executor, accessibilityManager,
+                windowManager, iWindowManager, scvhSupplier,
+                new SurfaceControl.Transaction(), null);
     }
 
     @VisibleForTesting
     FullscreenMagnificationController(
             @UiContext Context context,
+            @Main Handler handler,
             @Main Executor executor,
             AccessibilityManager accessibilityManager,
             WindowManager windowManager,
+            IWindowManager iWindowManager,
             Supplier<SurfaceControlViewHost> scvhSupplier,
             SurfaceControl.Transaction transaction,
             ValueAnimator valueAnimator) {
         mContext = context;
+        mHandler = handler;
         mExecutor = executor;
         mAccessibilityManager = accessibilityManager;
         mWindowManager = windowManager;
+        mIWindowManager = iWindowManager;
         mWindowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
         mTransaction = transaction;
         mScvhSupplier = scvhSupplier;
@@ -101,7 +126,10 @@
                 R.dimen.magnifier_border_width_fullscreen);
         mDisplayId = mContext.getDisplayId();
         mConfiguration = new Configuration(context.getResources().getConfiguration());
-        mShowHideBorderAnimator = valueAnimator;
+        mLongAnimationTimeMs = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_longAnimTime);
+        mShowHideBorderAnimator = (valueAnimator == null)
+                ? createNullTargetObjectAnimator() : valueAnimator;
         mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
@@ -114,15 +142,13 @@
         });
     }
 
-    private static ValueAnimator createNullTargetObjectAnimator(Context context) {
+    private ValueAnimator createNullTargetObjectAnimator() {
         final ValueAnimator valueAnimator =
                 ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f);
         Interpolator interpolator = new AccelerateDecelerateInterpolator();
-        final long longAnimationDuration = context.getResources().getInteger(
-                com.android.internal.R.integer.config_longAnimTime);
 
         valueAnimator.setInterpolator(interpolator);
-        valueAnimator.setDuration(longAnimationDuration);
+        valueAnimator.setDuration(mLongAnimationTimeMs);
         return valueAnimator;
     }
 
@@ -149,7 +175,11 @@
      */
     @UiThread
     private void removeFullscreenMagnificationBorder() {
+        if (mHandler.hasCallbacks(mShowBorderRunnable)) {
+            mHandler.removeCallbacks(mShowBorderRunnable);
+        }
         mContext.unregisterComponentCallbacks(this);
+
         mShowHideBorderAnimator.reverse();
     }
 
@@ -161,6 +191,11 @@
 
         if (mFullscreenBorder != null) {
             mFullscreenBorder = null;
+            try {
+                mIWindowManager.removeRotationWatcher(mRotationWatcher);
+            } catch (Exception e) {
+                Log.w(TAG, "Failed to remove rotation watcher", e);
+            }
         }
     }
 
@@ -186,6 +221,11 @@
             mSurfaceControlViewHost = mScvhSupplier.get();
             mSurfaceControlViewHost.setView(mFullscreenBorder, getBorderLayoutParams());
             mBorderSurfaceControl = mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl();
+            try {
+                mIWindowManager.watchRotation(mRotationWatcher, Display.DEFAULT_DISPLAY);
+            } catch (Exception e) {
+                Log.w(TAG, "Failed to register rotation watcher", e);
+            }
         }
 
         mTransaction
@@ -256,11 +296,55 @@
             reCreateWindow = true;
         }
 
-        if (mFullscreenBorder != null && reCreateWindow) {
+        if (mFullscreenBorder == null) {
+            return;
+        }
+
+        if (reCreateWindow) {
             final int newWidth = mWindowBounds.width() + 2 * mBorderOffset;
             final int newHeight = mWindowBounds.height() + 2 * mBorderOffset;
             mSurfaceControlViewHost.relayout(newWidth, newHeight);
         }
+
+        // Rotating from Landscape to ReverseLandscape will not trigger the config changes in
+        // CONFIG_SCREEN_SIZE and CONFIG_ORIENTATION. Therefore, we would like to check the device
+        // rotation separately.
+        // Since there's a possibility that {@link onConfigurationChanged} comes before
+        // {@link onRotationChanged}, we would like to handle screen rotation in either case that
+        // happens earlier.
+        int newRotation = RotationUtils.getRotation(mContext);
+        if (newRotation != mRotation) {
+            mRotation = newRotation;
+            handleScreenRotation();
+        }
+    }
+
+    private boolean isActivated() {
+        return mFullscreenBorder != null;
+    }
+
+    private void handleScreenRotation() {
+        if (!isActivated()) {
+            return;
+        }
+
+        if (mHandler.hasCallbacks(mShowBorderRunnable)) {
+            mHandler.removeCallbacks(mShowBorderRunnable);
+        }
+
+        // We hide the border immediately as early as possible to beat the redrawing of window
+        // in response to the orientation change so users won't see a weird shape border.
+        mHandler.postAtFrontOfQueue(() -> {
+            mFullscreenBorder.setAlpha(0f);
+        });
+
+        mHandler.postDelayed(mShowBorderRunnable, mLongAnimationTimeMs);
+    }
+
+    private void showBorderWithNullCheck() {
+        if (mShowHideBorderAnimator != null) {
+            mShowHideBorderAnimator.start();
+        }
     }
 
     private void updateDimensions() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
index 35c2024..e22a4e4 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
@@ -34,6 +34,7 @@
 import android.os.Message;
 import android.util.SparseArray;
 import android.view.Display;
+import android.view.IWindowManager;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
 import android.view.WindowManager;
@@ -148,13 +149,19 @@
             DisplayIdIndexSupplier<FullscreenMagnificationController> {
 
         private final Context mContext;
+        private final Handler mHandler;
         private final Executor mExecutor;
+        private final IWindowManager mIWindowManager;
 
-        FullscreenMagnificationControllerSupplier(Context context, DisplayManager displayManager,
-                Executor executor) {
+        FullscreenMagnificationControllerSupplier(Context context,
+                DisplayManager displayManager,
+                Handler handler,
+                Executor executor, IWindowManager iWindowManager) {
             super(displayManager);
             mContext = context;
+            mHandler = handler;
             mExecutor = executor;
+            mIWindowManager = iWindowManager;
         }
 
         @Override
@@ -166,9 +173,11 @@
             windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
             return new FullscreenMagnificationController(
                     windowContext,
+                    mHandler,
                     mExecutor,
                     windowContext.getSystemService(AccessibilityManager.class),
                     windowContext.getSystemService(WindowManager.class),
+                    mIWindowManager,
                     scvhSupplier);
         }
     }
@@ -211,14 +220,16 @@
     DisplayIdIndexSupplier<MagnificationSettingsController> mMagnificationSettingsSupplier;
 
     @Inject
-    public Magnification(Context context, @Main Handler mainHandler, @Main Executor executor,
+    public Magnification(Context context,
+            @Main Handler mainHandler, @Main Executor executor,
             CommandQueue commandQueue, ModeSwitchesController modeSwitchesController,
             SysUiState sysUiState, OverviewProxyService overviewProxyService,
             SecureSettings secureSettings, DisplayTracker displayTracker,
-            DisplayManager displayManager, AccessibilityLogger a11yLogger) {
+            DisplayManager displayManager, AccessibilityLogger a11yLogger,
+            IWindowManager iWindowManager) {
         this(context, mainHandler.getLooper(), executor, commandQueue,
                 modeSwitchesController, sysUiState, overviewProxyService, secureSettings,
-                displayTracker, displayManager, a11yLogger);
+                displayTracker, displayManager, a11yLogger, iWindowManager);
     }
 
     @VisibleForTesting
@@ -226,7 +237,8 @@
             CommandQueue commandQueue, ModeSwitchesController modeSwitchesController,
             SysUiState sysUiState, OverviewProxyService overviewProxyService,
             SecureSettings secureSettings, DisplayTracker displayTracker,
-            DisplayManager displayManager, AccessibilityLogger a11yLogger) {
+            DisplayManager displayManager, AccessibilityLogger a11yLogger,
+            IWindowManager iWindowManager) {
         mContext = context;
         mHandler = new Handler(looper) {
             @Override
@@ -248,7 +260,7 @@
                 mHandler, mWindowMagnifierCallback,
                 displayManager, sysUiState, secureSettings);
         mFullscreenMagnificationControllerSupplier = new FullscreenMagnificationControllerSupplier(
-                context, displayManager, mExecutor);
+                context, displayManager, mHandler, mExecutor, iWindowManager);
         mMagnificationSettingsSupplier = new SettingsSupplier(context,
                 mMagnificationSettingsControllerCallback, displayManager, secureSettings);
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/CommonSystemUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/CommonSystemUIUnfoldModule.kt
new file mode 100644
index 0000000..a91ce16
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dagger/CommonSystemUIUnfoldModule.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dagger
+
+import com.android.systemui.unfold.SysUIUnfoldComponent
+import com.android.systemui.unfold.SysUIUnfoldModule.BoundFromSysUiUnfoldModule
+import dagger.BindsOptionalOf
+import dagger.Module
+import dagger.Provides
+import java.util.Optional
+import kotlin.jvm.optionals.getOrElse
+
+
+/**
+ * Module for foldable-related classes that is available in all SystemUI variants.
+ * Provides `Optional<SysUIUnfoldComponent>` which is present when the device is a foldable
+ * device that has fold/unfold animation enabled.
+ */
+@Module
+abstract class CommonSystemUIUnfoldModule {
+
+    /* Note this will be injected as @BoundFromSysUiUnfoldModule Optional<Optional<...>> */
+    @BindsOptionalOf
+    @BoundFromSysUiUnfoldModule
+    abstract fun optionalSysUiUnfoldComponent(): Optional<SysUIUnfoldComponent>
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun sysUiUnfoldComponent(
+            /**
+             * This will be empty when [com.android.systemui.unfold.SysUIUnfoldModule] is not part
+             * of the graph, and contain the optional when it is.
+             */
+            @BoundFromSysUiUnfoldModule
+            optionalOfOptional: Optional<Optional<SysUIUnfoldComponent>>
+        ): Optional<SysUIUnfoldComponent> = optionalOfOptional.getOrElse { Optional.empty() }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
index a431a59..b71af69 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
@@ -19,6 +19,7 @@
 import com.android.systemui.keyguard.CustomizationProvider;
 import com.android.systemui.statusbar.NotificationInsetsModule;
 import com.android.systemui.statusbar.QsFrameTranslateModule;
+import com.android.systemui.unfold.SysUIUnfoldModule;
 
 import dagger.Subcomponent;
 
@@ -34,6 +35,7 @@
         SystemUIBinder.class,
         SystemUIModule.class,
         SystemUICoreStartableModule.class,
+        SysUIUnfoldModule.class,
         ReferenceSystemUIModule.class})
 public interface ReferenceSysUIComponent extends SysUIComponent {
 
@@ -51,3 +53,4 @@
      */
     void inject(CustomizationProvider customizationProvider);
 }
+
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 2ebb94f..a7ff3c3 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -143,7 +143,6 @@
 import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
 import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule;
 import com.android.systemui.tuner.dagger.TunerModule;
-import com.android.systemui.unfold.SysUIUnfoldModule;
 import com.android.systemui.user.UserModule;
 import com.android.systemui.user.domain.UserDomainLayerModule;
 import com.android.systemui.util.EventLogModule;
@@ -254,7 +253,7 @@
         SystemPropertiesFlagsModule.class,
         SysUIConcurrencyModule.class,
         SysUICoroutinesModule.class,
-        SysUIUnfoldModule.class,
+        CommonSystemUIUnfoldModule.class,
         TelephonyRepositoryModule.class,
         TemporaryDisplayModule.class,
         TunerModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
index 02e48fc..10cfd6b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
@@ -27,11 +27,13 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import com.android.systemui.util.kotlin.filterValuesNotNull
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -89,37 +91,36 @@
         isCommunalAvailable: Boolean,
         shadeMode: ShadeMode,
     ): Map<UserAction, UserActionResult> {
-        val shadeSceneKey =
+        val notifShadeSceneKey =
             UserActionResult(
-                toScene =
-                    if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade,
+                toScene = SceneFamilies.NotifShade,
                 transitionKey = ToSplitShade.takeIf { shadeMode is ShadeMode.Split },
             )
 
-        val quickSettingsIfSingleShade =
-            if (shadeMode is ShadeMode.Single) UserActionResult(Scenes.QuickSettings)
-            else shadeSceneKey
-
         return mapOf(
                 Swipe.Left to UserActionResult(Scenes.Communal).takeIf { isCommunalAvailable },
                 Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer,
 
                 // Swiping down from the top edge goes to QS (or shade if in split shade mode).
-                swipeDownFromTop(pointerCount = 1) to quickSettingsIfSingleShade,
-                swipeDownFromTop(pointerCount = 2) to
-                    // TODO(b/338577208): Remove 'Dual' once we add Dual Shade invocation zones.
-                    if (shadeMode is ShadeMode.Dual) {
-                        UserActionResult(Scenes.QuickSettingsShade)
+                swipeDownFromTop(pointerCount = 1) to
+                    if (shadeMode is ShadeMode.Single) {
+                        UserActionResult(Scenes.QuickSettings)
                     } else {
-                        quickSettingsIfSingleShade
+                        notifShadeSceneKey
                     },
 
-                // Swiping down, not from the edge, always navigates to the shade scene.
-                swipeDown(pointerCount = 1) to shadeSceneKey,
-                swipeDown(pointerCount = 2) to shadeSceneKey,
+                // TODO(b/338577208): Remove once we add Dual Shade invocation zones.
+                swipeDownFromTop(pointerCount = 2) to
+                    UserActionResult(
+                        toScene = SceneFamilies.QuickSettings,
+                        transitionKey = ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
+                    ),
+
+                // Swiping down, not from the edge, always navigates to the notif shade scene.
+                swipeDown(pointerCount = 1) to notifShadeSceneKey,
+                swipeDown(pointerCount = 2) to notifShadeSceneKey,
             )
-            .filterValues { it != null }
-            .mapValues { checkNotNull(it.value) }
+            .filterValuesNotNull()
     }
 
     private fun swipeDownFromTop(pointerCount: Int): Swipe {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index f3cc35ba..abc2b7f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -217,10 +217,13 @@
     private void setBrightnessViewMargin() {
         if (mBrightnessView != null) {
             MarginLayoutParams lp = (MarginLayoutParams) mBrightnessView.getLayoutParams();
+            // For Brightness Slider to extend its boundary to draw focus background
+            int offset = getResources()
+                    .getDimensionPixelSize(R.dimen.rounded_slider_boundary_offset);
             lp.topMargin = mContext.getResources()
-                    .getDimensionPixelSize(R.dimen.qs_brightness_margin_top);
+                    .getDimensionPixelSize(R.dimen.qs_brightness_margin_top) - offset;
             lp.bottomMargin = mContext.getResources()
-                    .getDimensionPixelSize(R.dimen.qs_brightness_margin_bottom);
+                    .getDimensionPixelSize(R.dimen.qs_brightness_margin_bottom) - offset;
             mBrightnessView.setLayoutParams(lp);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 0327ec7..23faf7d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -73,7 +73,6 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.compose.animation.scene.SceneKey;
 import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.AssistUtils;
@@ -99,12 +98,10 @@
 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
 import com.android.systemui.scene.domain.interactor.SceneInteractor;
 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
-import com.android.systemui.scene.shared.model.Scenes;
+import com.android.systemui.scene.shared.model.SceneFamilies;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeViewController;
-import com.android.systemui.shade.domain.interactor.ShadeInteractor;
-import com.android.systemui.shade.shared.model.ShadeMode;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -158,7 +155,6 @@
     private final ScreenPinningRequest mScreenPinningRequest;
     private final NotificationShadeWindowController mStatusBarWinController;
     private final Provider<SceneInteractor> mSceneInteractor;
-    private final Provider<ShadeInteractor> mShadeInteractor;
 
     private final Runnable mConnectionRunnable = () ->
             internalConnectToCurrentUser("runnable: startConnectionToCurrentUser");
@@ -247,7 +243,7 @@
                             // Gesture was too short to be picked up by scene container touch
                             // handling; programmatically start the transition to shade scene.
                             mSceneInteractor.get().changeScene(
-                                    getShadeSceneKey(),
+                                    SceneFamilies.NotifShade,
                                     "short launcher swipe"
                             );
                         }
@@ -267,7 +263,7 @@
                                 "trackpad swipe");
                     } else if (action == ACTION_UP) {
                         mSceneInteractor.get().changeScene(
-                                getShadeSceneKey(),
+                                SceneFamilies.NotifShade,
                                 "short trackpad swipe"
                         );
                     }
@@ -632,7 +628,6 @@
             NotificationShadeWindowController statusBarWinController,
             SysUiState sysUiState,
             Provider<SceneInteractor> sceneInteractor,
-            Provider<ShadeInteractor> shadeInteractor,
             UserTracker userTracker,
             WakefulnessLifecycle wakefulnessLifecycle,
             UiEventLogger uiEventLogger,
@@ -659,7 +654,6 @@
         mScreenPinningRequest = screenPinningRequest;
         mStatusBarWinController = statusBarWinController;
         mSceneInteractor = sceneInteractor;
-        mShadeInteractor = shadeInteractor;
         mUserTracker = userTracker;
         mConnectionBackoffAttempts = 0;
         mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
@@ -925,12 +919,6 @@
         }
     }
 
-    private SceneKey getShadeSceneKey() {
-        return mShadeInteractor.get().getShadeMode().getValue() == ShadeMode.dual()
-                ? Scenes.NotificationsShade
-                : Scenes.Shade;
-    }
-
     private void notifyHomeRotationEnabled(boolean enabled) {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
             mConnectionCallbacks.get(i).onHomeRotationEnabled(enabled);
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index da23936..323ca87 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -20,6 +20,9 @@
 import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
 import com.android.systemui.scene.domain.SceneDomainModule
 import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.scene.domain.resolver.HomeSceneFamilyResolverModule
+import com.android.systemui.scene.domain.resolver.NotifShadeSceneFamilyResolverModule
+import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolverModule
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.domain.startable.ScrimStartable
 import com.android.systemui.scene.shared.model.SceneContainerConfig
@@ -42,6 +45,11 @@
             QuickSettingsSceneModule::class,
             ShadeSceneModule::class,
             SceneDomainModule::class,
+
+            // List SceneResolver modules for supported SceneFamilies
+            HomeSceneFamilyResolverModule::class,
+            NotifShadeSceneFamilyResolverModule::class,
+            QuickSettingsSceneFamilyResolverModule::class,
         ],
 )
 interface KeyguardlessSceneContainerFrameworkModule {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index a0cf82a..4691eba 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -21,6 +21,9 @@
 import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
 import com.android.systemui.scene.domain.SceneDomainModule
 import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.scene.domain.resolver.HomeSceneFamilyResolverModule
+import com.android.systemui.scene.domain.resolver.NotifShadeSceneFamilyResolverModule
+import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolverModule
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.domain.startable.ScrimStartable
 import com.android.systemui.scene.shared.model.SceneContainerConfig
@@ -48,6 +51,11 @@
             NotificationsShadeSceneModule::class,
             NotificationsShadeSessionModule::class,
             SceneDomainModule::class,
+
+            // List SceneResolver modules for supported SceneFamilies
+            HomeSceneFamilyResolverModule::class,
+            NotifShadeSceneFamilyResolverModule::class,
+            QuickSettingsSceneFamilyResolverModule::class,
         ],
 )
 interface SceneContainerFrameworkModule {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ShadelessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/ShadelessSceneContainerFrameworkModule.kt
index a326ec1..9a7eef8 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ShadelessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ShadelessSceneContainerFrameworkModule.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.scene
 
 import com.android.systemui.scene.domain.SceneDomainModule
+import com.android.systemui.scene.domain.resolver.HomeSceneFamilyResolverModule
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
 import dagger.Module
@@ -31,6 +32,9 @@
             GoneSceneModule::class,
             LockscreenSceneModule::class,
             SceneDomainModule::class,
+
+            // List SceneResolver modules for supported SceneFamilies
+            HomeSceneFamilyResolverModule::class,
         ],
 )
 object ShadelessSceneContainerFrameworkModule {
@@ -49,11 +53,12 @@
                     Scenes.Bouncer,
                 ),
             initialSceneKey = Scenes.Lockscreen,
-            mapOf(
-                Scenes.Gone to 0,
-                Scenes.Lockscreen to 0,
-                Scenes.Bouncer to 1,
-            )
+            navigationDistances =
+                mapOf(
+                    Scenes.Gone to 0,
+                    Scenes.Lockscreen to 0,
+                    Scenes.Bouncer to 1,
+                )
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/SceneDomainModule.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/SceneDomainModule.kt
index 9b2a6dd..be792df 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/SceneDomainModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/SceneDomainModule.kt
@@ -16,14 +16,12 @@
 
 package com.android.systemui.scene.domain
 
-import com.android.systemui.scene.domain.resolver.HomeSceneFamilyResolverModule
 import com.android.systemui.scene.domain.resolver.SceneResolverModule
 import dagger.Module
 
 @Module(
     includes =
         [
-            HomeSceneFamilyResolverModule::class,
             SceneResolverModule::class,
         ]
 )
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 998537c..c98a49b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -36,7 +36,9 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emitAll
 import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
@@ -239,7 +241,14 @@
         loggingReason: String,
     ) {
         val currentSceneKey = currentScene.value
-        val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene
+        val resolvedScene =
+            sceneFamilyResolvers.get()[toScene]?.let { familyResolver ->
+                if (familyResolver.includesScene(currentSceneKey)) {
+                    return
+                } else {
+                    familyResolver.resolvedScene.value
+                }
+            } ?: toScene
         if (
             !validateSceneChange(
                 from = currentSceneKey,
@@ -320,8 +329,9 @@
      * Returns the [concrete scene][Scenes] for [sceneKey] if it is a [scene family][SceneFamilies],
      * otherwise returns a singleton [Flow] containing [sceneKey].
      */
-    fun resolveSceneFamily(sceneKey: SceneKey): Flow<SceneKey> =
-        sceneFamilyResolvers.get()[sceneKey]?.resolvedScene ?: flowOf(sceneKey)
+    fun resolveSceneFamily(sceneKey: SceneKey): Flow<SceneKey> = flow {
+        emitAll(sceneFamilyResolvers.get()[sceneKey]?.resolvedScene ?: flowOf(sceneKey))
+    }
 
     private fun isVisibleInternal(
         raw: Boolean = repository.isVisible.value,
@@ -365,4 +375,12 @@
 
         return from != to
     }
+
+    /** Returns a flow indicating if the currently visible scene can be resolved from [family]. */
+    fun isCurrentSceneInFamily(family: SceneKey): Flow<Boolean> =
+        currentScene.map { currentScene -> isSceneInFamily(currentScene, family) }
+
+    /** Returns `true` if [scene] can be resolved from [family]. */
+    fun isSceneInFamily(scene: SceneKey, family: SceneKey): Boolean =
+        sceneFamilyResolvers.get()[family]?.includesScene(scene) == true
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
index f19929c..9e91b66 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
@@ -51,25 +51,42 @@
     override val resolvedScene: StateFlow<SceneKey> =
         combine(
                 deviceEntryInteractor.canSwipeToEnter,
+                deviceEntryInteractor.isDeviceEntered,
                 deviceEntryInteractor.isUnlocked,
                 transform = ::homeScene,
             )
             .stateIn(
                 scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
+                started = SharingStarted.Eagerly,
                 initialValue =
                     homeScene(
                         deviceEntryInteractor.canSwipeToEnter.value,
+                        deviceEntryInteractor.isDeviceEntered.value,
                         deviceEntryInteractor.isUnlocked.value,
                     )
             )
 
-    private fun homeScene(canSwipeToEnter: Boolean?, isUnlocked: Boolean): SceneKey =
+    override fun includesScene(scene: SceneKey): Boolean = scene in homeScenes
+
+    private fun homeScene(
+        canSwipeToEnter: Boolean?,
+        isDeviceEntered: Boolean,
+        isUnlocked: Boolean,
+    ): SceneKey =
         when {
             canSwipeToEnter == true -> Scenes.Lockscreen
-            isUnlocked -> Scenes.Gone
-            else -> Scenes.Lockscreen
+            !isDeviceEntered -> Scenes.Lockscreen
+            !isUnlocked -> Scenes.Lockscreen
+            else -> Scenes.Gone
         }
+
+    companion object {
+        val homeScenes =
+            setOf(
+                Scenes.Gone,
+                Scenes.Lockscreen,
+            )
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt
new file mode 100644
index 0000000..99e554e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.scene.domain.resolver
+
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.scene.shared.model.SceneFamilies
+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 dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class NotifShadeSceneFamilyResolver
+@Inject
+constructor(
+    @Application applicationScope: CoroutineScope,
+    shadeInteractor: ShadeInteractor,
+) : SceneResolver {
+    override val targetFamily: SceneKey = SceneFamilies.NotifShade
+
+    override val resolvedScene: StateFlow<SceneKey> =
+        shadeInteractor.shadeMode
+            .map(::notifShadeScene)
+            .stateIn(
+                applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = notifShadeScene(shadeInteractor.shadeMode.value),
+            )
+
+    override fun includesScene(scene: SceneKey): Boolean = scene in notifShadeScenes
+
+    private fun notifShadeScene(shadeMode: ShadeMode) =
+        when (shadeMode) {
+            is ShadeMode.Single -> Scenes.Shade
+            is ShadeMode.Dual -> Scenes.NotificationsShade
+            is ShadeMode.Split -> Scenes.Shade
+        }
+
+    companion object {
+        val notifShadeScenes =
+            setOf(
+                Scenes.NotificationsShade,
+                Scenes.Shade,
+            )
+    }
+}
+
+@Module
+interface NotifShadeSceneFamilyResolverModule {
+    @Binds @IntoSet fun bindResolver(interactor: NotifShadeSceneFamilyResolver): SceneResolver
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt
new file mode 100644
index 0000000..2962a3e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.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.scene.domain.resolver
+
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.scene.shared.model.SceneFamilies
+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 dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class QuickSettingsSceneFamilyResolver
+@Inject
+constructor(
+    @Application applicationScope: CoroutineScope,
+    shadeInteractor: ShadeInteractor,
+) : SceneResolver {
+    override val targetFamily: SceneKey = SceneFamilies.QuickSettings
+
+    override val resolvedScene: StateFlow<SceneKey> =
+        shadeInteractor.shadeMode
+            .map(::quickSettingsScene)
+            .stateIn(
+                applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = quickSettingsScene(shadeInteractor.shadeMode.value),
+            )
+
+    override fun includesScene(scene: SceneKey): Boolean = scene in quickSettingsScenes
+
+    private fun quickSettingsScene(shadeMode: ShadeMode) =
+        when (shadeMode) {
+            is ShadeMode.Single -> Scenes.QuickSettings
+            is ShadeMode.Dual -> Scenes.QuickSettingsShade
+            is ShadeMode.Split -> Scenes.Shade
+        }
+
+    companion object {
+        val quickSettingsScenes =
+            setOf(
+                Scenes.QuickSettings,
+                Scenes.QuickSettingsShade,
+                Scenes.Shade,
+            )
+    }
+}
+
+@Module
+interface QuickSettingsSceneFamilyResolverModule {
+    @Binds @IntoSet fun bindResolver(interactor: QuickSettingsSceneFamilyResolver): SceneResolver
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/SceneResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/SceneResolver.kt
index 8372529..8d7247b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/SceneResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/SceneResolver.kt
@@ -29,6 +29,9 @@
 
     /** The concrete scene that [targetFamily] is currently resolved to. */
     val resolvedScene: StateFlow<SceneKey>
+
+    /** Returns `true` if [scene] can be resolved from [targetFamily]. */
+    fun includesScene(scene: SceneKey): Boolean
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
index 99118bc..c34a6cd 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
@@ -23,7 +23,7 @@
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
@@ -43,39 +43,42 @@
 ) {
     val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
         shadeInteractor.shadeMode
-            .map { shadeMode -> destinationScenes(shadeMode = shadeMode) }
+            .map(::destinationScenes)
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = destinationScenes(shadeMode = shadeInteractor.shadeMode.value)
+                initialValue =
+                    destinationScenes(
+                        shadeMode = shadeInteractor.shadeMode.value,
+                    )
             )
 
-    private fun destinationScenes(shadeMode: ShadeMode): Map<UserAction, UserActionResult> {
+    private fun destinationScenes(
+        shadeMode: ShadeMode,
+    ): Map<UserAction, UserActionResult> {
         return buildMap {
-            if (shadeMode is ShadeMode.Single) {
-                this[
+            if (
+                shadeMode is ShadeMode.Single ||
+                    // TODO(b/338577208): Remove this once we add Dual Shade invocation zones.
+                    shadeMode is ShadeMode.Dual
+            ) {
+                put(
                     Swipe(
                         pointerCount = 2,
                         fromSource = Edge.Top,
                         direction = SwipeDirection.Down,
-                    )] = UserActionResult(Scenes.QuickSettings)
+                    ),
+                    UserActionResult(SceneFamilies.QuickSettings)
+                )
             }
 
-            // TODO(b/338577208): Remove this once we add Dual Shade invocation zones.
-            if (shadeMode is ShadeMode.Dual) {
-                this[
-                    Swipe(
-                        pointerCount = 2,
-                        fromSource = Edge.Top,
-                        direction = SwipeDirection.Down,
-                    )] = UserActionResult(Scenes.QuickSettingsShade)
-            }
-
-            val downSceneKey =
-                if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
-            val downTransitionKey = ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
-            this[Swipe(direction = SwipeDirection.Down)] =
-                UserActionResult(downSceneKey, downTransitionKey)
+            put(
+                Swipe(direction = SwipeDirection.Down),
+                UserActionResult(
+                    SceneFamilies.NotifShade,
+                    ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
+                )
+            )
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index ac1f971..004db16 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -21,15 +21,12 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.dagger.ShadeTouchLog
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
 import com.android.systemui.shade.ShadeController.ShadeVisibilityListener
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.VibratorHelper
@@ -60,7 +57,6 @@
     private val shadeInteractor: ShadeInteractor,
     private val sceneInteractor: SceneInteractor,
     private val notificationStackScrollLayout: NotificationStackScrollLayout,
-    @ShadeTouchLog private val touchLog: LogBuffer,
     private val vibratorHelper: VibratorHelper,
     commandQueue: CommandQueue,
     statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
@@ -176,19 +172,14 @@
     }
 
     override fun expandToNotifications() {
-        val shadeMode = shadeInteractor.shadeMode.value
         sceneInteractor.changeScene(
-            if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade,
-            "ShadeController.animateExpandShade"
+            SceneFamilies.NotifShade,
+            "ShadeController.animateExpandShade",
         )
     }
 
     override fun expandToQs() {
-        val shadeMode = shadeInteractor.shadeMode.value
-        sceneInteractor.changeScene(
-            if (shadeMode is ShadeMode.Dual) Scenes.QuickSettingsShade else Scenes.QuickSettings,
-            "ShadeController.animateExpandQs"
-        )
+        sceneInteractor.changeScene(SceneFamilies.QuickSettings, "ShadeController.animateExpandQs")
     }
 
     override fun setVisibilityListener(listener: ShadeVisibilityListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index fe16fc0..d5b4f4d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -21,25 +21,23 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /** ShadeInteractor implementation for Scene Container. */
-@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class ShadeInteractorSceneContainerImpl
 @Inject
@@ -52,10 +50,11 @@
     override val shadeMode: StateFlow<ShadeMode> = shadeRepository.shadeMode
 
     override val shadeExpansion: StateFlow<Float> =
-        sceneBasedExpansion(sceneInteractor, notificationsScene)
+        sceneBasedExpansion(sceneInteractor, SceneFamilies.NotifShade)
             .stateIn(scope, SharingStarted.Eagerly, 0f)
 
-    private val sceneBasedQsExpansion = sceneBasedExpansion(sceneInteractor, quickSettingsScene)
+    private val sceneBasedQsExpansion =
+        sceneBasedExpansion(sceneInteractor, SceneFamilies.QuickSettings)
 
     override val qsExpansion: StateFlow<Float> =
         combine(
@@ -78,23 +77,38 @@
             .stateIn(scope, SharingStarted.Eagerly, false)
 
     override val isQsBypassingShade: Flow<Boolean> =
-        sceneInteractor.transitionState
-            .map { state ->
-                when (state) {
-                    is ObservableTransitionState.Idle -> false
-                    is ObservableTransitionState.Transition ->
-                        state.toScene == quickSettingsScene && state.fromScene != notificationsScene
-                }
+        combine(
+                sceneInteractor.resolveSceneFamily(SceneFamilies.QuickSettings),
+                sceneInteractor.resolveSceneFamily(SceneFamilies.NotifShade),
+                ::Pair
+            )
+            .flatMapLatestConflated { (quickSettingsScene, notificationsScene) ->
+                sceneInteractor.transitionState
+                    .map { state ->
+                        when (state) {
+                            is ObservableTransitionState.Idle -> false
+                            is ObservableTransitionState.Transition ->
+                                state.toScene == quickSettingsScene &&
+                                    state.fromScene != notificationsScene
+                        }
+                    }
+                    .distinctUntilChanged()
             }
             .distinctUntilChanged()
 
     override val isQsFullscreen: Flow<Boolean> =
-        sceneInteractor.transitionState
-            .map { state ->
-                when (state) {
-                    is ObservableTransitionState.Idle -> state.currentScene == quickSettingsScene
-                    is ObservableTransitionState.Transition -> false
-                }
+        sceneInteractor
+            .resolveSceneFamily(SceneFamilies.QuickSettings)
+            .flatMapLatestConflated { quickSettingsScene ->
+                sceneInteractor.transitionState
+                    .map { state ->
+                        when (state) {
+                            is ObservableTransitionState.Idle ->
+                                state.currentScene == quickSettingsScene
+                            is ObservableTransitionState.Transition -> false
+                        }
+                    }
+                    .distinctUntilChanged()
             }
             .distinctUntilChanged()
 
@@ -108,34 +122,39 @@
             .stateIn(scope, SharingStarted.Eagerly, false)
 
     override val isUserInteractingWithShade: Flow<Boolean> =
-        sceneBasedInteracting(sceneInteractor, notificationsScene)
+        sceneBasedInteracting(sceneInteractor, SceneFamilies.NotifShade)
 
     override val isUserInteractingWithQs: Flow<Boolean> =
-        sceneBasedInteracting(sceneInteractor, quickSettingsScene)
+        sceneBasedInteracting(sceneInteractor, SceneFamilies.QuickSettings)
 
     /**
      * Returns a flow that uses scene transition progress to and from a scene that is pulled down
      * from the top of the screen to a 0-1 expansion amount float.
      */
     fun sceneBasedExpansion(sceneInteractor: SceneInteractor, sceneKey: SceneKey) =
-        sceneInteractor.transitionState
-            .flatMapLatest { state ->
-                when (state) {
-                    is ObservableTransitionState.Idle ->
-                        if (state.currentScene == sceneKey) {
-                            flowOf(1f)
-                        } else {
-                            flowOf(0f)
+        sceneInteractor
+            .resolveSceneFamily(sceneKey)
+            .flatMapLatestConflated { resolvedSceneKey ->
+                sceneInteractor.transitionState
+                    .flatMapLatestConflated { state ->
+                        when (state) {
+                            is ObservableTransitionState.Idle ->
+                                if (state.currentScene == resolvedSceneKey) {
+                                    flowOf(1f)
+                                } else {
+                                    flowOf(0f)
+                                }
+                            is ObservableTransitionState.Transition ->
+                                if (state.toScene == resolvedSceneKey) {
+                                    state.progress
+                                } else if (state.fromScene == resolvedSceneKey) {
+                                    state.progress.map { progress -> 1 - progress }
+                                } else {
+                                    flowOf(0f)
+                                }
                         }
-                    is ObservableTransitionState.Transition ->
-                        if (state.toScene == sceneKey) {
-                            state.progress
-                        } else if (state.fromScene == sceneKey) {
-                            state.progress.map { progress -> 1 - progress }
-                        } else {
-                            flowOf(0f)
-                        }
-                }
+                    }
+                    .distinctUntilChanged()
             }
             .distinctUntilChanged()
 
@@ -145,29 +164,16 @@
      */
     fun sceneBasedInteracting(sceneInteractor: SceneInteractor, sceneKey: SceneKey) =
         sceneInteractor.transitionState
-            .map { state ->
+            .flatMapLatestConflated { state ->
                 when (state) {
-                    is ObservableTransitionState.Idle -> false
+                    is ObservableTransitionState.Idle -> flowOf(false)
                     is ObservableTransitionState.Transition ->
-                        state.isInitiatedByUserInput &&
-                            (state.toScene == sceneKey || state.fromScene == sceneKey)
+                        sceneInteractor.resolveSceneFamily(sceneKey).map { resolvedSceneKey ->
+                            state.isInitiatedByUserInput &&
+                                (state.toScene == resolvedSceneKey ||
+                                    state.fromScene == resolvedSceneKey)
+                        }
                 }
             }
             .distinctUntilChanged()
-
-    private val notificationsScene: SceneKey
-        get() =
-            if (shadeMode.value is ShadeMode.Dual) {
-                Scenes.NotificationsShade
-            } else {
-                Scenes.Shade
-            }
-
-    private val quickSettingsScene: SceneKey
-        get() =
-            if (shadeMode.value is ShadeMode.Dual) {
-                Scenes.QuickSettingsShade
-            } else {
-                Scenes.QuickSettings
-            }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
index e7fc18e..558f179 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
@@ -17,12 +17,11 @@
 package com.android.systemui.shade.domain.interactor
 
 import com.android.keyguard.LockIconViewController
-import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
@@ -31,7 +30,6 @@
 class ShadeLockscreenInteractorImpl
 @Inject
 constructor(
-    @Application private val applicationScope: CoroutineScope,
     @Background private val backgroundScope: CoroutineScope,
     private val shadeInteractor: ShadeInteractor,
     private val sceneInteractor: SceneInteractor,
@@ -69,6 +67,7 @@
     override fun setPulsing(pulsing: Boolean) {
         // Now handled elsewhere. Do nothing.
     }
+
     override fun transitionToExpandedShade(delay: Long) {
         backgroundScope.launch {
             delay(delay)
@@ -98,12 +97,9 @@
     }
 
     private fun changeToShadeScene() {
-        applicationScope.launch {
-            val shadeMode = shadeInteractor.shadeMode.value
-            sceneInteractor.changeScene(
-                if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade,
-                "ShadeLockscreenInteractorImpl.expandToNotifications",
-            )
-        }
+        sceneInteractor.changeScene(
+            SceneFamilies.NotifShade,
+            "ShadeLockscreenInteractorImpl.expandToNotifications",
+        )
     }
 }
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 e90a64a..cf5a562 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
@@ -24,6 +24,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
@@ -36,11 +37,9 @@
 import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
 
 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
 @SysUISingleton
@@ -50,7 +49,7 @@
     dumpManager: DumpManager,
     stackAppearanceInteractor: NotificationStackAppearanceInteractor,
     shadeInteractor: ShadeInteractor,
-    sceneInteractor: SceneInteractor,
+    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
     keyguardInteractor: Lazy<KeyguardInteractor>,
@@ -63,9 +62,11 @@
     val expandFraction: Flow<Float> =
         combine(
                 shadeInteractor.shadeExpansion,
+                shadeInteractor.shadeMode,
                 shadeInteractor.qsExpansion,
                 sceneInteractor.transitionState,
-            ) { shadeExpansion, qsExpansion, transitionState ->
+                sceneInteractor.resolveSceneFamily(SceneFamilies.QuickSettings),
+            ) { shadeExpansion, shadeMode, qsExpansion, transitionState, quickSettingsScene ->
                 when (transitionState) {
                     is ObservableTransitionState.Idle -> {
                         if (transitionState.currentScene == Scenes.Lockscreen) {
@@ -76,16 +77,16 @@
                     }
                     is ObservableTransitionState.Transition -> {
                         if (
-                            (transitionState.fromScene == notificationsScene &&
+                            (transitionState.fromScene in SceneFamilies.NotifShade &&
                                 transitionState.toScene == quickSettingsScene) ||
-                                (transitionState.fromScene == quickSettingsScene &&
-                                    transitionState.toScene == notificationsScene)
+                                (transitionState.fromScene in quickSettingsScene &&
+                                    transitionState.toScene in SceneFamilies.NotifShade)
                         ) {
                             1f
                         } else if (
-                            (transitionState.fromScene == Scenes.Gone ||
-                                transitionState.fromScene == Scenes.Lockscreen) &&
-                                transitionState.toScene == quickSettingsScene
+                            shadeMode != ShadeMode.Split &&
+                                transitionState.fromScene in SceneFamilies.Home &&
+                                transitionState.toScene in quickSettingsScene
                         ) {
                             // during QS expansion, increase fraction at same rate as scrim alpha,
                             // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN.
@@ -101,6 +102,9 @@
             .distinctUntilChanged()
             .dumpWhileCollecting("expandFraction")
 
+    private operator fun SceneKey.contains(scene: SceneKey) =
+        sceneInteractor.isSceneInFamily(scene, this)
+
     /** The bounds of the notification stack in the current scene. */
     private val shadeScrimClipping: Flow<ShadeScrimClipping?> =
         combine(
@@ -151,8 +155,8 @@
 
     /** Whether the notification stack is scrollable or not. */
     val isScrollable: Flow<Boolean> =
-        sceneInteractor.currentScene
-            .map { it == notificationsScene }
+        sceneInteractor
+            .isCurrentSceneInFamily(SceneFamilies.NotifShade)
             .dumpWhileCollecting("isScrollable")
 
     /** Whether the notification stack is displayed in doze mode. */
@@ -163,22 +167,4 @@
             keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing")
         }
     }
-
-    private val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode
-
-    private val notificationsScene: SceneKey
-        get() =
-            if (shadeMode.value is ShadeMode.Dual) {
-                Scenes.NotificationsShade
-            } else {
-                Scenes.Shade
-            }
-
-    private val quickSettingsScene: SceneKey
-        get() =
-            if (shadeMode.value is ShadeMode.Dual) {
-                Scenes.QuickSettingsShade
-            } else {
-                Scenes.QuickSettings
-            }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
index 139ac7e..a27989d 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
@@ -36,6 +36,7 @@
 import dagger.multibindings.IntoSet
 import java.util.Optional
 import javax.inject.Named
+import javax.inject.Qualifier
 import javax.inject.Scope
 
 @Scope @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class SysUIUnfoldScope
@@ -54,8 +55,17 @@
 @Module(subcomponents = [SysUIUnfoldComponent::class])
 class SysUIUnfoldModule {
 
+    /**
+     * Qualifier for dependencies bound in [com.android.systemui.unfold.SysUIUnfoldModule]
+     */
+    @Qualifier
+    @MustBeDocumented
+    @Retention(AnnotationRetention.RUNTIME)
+    annotation class BoundFromSysUiUnfoldModule
+
     @Provides
     @SysUISingleton
+    @BoundFromSysUiUnfoldModule
     fun provideSysUIUnfoldComponent(
         provider: Optional<UnfoldTransitionProgressProvider>,
         rotationProvider: Optional<NaturalRotationUnfoldProgressProvider>,
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt
index 41cd95b..8d202ac 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/MapUtils.kt
@@ -30,3 +30,6 @@
     }
     return destination
 }
+
+/** Returns a map with all entries containing `null` values removed. */
+fun <K, V> Map<K, V?>.filterValuesNotNull(): Map<K, V> = mapValuesNotNull { it.value }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index e613216..f457470 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -136,6 +136,7 @@
 import com.android.systemui.util.RoundedCornerProgressDrawable;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
+import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag;
 import com.android.systemui.volume.ui.binder.VolumeDialogMenuIconBinder;
 import com.android.systemui.volume.ui.navigation.VolumeNavigator;
 
@@ -313,6 +314,7 @@
     private final VibratorHelper mVibratorHelper;
     private final com.android.systemui.util.time.SystemClock mSystemClock;
     private final VolumeDialogMenuIconBinder mVolumeDialogMenuIconBinder;
+    private final VolumePanelFlag mVolumePanelFlag;
 
     public VolumeDialogImpl(
             Context context,
@@ -328,6 +330,7 @@
             CsdWarningDialog.Factory csdWarningDialogFactory,
             DevicePostureController devicePostureController,
             Looper looper,
+            VolumePanelFlag volumePanelFlag,
             DumpManager dumpManager,
             Lazy<SecureSettings> secureSettings,
             VibratorHelper vibratorHelper,
@@ -366,6 +369,7 @@
         mSecureSettings = secureSettings;
         mVolumeDialogMenuIconBinder = volumeDialogMenuIconBinder;
         mDialogTimeoutMillis = DIALOG_TIMEOUT_MILLIS;
+        mVolumePanelFlag = volumePanelFlag;
 
         dumpManager.registerDumpable("VolumeDialogImpl", this);
 
@@ -1364,6 +1368,9 @@
     }
 
     private void updateODICaptionsH(boolean isServiceComponentEnabled, boolean fromTooltip) {
+        // don't show captions view when the new volume panel is enabled.
+        isServiceComponentEnabled =
+                isServiceComponentEnabled && !mVolumePanelFlag.canUseNewVolumePanel();
         if (mODICaptionsView != null) {
             mODICaptionsView.setVisibility(isServiceComponentEnabled ? VISIBLE : GONE);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index fd68bfb..f8ddc42 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -44,6 +44,7 @@
 import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
 import com.android.systemui.volume.panel.dagger.VolumePanelComponent;
 import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory;
+import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag;
 import com.android.systemui.volume.ui.binder.VolumeDialogMenuIconBinder;
 import com.android.systemui.volume.ui.navigation.VolumeNavigator;
 
@@ -112,6 +113,7 @@
             VolumeNavigator volumeNavigator,
             CsdWarningDialog.Factory csdFactory,
             DevicePostureController devicePostureController,
+            VolumePanelFlag volumePanelFlag,
             DumpManager dumpManager,
             Lazy<SecureSettings> secureSettings,
             VibratorHelper vibratorHelper,
@@ -131,6 +133,7 @@
                 csdFactory,
                 devicePostureController,
                 Looper.getMainLooper(),
+                volumePanelFlag,
                 dumpManager,
                 secureSettings,
                 vibratorHelper,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
index 5bc9aa4..cbd535b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
@@ -21,8 +21,10 @@
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
-import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -34,8 +36,12 @@
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
+import android.os.RemoteException;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.Display;
+import android.view.IRotationWatcher;
+import android.view.IWindowManager;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
 import android.view.View;
@@ -55,6 +61,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -73,9 +81,12 @@
     private ValueAnimator mShowHideBorderAnimator;
     private SurfaceControl.Transaction mTransaction;
     private TestableWindowManager mWindowManager;
+    @Mock
+    private IWindowManager mIWindowManager;
 
     @Before
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
         getInstrumentation().runOnMainSync(() -> mSurfaceControlViewHost =
                 spy(new SurfaceControlViewHost(mContext, mContext.getDisplay(),
                         new InputTransferToken(), "FullscreenMagnification")));
@@ -88,9 +99,11 @@
         mShowHideBorderAnimator = spy(newNullTargetObjectAnimator());
         mFullscreenMagnificationController = new FullscreenMagnificationController(
                 mContext,
+                mContext.getMainThreadHandler(),
                 mContext.getMainExecutor(),
                 mContext.getSystemService(AccessibilityManager.class),
                 mContext.getSystemService(WindowManager.class),
+                mIWindowManager,
                 scvhSupplier,
                 mTransaction,
                 mShowHideBorderAnimator);
@@ -104,7 +117,8 @@
     }
 
     @Test
-    public void enableFullscreenMagnification_visibleBorder() throws InterruptedException {
+    public void enableFullscreenMagnification_visibleBorder()
+            throws InterruptedException, RemoteException {
         CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
         CountDownLatch animationEndLatch = new CountDownLatch(1);
         mTransaction.addTransactionCommittedListener(
@@ -119,17 +133,21 @@
                 //Enable fullscreen magnification
                 mFullscreenMagnificationController
                         .onFullscreenMagnificationActivationChanged(true));
-        assertTrue("Failed to wait for transaction committed",
-                transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
-        assertTrue("Failed to wait for animation to be finished",
-                animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertWithMessage("Failed to wait for transaction committed")
+                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
+                .isTrue();
+        assertWithMessage("Failed to wait for animation to be finished")
+                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+                .isTrue();
         verify(mShowHideBorderAnimator).start();
+        verify(mIWindowManager)
+                .watchRotation(any(IRotationWatcher.class), eq(Display.DEFAULT_DISPLAY));
         assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue();
     }
 
     @Test
     public void disableFullscreenMagnification_reverseAnimationAndReleaseScvh()
-            throws InterruptedException {
+            throws InterruptedException, RemoteException {
         CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
         CountDownLatch enableAnimationEndLatch = new CountDownLatch(1);
         CountDownLatch disableAnimationEndLatch = new CountDownLatch(1);
@@ -149,11 +167,12 @@
                 //Enable fullscreen magnification
                 mFullscreenMagnificationController
                         .onFullscreenMagnificationActivationChanged(true));
-        assertTrue("Failed to wait for transaction committed",
-                transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
-        assertTrue("Failed to wait for enabling animation to be finished",
-                enableAnimationEndLatch.await(
-                        ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertWithMessage("Failed to wait for transaction committed")
+                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
+                .isTrue();
+        assertWithMessage("Failed to wait for enabling animation to be finished")
+                .that(enableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+                .isTrue();
         verify(mShowHideBorderAnimator).start();
 
         getInstrumentation().runOnMainSync(() ->
@@ -161,11 +180,12 @@
                 mFullscreenMagnificationController
                         .onFullscreenMagnificationActivationChanged(false));
 
-        assertTrue("Failed to wait for disabling animation to be finished",
-                disableAnimationEndLatch.await(
-                        ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertWithMessage("Failed to wait for disabling animation to be finished")
+                .that(disableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+                .isTrue();
         verify(mShowHideBorderAnimator).reverse();
         verify(mSurfaceControlViewHost).release();
+        verify(mIWindowManager).removeRotationWatcher(any(IRotationWatcher.class));
     }
 
     @Test
@@ -188,10 +208,12 @@
                 () -> mFullscreenMagnificationController
                             .onFullscreenMagnificationActivationChanged(true));
 
-        assertTrue("Failed to wait for transaction committed",
-                transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
-        assertTrue("Failed to wait for animation to be finished",
-                animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertWithMessage("Failed to wait for transaction committed")
+                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
+                .isTrue();
+        assertWithMessage("Failed to wait for animation to be finished")
+                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+                        .isTrue();
         verify(mShowHideBorderAnimator).reverse();
     }
 
@@ -212,10 +234,12 @@
                 //Enable fullscreen magnification
                 mFullscreenMagnificationController
                         .onFullscreenMagnificationActivationChanged(true));
-        assertTrue("Failed to wait for transaction committed",
-                transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS));
-        assertTrue("Failed to wait for animation to be finished",
-                animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertWithMessage("Failed to wait for transaction committed")
+                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
+                .isTrue();
+        assertWithMessage("Failed to wait for animation to be finished")
+                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+                .isTrue();
         final Rect testWindowBounds = new Rect(
                 mWindowManager.getCurrentWindowMetrics().getBounds());
         testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
index 5bfb3cf..361a945 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
@@ -39,6 +39,7 @@
 import android.provider.Settings;
 import android.testing.TestableLooper;
 import android.view.Display;
+import android.view.IWindowManager;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IMagnificationConnection;
 import android.view.accessibility.IMagnificationConnectionCallback;
@@ -99,6 +100,8 @@
     private SecureSettings mSecureSettings;
     @Mock
     private AccessibilityLogger mA11yLogger;
+    @Mock
+    private IWindowManager mIWindowManager;
 
     private IMagnificationConnection mIMagnificationConnection;
     private Magnification mMagnification;
@@ -117,9 +120,10 @@
         mTestableLooper = TestableLooper.get(this);
         assertNotNull(mTestableLooper);
         mMagnification = new Magnification(getContext(),
-                mTestableLooper.getLooper(), getContext().getMainExecutor(), mCommandQueue,
+                mTestableLooper.getLooper(), mContext.getMainExecutor(), mCommandQueue,
                 mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings,
-                mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger);
+                mDisplayTracker, getContext().getSystemService(DisplayManager.class),
+                mA11yLogger, mIWindowManager);
         mMagnification.mWindowMagnificationControllerSupplier =
                 new FakeWindowMagnificationControllerSupplier(
                         mContext.getSystemService(DisplayManager.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
index ffba25c..17b7e21 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
@@ -42,6 +42,7 @@
 import android.os.RemoteException;
 import android.testing.TestableLooper;
 import android.view.Display;
+import android.view.IWindowManager;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IMagnificationConnection;
 import android.view.accessibility.IMagnificationConnectionCallback;
@@ -93,6 +94,8 @@
     private MagnificationSettingsController mMagnificationSettingsController;
     @Mock
     private AccessibilityLogger mA11yLogger;
+    @Mock
+    private IWindowManager mIWindowManager;
 
     @Before
     public void setUp() throws Exception {
@@ -122,10 +125,10 @@
 
         mCommandQueue = new CommandQueue(getContext(), mDisplayTracker);
         mMagnification = new Magnification(getContext(),
-                getContext().getMainThreadHandler(), getContext().getMainExecutor(),
+                getContext().getMainThreadHandler(), mContext.getMainExecutor(),
                 mCommandQueue, mModeSwitchesController,
                 mSysUiState, mOverviewProxyService, mSecureSettings, mDisplayTracker,
-                getContext().getSystemService(DisplayManager.class), mA11yLogger);
+                getContext().getSystemService(DisplayManager.class), mA11yLogger, mIWindowManager);
         mMagnification.mWindowMagnificationControllerSupplier = new FakeControllerSupplier(
                 mContext.getSystemService(DisplayManager.class), mWindowMagnificationController);
         mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
index c782e9d..459e41d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.util.mockTopActivityClassName
 import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.user.domain.UserDomainLayerModule
 import dagger.BindsInstance
 import dagger.Component
 import junit.framework.Assert.assertEquals
@@ -443,6 +444,7 @@
             [
                 SysUITestModule::class,
                 BiometricsDomainLayerModule::class,
+                UserDomainLayerModule::class,
             ]
     )
     interface TestComponent {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
index 33e9b36..c7f4416 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
 import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
+import com.android.systemui.user.domain.UserDomainLayerModule
 import com.android.systemui.util.mockito.any
 import dagger.BindsInstance
 import dagger.Component
@@ -120,6 +121,7 @@
             [
                 SysUITestModule::class,
                 BiometricsDomainLayerModule::class,
+                UserDomainLayerModule::class,
             ]
     )
     interface TestComponent {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index fc74586..6e6e311 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -241,7 +241,6 @@
             statusBarWinController,
             sysUiState,
             mock(),
-            mock(),
             userTracker,
             wakefulnessLifecycle,
             uiEventLogger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index c9f2add..26f5370 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.statusbar.notification.shared.byIsSilent
 import com.android.systemui.statusbar.notification.shared.byIsSuppressedFromStatusBar
 import com.android.systemui.statusbar.notification.shared.byKey
+import com.android.systemui.user.domain.UserDomainLayerModule
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -156,7 +157,14 @@
 
     private val bubbles: Bubbles = mock()
 
-    @Component(modules = [SysUITestModule::class, BiometricsDomainLayerModule::class])
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                BiometricsDomainLayerModule::class,
+                UserDomainLayerModule::class,
+            ]
+    )
     @SysUISingleton
     interface TestComponent : SysUITestComponent<AlwaysOnDisplayNotificationIconsInteractor> {
 
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 dbdbe65..fac6a4c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -87,6 +87,7 @@
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
+import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag;
 import com.android.systemui.volume.ui.binder.VolumeDialogMenuIconBinder;
 import com.android.systemui.volume.ui.navigation.VolumeNavigator;
 
@@ -119,7 +120,6 @@
     View mDrawerNormal;
     ViewGroup mDialogRowsView;
     CaptionsToggleImageButton mODICaptionsIcon;
-
     private TestableLooper mTestableLooper;
     private ConfigurationController mConfigurationController;
     private int mOriginalOrientation;
@@ -151,6 +151,8 @@
     private VolumeNavigator mVolumeNavigator;
     @Mock
     private VolumeDialogMenuIconBinder mVolumeDialogMenuIconBinder;
+    @Mock
+    private VolumePanelFlag mVolumePanelFlag;
 
     private final CsdWarningDialog.Factory mCsdWarningDialogFactory =
             new CsdWarningDialog.Factory() {
@@ -211,6 +213,7 @@
                 mCsdWarningDialogFactory,
                 mPostureController,
                 mTestableLooper.getLooper(),
+                mVolumePanelFlag,
                 mDumpManager,
                 mLazySecureSettings,
                 mVibratorHelper,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
index d1fbb5e..066736c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
@@ -16,16 +16,12 @@
 
 package com.android.systemui.scene.domain.interactor
 
-import com.android.compose.animation.scene.SceneKey
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.scene.data.repository.sceneContainerRepository
-import com.android.systemui.scene.domain.resolver.HomeSceneFamilyResolver
-import com.android.systemui.scene.domain.resolver.SceneResolver
+import com.android.systemui.scene.domain.resolver.sceneFamilyResolvers
 import com.android.systemui.scene.shared.logger.sceneLogger
-import com.android.systemui.scene.shared.model.SceneFamilies
 
 val Kosmos.sceneInteractor by
     Kosmos.Fixture {
@@ -37,14 +33,3 @@
             deviceUnlockedInteractor = deviceUnlockedInteractor,
         )
     }
-
-val Kosmos.sceneFamilyResolvers: Map<SceneKey, SceneResolver>
-    get() = mapOf(SceneFamilies.Home to homeSceneFamilyResolver)
-
-val Kosmos.homeSceneFamilyResolver by
-    Kosmos.Fixture {
-        HomeSceneFamilyResolver(
-            applicationScope = applicationCoroutineScope,
-            deviceEntryInteractor = deviceEntryInteractor,
-        )
-    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
new file mode 100644
index 0000000..6be1939
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.domain.resolver
+
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.sceneFamilyResolvers: Map<SceneKey, SceneResolver>
+    get() =
+        mapOf(
+            SceneFamilies.Home to homeSceneFamilyResolver,
+            SceneFamilies.NotifShade to notifShadeSceneFamilyResolver,
+            SceneFamilies.QuickSettings to quickSettingsSceneFamilyResolver,
+        )
+
+val Kosmos.homeSceneFamilyResolver by
+    Kosmos.Fixture {
+        HomeSceneFamilyResolver(
+            applicationScope = applicationCoroutineScope,
+            deviceEntryInteractor = deviceEntryInteractor,
+        )
+    }
+
+val Kosmos.notifShadeSceneFamilyResolver by
+    Kosmos.Fixture {
+        NotifShadeSceneFamilyResolver(
+            applicationScope = applicationCoroutineScope,
+            shadeInteractor = shadeInteractor,
+        )
+    }
+
+val Kosmos.quickSettingsSceneFamilyResolver by
+    Kosmos.Fixture {
+        QuickSettingsSceneFamilyResolver(
+            applicationScope = applicationCoroutineScope,
+            shadeInteractor = shadeInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
index cc836ac..0bc4d54 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
@@ -23,7 +23,6 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.log.LogBuffer
 import com.android.systemui.plugins.statusbar.statusBarStateController
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -50,7 +49,6 @@
             shadeInteractor = shadeInteractor,
             sceneInteractor = sceneInteractor,
             notificationStackScrollLayout = mock<NotificationStackScrollLayout>(),
-            touchLog = mock<LogBuffer>(),
             vibratorHelper = mock<VibratorHelper>(),
             commandQueue = mock<CommandQueue>(),
             statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt
index 0a3a2ee..bcea983 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt
@@ -25,7 +25,6 @@
 val Kosmos.shadeLockscreenInteractor by
     Kosmos.Fixture {
         ShadeLockscreenInteractorImpl(
-            applicationScope = applicationCoroutineScope,
             backgroundScope = applicationCoroutineScope,
             shadeInteractor = shadeInteractorImpl,
             sceneInteractor = sceneInteractor,
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
index 4f3aee9..fec6ff1 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
@@ -27,6 +27,7 @@
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import java.util.concurrent.CopyOnWriteArrayList
 
 /**
  * Allows to subscribe to rotation changes. Updates are provided for the display associated to
@@ -41,7 +42,7 @@
     @Assisted private val callbackHandler: Handler,
 ) : CallbackController<RotationChangeProvider.RotationListener> {
 
-    private val listeners = mutableListOf<RotationListener>()
+    private val listeners = CopyOnWriteArrayList<RotationListener>()
 
     private val displayListener = RotationDisplayListener()
     private var lastRotation: Int? = null
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index b1b1dba..93bd926 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -212,24 +212,46 @@
     public static final int TOUCH_VIRTUAL = 3;
 
     /**
-     * Diff result: The {@link #state} or {@link #committedState} fields differ.
-     */
-    public static final int DIFF_STATE = 1 << 0;
-
-    /**
      * Diff result: Other fields differ.
      */
-    public static final int DIFF_OTHER = 1 << 1;
+    public static final int DIFF_OTHER = 1 << 0;
+
+    /**
+     * Diff result: The {@link #state} or {@link #committedState} fields differ.
+     */
+    public static final int DIFF_STATE = 1 << 1;
+
+    /**
+     * Diff result: The committed state differs. Note this is slightly different from the state,
+     * which is what most of the device should care about.
+     */
+    public static final int DIFF_COMMITTED_STATE = 1 << 2;
 
     /**
      * Diff result: The color mode fields differ.
      */
-    public static final int DIFF_COLOR_MODE = 1 << 2;
+    public static final int DIFF_COLOR_MODE = 1 << 3;
 
     /**
      * Diff result: The hdr/sdr ratio differs
      */
-    public static final int DIFF_HDR_SDR_RATIO = 1 << 3;
+    public static final int DIFF_HDR_SDR_RATIO = 1 << 4;
+
+    /**
+     * Diff result: The rotation differs
+     */
+    public static final int DIFF_ROTATION = 1 << 5;
+
+    /**
+     * Diff result: The render timings. Note this could be any of {@link #renderFrameRate},
+     * {@link #presentationDeadlineNanos}, or {@link #appVsyncOffsetNanos}.
+     */
+    public static final int DIFF_RENDER_TIMINGS = 1 << 6;
+
+    /**
+     * Diff result: The mode ID differs.
+     */
+    public static final int DIFF_MODE_ID = 1 << 7;
 
     /**
      * Diff result: Catch-all for "everything changed"
@@ -462,21 +484,33 @@
      */
     public int diff(DisplayDeviceInfo other) {
         int diff = 0;
-        if (state != other.state || committedState != other.committedState) {
+        if (state != other.state) {
             diff |= DIFF_STATE;
         }
+        if (committedState != other.committedState) {
+            diff |= DIFF_COMMITTED_STATE;
+        }
         if (colorMode != other.colorMode) {
             diff |= DIFF_COLOR_MODE;
         }
         if (!BrightnessSynchronizer.floatEquals(hdrSdrRatio, other.hdrSdrRatio)) {
             diff |= DIFF_HDR_SDR_RATIO;
         }
+        if (rotation != other.rotation) {
+            diff |= DIFF_ROTATION;
+        }
+        if (renderFrameRate != other.renderFrameRate
+                || presentationDeadlineNanos != other.presentationDeadlineNanos
+                || appVsyncOffsetNanos != other.appVsyncOffsetNanos) {
+            diff |= DIFF_RENDER_TIMINGS;
+        }
+        if (modeId != other.modeId) {
+            diff |= DIFF_MODE_ID;
+        }
         if (!Objects.equals(name, other.name)
                 || !Objects.equals(uniqueId, other.uniqueId)
                 || width != other.width
                 || height != other.height
-                || modeId != other.modeId
-                || renderFrameRate != other.renderFrameRate
                 || defaultModeId != other.defaultModeId
                 || userPreferredModeId != other.userPreferredModeId
                 || !Arrays.equals(supportedModes, other.supportedModes)
@@ -487,12 +521,9 @@
                 || densityDpi != other.densityDpi
                 || xDpi != other.xDpi
                 || yDpi != other.yDpi
-                || appVsyncOffsetNanos != other.appVsyncOffsetNanos
-                || presentationDeadlineNanos != other.presentationDeadlineNanos
                 || flags != other.flags
                 || !Objects.equals(displayCutout, other.displayCutout)
                 || touch != other.touch
-                || rotation != other.rotation
                 || type != other.type
                 || !Objects.equals(address, other.address)
                 || !Objects.equals(deviceProductInfo, other.deviceProductInfo)
diff --git a/services/core/java/com/android/server/display/DisplayDeviceRepository.java b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
index 6164154..086f8a9 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceRepository.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
@@ -21,6 +21,7 @@
 import android.util.Slog;
 import android.view.Display;
 import android.view.DisplayAddress;
+import android.view.Surface;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.display.DisplayManagerService.SyncRoot;
@@ -179,6 +180,20 @@
             if (diff == DisplayDeviceInfo.DIFF_STATE) {
                 Slog.i(TAG, "Display device changed state: \"" + info.name
                         + "\", " + Display.stateToString(info.state));
+            } else if (diff == DisplayDeviceInfo.DIFF_ROTATION) {
+                Slog.i(TAG, "Display device rotated: \"" + info.name
+                        + "\", " + Surface.rotationToString(info.rotation));
+            } else if (diff
+                    == (DisplayDeviceInfo.DIFF_MODE_ID | DisplayDeviceInfo.DIFF_RENDER_TIMINGS)) {
+                Slog.i(TAG, "Display device changed render timings: \"" + info.name
+                        + "\", renderFrameRate=" + info.renderFrameRate
+                        + ", presentationDeadlineNanos=" + info.presentationDeadlineNanos
+                        + ", appVsyncOffsetNanos=" + info.appVsyncOffsetNanos);
+            } else if (diff == DisplayDeviceInfo.DIFF_COMMITTED_STATE) {
+                if (DEBUG) {
+                    Slog.i(TAG, "Display device changed committed state: \"" + info.name
+                            + "\", " + Display.stateToString(info.committedState));
+                }
             } else if (diff != DisplayDeviceInfo.DIFF_HDR_SDR_RATIO) {
                 Slog.i(TAG, "Display device changed: " + info);
             }
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
index 2b5241f..b43b35b 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
@@ -102,6 +102,9 @@
 
     private DisplayManagerFlags mDisplayManagerFlags;
 
+    // Indicates if the current auto-brightness should be ramped up or down slowly.
+    private boolean mIsSlowChange;
+
     @VisibleForTesting
     AutomaticBrightnessStrategy(Context context, int displayId, Injector injector,
             DisplayManagerFlags displayManagerFlags) {
@@ -172,6 +175,11 @@
                 isValid = true;
             }
         }
+
+        // A change is slow when the auto-brightness was already applied, and there are no new
+        // auto-brightness adjustments from an external client(e.g. Moving the slider). As such,
+        // it is important to record this value before applying the current auto-brightness.
+        mIsSlowChange = hasAppliedAutoBrightness() && !getAutoBrightnessAdjustmentChanged();
         setAutoBrightnessApplied(isValid);
         return isValid;
     }
@@ -284,8 +292,7 @@
                 .setSdrBrightness(brightness)
                 .setBrightnessReason(brightnessReason)
                 .setDisplayBrightnessStrategyName(getName())
-                .setIsSlowChange(hasAppliedAutoBrightness()
-                        && !getAutoBrightnessAdjustmentChanged())
+                .setIsSlowChange(mIsSlowChange)
                 .setBrightnessEvent(brightnessEvent)
                 .setBrightnessAdjustmentFlag(mAutoBrightnessAdjustmentReasonsFlags)
                 .setShouldUpdateScreenBrightnessSetting(
diff --git a/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java
index 8a3a56c..fd3a92e 100644
--- a/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java
+++ b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java
@@ -212,6 +212,16 @@
         }
     }
 
+    public void setChargingPolicy(int policy) throws RemoteException {
+        IHealth service = mLastService.get();
+        if (service == null) return;
+        try {
+            service.setChargingPolicy(policy);
+        } catch (UnsupportedOperationException | ServiceSpecificException ex) {
+            return;
+        }
+    }
+
     private static void traceBegin(String name) {
         Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, name);
     }
diff --git a/services/core/java/com/android/server/health/OWNERS b/services/core/java/com/android/server/health/OWNERS
index 81522fc..44ab7f7 100644
--- a/services/core/java/com/android/server/health/OWNERS
+++ b/services/core/java/com/android/server/health/OWNERS
@@ -1 +1 @@
-file:platform/hardware/interfaces:/health/aidl/OWNERS
+file:platform/hardware/interfaces:/health/OWNERS
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index d5e85da..3673eb0 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -57,11 +57,8 @@
 import java.util.Objects;
 import java.util.Set;
 
-/**
- * Maintains a connection to a particular {@link MediaRoute2ProviderService}.
- */
-final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider
-        implements ServiceConnection {
+/** Maintains a connection to a particular {@link MediaRoute2ProviderService}. */
+final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
     private static final String TAG = "MR2ProviderSvcProxy";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -69,6 +66,7 @@
     private final int mUserId;
     private final Handler mHandler;
     private final boolean mIsSelfScanOnlyProvider;
+    private final ServiceConnection mServiceConnection = new ServiceConnectionImpl();
 
     // Connection state
     private boolean mRunning;
@@ -303,9 +301,12 @@
             Intent service = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE);
             service.setComponent(mComponentName);
             try {
-                mBound = mContext.bindServiceAsUser(service, this,
-                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
-                        new UserHandle(mUserId));
+                mBound =
+                        mContext.bindServiceAsUser(
+                                service,
+                                mServiceConnection,
+                                Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+                                new UserHandle(mUserId));
                 if (!mBound && DEBUG) {
                     Slog.d(TAG, this + ": Bind failed");
                 }
@@ -325,12 +326,11 @@
 
             mBound = false;
             disconnect();
-            mContext.unbindService(this);
+            mContext.unbindService(mServiceConnection);
         }
     }
 
-    @Override
-    public void onServiceConnected(ComponentName name, IBinder service) {
+    private void onServiceConnectedInternal(IBinder service) {
         if (DEBUG) {
             Slog.d(TAG, this + ": Connected");
         }
@@ -354,16 +354,14 @@
         }
     }
 
-    @Override
-    public void onServiceDisconnected(ComponentName name) {
+    private void onServiceDisconnectedInternal() {
         if (DEBUG) {
             Slog.d(TAG, this + ": Service disconnected");
         }
         disconnect();
     }
 
-    @Override
-    public void onBindingDied(ComponentName name) {
+    private void onBindingDiedInternal(ComponentName name) {
         unbind();
         if (Flags.enablePreventionOfKeepAliveRouteProviders()) {
             Slog.w(
@@ -662,6 +660,37 @@
                 pendingTransferCount);
     }
 
+    // All methods in this class are called on the main thread.
+    private final class ServiceConnectionImpl implements ServiceConnection {
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            if (Flags.enableMr2ServiceNonMainBgThread()) {
+                mHandler.post(() -> onServiceConnectedInternal(service));
+            } else {
+                onServiceConnectedInternal(service);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            if (Flags.enableMr2ServiceNonMainBgThread()) {
+                mHandler.post(() -> onServiceDisconnectedInternal());
+            } else {
+                onServiceDisconnectedInternal();
+            }
+        }
+
+        @Override
+        public void onBindingDied(ComponentName name) {
+            if (Flags.enableMr2ServiceNonMainBgThread()) {
+                mHandler.post(() -> onBindingDiedInternal(name));
+            } else {
+                onBindingDiedInternal(name);
+            }
+        }
+    }
+
     private final class Connection implements DeathRecipient {
         private final IMediaRoute2ProviderService mService;
         private final ServiceCallbackStub mCallbackStub;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d9e14340..d38cd88 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6548,7 +6548,10 @@
         // Schedule an idle timeout in case the app doesn't do it for us.
         mTaskSupervisor.scheduleIdleTimeout(this);
 
-        mTaskSupervisor.reportResumedActivityLocked(this);
+        mTaskSupervisor.mStoppingActivities.remove(this);
+        if (getDisplayArea().allResumedActivitiesComplete()) {
+            mRootWindowContainer.executeAppTransitionForAllDisplay();
+        }
 
         resumeKeyDispatchingLocked();
         final Task rootTask = getRootTask();
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 3867d2d..b6e6991 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2064,21 +2064,6 @@
         }
     }
 
-    boolean reportResumedActivityLocked(ActivityRecord r) {
-        // A resumed activity cannot be stopping. remove from list
-        mStoppingActivities.remove(r);
-
-        final Task rootTask = r.getRootTask();
-        if (rootTask.getDisplayArea().allResumedActivitiesComplete()) {
-            mRootWindowContainer.ensureActivitiesVisible();
-            // Make sure activity & window visibility should be identical
-            // for all displays in this stage.
-            mRootWindowContainer.executeAppTransitionForAllDisplay();
-            return true;
-        }
-        return false;
-    }
-
     // Called when WindowManager has finished animating the launchingBehind activity to the back.
     private void handleLaunchTaskBehindCompleteLocked(ActivityRecord r) {
         final Task task = r.getTask();
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index f91ef1d..0febec9 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -200,6 +200,7 @@
             }
             infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback());
             infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback());
+            infoBuilder.setTouchableRegion(window.getFrame());
             mNavigationMonitor.startMonitor(window, navigationObserver);
 
             ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, "
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index c683d4d..f70d2a5 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -538,13 +538,6 @@
                 || !mWindowManager.isKeyguardSecure(mService.getCurrentUserId());
     }
 
-    /**
-     * @return Whether the dream activity is on top of default display.
-     */
-    boolean isShowingDream() {
-        return getDisplayState(DEFAULT_DISPLAY).mShowingDream;
-    }
-
     private void updateKeyguardSleepToken() {
         for (int displayNdx = mRootWindowContainer.getChildCount() - 1;
              displayNdx >= 0; displayNdx--) {
@@ -631,7 +624,6 @@
          * Note that this can be true even if the keyguard is disabled or not showing.
          */
         private boolean mOccluded;
-        private boolean mShowingDream;
 
         private ActivityRecord mTopOccludesActivity;
         private ActivityRecord mDismissingKeyguardActivity;
@@ -669,7 +661,6 @@
 
             mRequestDismissKeyguard = false;
             mOccluded = false;
-            mShowingDream = false;
 
             mTopOccludesActivity = null;
             mDismissingKeyguardActivity = null;
@@ -697,21 +688,18 @@
 
                 // Only the top activity may control occluded, as we can't occlude the Keyguard
                 // if the top app doesn't want to occlude it.
-                occludedByActivity = mTopOccludesActivity != null
+                mOccluded = mTopOccludesActivity != null
                         || (mDismissingKeyguardActivity != null
                         && task.topRunningActivity() == mDismissingKeyguardActivity
                         && controller.canShowWhileOccluded(
                                 true /* dismissKeyguard */, false /* showWhenLocked */));
                 // FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD only apply for secondary display.
                 if (mDisplayId != DEFAULT_DISPLAY) {
-                    occludedByActivity |= display.canShowWithInsecureKeyguard()
+                    mOccluded |= display.canShowWithInsecureKeyguard()
                             && controller.canDismissKeyguard();
                 }
             }
 
-            mShowingDream = display.getDisplayPolicy().isShowingDreamLw() && (top != null
-                    && top.getActivityType() == ACTIVITY_TYPE_DREAM);
-            mOccluded = mShowingDream || occludedByActivity;
             mRequestDismissKeyguard = lastDismissKeyguardActivity != mDismissingKeyguardActivity
                     && !mOccluded && !mKeyguardGoingAway
                     && mDismissingKeyguardActivity != null;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 9c7c41c..4770358 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1898,6 +1898,7 @@
             // Don't do recursive work.
             return;
         }
+        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "RWC_ensureActivitiesVisible");
         mTaskSupervisor.beginActivityVisibilityUpdate();
         try {
             // First the front root tasks. In case any are not fullscreen and are in front of home.
@@ -1907,6 +1908,7 @@
             }
         } finally {
             mTaskSupervisor.endActivityVisibilityUpdate();
+            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 8a6c73a..b814ccd 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -3502,10 +3502,11 @@
         if (!checkCallingPermission(permission.CONTROL_KEYGUARD, "dismissKeyguard")) {
             throw new SecurityException("Requires CONTROL_KEYGUARD permission");
         }
-        if (!dreamHandlesConfirmKeys() && mAtmService.mKeyguardController.isShowingDream()) {
-            mAtmService.mTaskSupervisor.wakeUp("leaveDream");
-        }
         synchronized (mGlobalLock) {
+            if (!dreamHandlesConfirmKeys()
+                    && getDefaultDisplayContentLocked().getDisplayPolicy().isShowingDreamLw()) {
+                mAtmService.mTaskSupervisor.wakeUp("leaveDream");
+            }
             mPolicy.dismissKeyguardLw(callback, message);
         }
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceInfoTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceInfoTest.java
new file mode 100644
index 0000000..bacbf89
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceInfoTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static com.android.server.display.DisplayDeviceInfo.DIFF_COLOR_MODE;
+import static com.android.server.display.DisplayDeviceInfo.DIFF_COMMITTED_STATE;
+import static com.android.server.display.DisplayDeviceInfo.DIFF_HDR_SDR_RATIO;
+import static com.android.server.display.DisplayDeviceInfo.DIFF_MODE_ID;
+import static com.android.server.display.DisplayDeviceInfo.DIFF_RENDER_TIMINGS;
+import static com.android.server.display.DisplayDeviceInfo.DIFF_ROTATION;
+import static com.android.server.display.DisplayDeviceInfo.DIFF_STATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DisplayDeviceInfoTest {
+
+    @Test
+    public void testDiff_noChange() {
+        var oldDdi = createInfo();
+        var newDdi = createInfo();
+
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(0);
+    }
+
+    @Test
+    public void testDiff_state() {
+        var oldDdi = createInfo();
+        var newDdi = createInfo();
+
+        newDdi.state = Display.STATE_VR;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_STATE);
+    }
+
+    @Test
+    public void testDiff_committedState() {
+        var oldDdi = createInfo();
+        var newDdi = createInfo();
+
+        newDdi.committedState = Display.STATE_UNKNOWN;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_COMMITTED_STATE);
+    }
+
+    @Test
+    public void testDiff_colorMode() {
+        var oldDdi = createInfo();
+        var newDdi = createInfo();
+
+        newDdi.colorMode = Display.COLOR_MODE_DISPLAY_P3;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_COLOR_MODE);
+    }
+
+    @Test
+    public void testDiff_hdrSdrRatio() {
+        var oldDdi = createInfo();
+        var newDdi = createInfo();
+
+        /* First change new ratio to non-NaN */
+        newDdi.hdrSdrRatio = 2.3f;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_HDR_SDR_RATIO);
+
+        /* Then change old to be non-NaN and also distinct */
+        oldDdi.hdrSdrRatio = 1.1f;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_HDR_SDR_RATIO);
+
+        /* Now make the new one NaN and the old one non-NaN */
+        newDdi.hdrSdrRatio = Float.NaN;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_HDR_SDR_RATIO);
+    }
+
+    @Test
+    public void testDiff_rotation() {
+        var oldDdi = createInfo();
+        var newDdi = createInfo();
+
+        newDdi.rotation = Surface.ROTATION_270;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_ROTATION);
+    }
+
+    @Test
+    public void testDiff_frameRate() {
+        var oldDdi = createInfo();
+        var newDdi = createInfo();
+
+        newDdi.renderFrameRate = 123.4f;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_RENDER_TIMINGS);
+        newDdi.renderFrameRate = oldDdi.renderFrameRate;
+
+        newDdi.appVsyncOffsetNanos = 31222221;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_RENDER_TIMINGS);
+        newDdi.appVsyncOffsetNanos = oldDdi.appVsyncOffsetNanos;
+
+        newDdi.presentationDeadlineNanos = 23000000;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_RENDER_TIMINGS);
+    }
+
+    @Test
+    public void testDiff_modeId() {
+        var oldDdi = createInfo();
+        var newDdi = createInfo();
+
+        newDdi.modeId = 9;
+        assertThat(oldDdi.diff(newDdi)).isEqualTo(DIFF_MODE_ID);
+    }
+
+    private static DisplayDeviceInfo createInfo() {
+        var ddi = new DisplayDeviceInfo();
+        ddi.name = "TestDisplayDeviceInfo";
+        ddi.uniqueId = "test:51651561321";
+        ddi.width = 671;
+        ddi.height = 483;
+        ddi.modeId = 2;
+        ddi.renderFrameRate = 68.9f;
+        ddi.supportedModes = new Display.Mode[] {
+                new Display.Mode.Builder().setRefreshRate(68.9f).setResolution(671, 483).build(),
+        };
+        ddi.appVsyncOffsetNanos = 6233332;
+        ddi.presentationDeadlineNanos = 11500000;
+        ddi.rotation = Surface.ROTATION_90;
+        ddi.state = Display.STATE_ON;
+        ddi.committedState = Display.STATE_DOZE_SUSPEND;
+        return ddi;
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
index 8a33f34..1d04baa 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
@@ -570,6 +570,86 @@
         assertEquals(expectedDisplayBrightnessState, actualDisplayBrightnessState);
     }
 
+    @Test
+    public void
+            updateBrightness_constructsDisplayBrightnessState_withNoAdjustmentFlag_isSlowChange() {
+        BrightnessEvent brightnessEvent = new BrightnessEvent(DISPLAY_ID);
+        mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(
+                mContext, DISPLAY_ID, displayId -> brightnessEvent, mDisplayManagerFlags);
+        mAutomaticBrightnessStrategy.setAutomaticBrightnessController(
+                mAutomaticBrightnessController);
+        float brightness = 0.4f;
+        BrightnessReason brightnessReason = new BrightnessReason();
+        brightnessReason.setReason(BrightnessReason.REASON_AUTOMATIC);
+        when(mAutomaticBrightnessController.getAutomaticScreenBrightness(brightnessEvent))
+                .thenReturn(brightness);
+
+        // Set the state such that auto-brightness was already applied
+        mAutomaticBrightnessStrategy.setAutoBrightnessApplied(true);
+
+        // Update the auto-brightess validity state to change the isSlowChange flag
+        mAutomaticBrightnessStrategy.isAutoBrightnessValid();
+
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest =
+                mock(DisplayManagerInternal.DisplayPowerRequest.class);
+
+        DisplayBrightnessState expectedDisplayBrightnessState = new DisplayBrightnessState.Builder()
+                .setBrightness(brightness)
+                .setSdrBrightness(brightness)
+                .setBrightnessReason(brightnessReason)
+                .setDisplayBrightnessStrategyName(mAutomaticBrightnessStrategy.getName())
+                .setIsSlowChange(true)
+                .setBrightnessEvent(brightnessEvent)
+                .setBrightnessAdjustmentFlag(0)
+                .setShouldUpdateScreenBrightnessSetting(true)
+                .setIsUserInitiatedChange(true)
+                .build();
+        DisplayBrightnessState actualDisplayBrightnessState = mAutomaticBrightnessStrategy
+                .updateBrightness(new StrategyExecutionRequest(displayPowerRequest, 0.6f,
+                        /* userSetBrightnessChanged= */ true));
+        assertEquals(expectedDisplayBrightnessState, actualDisplayBrightnessState);
+    }
+
+
+    @Test
+    public void updateBrightness_autoBrightnessNotApplied_noAdjustments_isNotSlowChange() {
+        BrightnessEvent brightnessEvent = new BrightnessEvent(DISPLAY_ID);
+        mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(
+                mContext, DISPLAY_ID, displayId -> brightnessEvent, mDisplayManagerFlags);
+        mAutomaticBrightnessStrategy.setAutomaticBrightnessController(
+                mAutomaticBrightnessController);
+        float brightness = 0.4f;
+        BrightnessReason brightnessReason = new BrightnessReason();
+        brightnessReason.setReason(BrightnessReason.REASON_AUTOMATIC);
+        when(mAutomaticBrightnessController.getAutomaticScreenBrightness(brightnessEvent))
+                .thenReturn(brightness);
+
+        // Set the state such that auto-brightness was not already applied
+        mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false);
+
+        // Update the auto-brightess validity state to change the isSlowChange flag
+        mAutomaticBrightnessStrategy.isAutoBrightnessValid();
+
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest =
+                mock(DisplayManagerInternal.DisplayPowerRequest.class);
+
+        DisplayBrightnessState expectedDisplayBrightnessState = new DisplayBrightnessState.Builder()
+                .setBrightness(brightness)
+                .setSdrBrightness(brightness)
+                .setBrightnessReason(brightnessReason)
+                .setDisplayBrightnessStrategyName(mAutomaticBrightnessStrategy.getName())
+                .setIsSlowChange(false)
+                .setBrightnessEvent(brightnessEvent)
+                .setBrightnessAdjustmentFlag(0)
+                .setShouldUpdateScreenBrightnessSetting(true)
+                .setIsUserInitiatedChange(true)
+                .build();
+        DisplayBrightnessState actualDisplayBrightnessState = mAutomaticBrightnessStrategy
+                .updateBrightness(new StrategyExecutionRequest(displayPowerRequest, 0.6f,
+                        /* userSetBrightnessChanged= */ true));
+        assertEquals(expectedDisplayBrightnessState, actualDisplayBrightnessState);
+    }
+
     private void setPendingAutoBrightnessAdjustment(float pendingAutoBrightnessAdjustment) {
         Settings.System.putFloat(mContext.getContentResolver(),
                 Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingAutoBrightnessAdjustment);
diff --git a/telephony/java/com/android/internal/telephony/PhoneConstants.java b/telephony/java/com/android/internal/telephony/PhoneConstants.java
index a9ebd5c..2158f3d 100644
--- a/telephony/java/com/android/internal/telephony/PhoneConstants.java
+++ b/telephony/java/com/android/internal/telephony/PhoneConstants.java
@@ -252,4 +252,10 @@
 
     /** The key to specify the emergency service category */
     public static final String EXTRA_EMERGENCY_SERVICE_CATEGORY = "emergency_service_category";
+
+    /** The key to specify the alternate emergency URNs */
+    public static final String EXTRA_EMERGENCY_URNS = "emergency_urns";
+
+    /** The key to specify whether or not to use emergency routing */
+    public static final String EXTRA_USE_EMERGENCY_ROUTING = "use_emergency_routing";
 }
diff --git a/tests/Internal/TEST_MAPPING b/tests/Internal/TEST_MAPPING
new file mode 100644
index 0000000..20af028
--- /dev/null
+++ b/tests/Internal/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "InternalTests"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tools/localedata/extract_icu_data.py b/tools/localedata/extract_icu_data.py
index 81ac897..8f67fa8 100755
--- a/tools/localedata/extract_icu_data.py
+++ b/tools/localedata/extract_icu_data.py
@@ -22,6 +22,8 @@
 import os.path
 import sys
 
+import xml.etree.ElementTree as ElementTree
+
 
 def get_locale_parts(locale):
     """Split a locale into three parts, for langauge, script, and region."""
@@ -40,42 +42,43 @@
 
 def read_likely_subtags(input_file_name):
     """Read and parse ICU's likelySubtags.txt."""
-    with open(input_file_name) as input_file:
-        likely_script_dict = {
-            # Android's additions for pseudo-locales. These internal codes make
-            # sure that the pseudo-locales would not match other English or
-            # Arabic locales. (We can't use private-use ISO 15924 codes, since
-            # they may be used by apps for other purposes.)
-            "en_XA": "~~~A",
-            "ar_XB": "~~~B",
-            # Removed data from later versions of ICU
-            "ji": "Hebr", # Old code for Yiddish, still used in Java and Android
-        }
-        representative_locales = {
-            # Android's additions
-            "en_Latn_GB", # representative for en_Latn_001
-            "es_Latn_MX", # representative for es_Latn_419
-            "es_Latn_US", # representative for es_Latn_419 (not the best idea,
-                          # but Android has been shipping with it for quite a
-                          # while. Fortunately, MX < US, so if both exist, MX
-                          # would be chosen.)
-        }
-        for line in input_file:
-            line = line.strip(u' \n\uFEFF')
-            if line.startswith('//'):
-                continue
-            if '{' in line and '}' in line:
-                from_locale = line[:line.index('{')]
-                to_locale = line[line.index('"')+1:line.rindex('"')]
-                from_lang, from_scr, from_region = get_locale_parts(from_locale)
-                _, to_scr, to_region = get_locale_parts(to_locale)
-                if from_lang == 'und':
-                    continue  # not very useful for our purposes
-                if from_region is None and to_region not in ['001', 'ZZ']:
-                    representative_locales.add(to_locale)
-                if from_scr is None:
-                    likely_script_dict[from_locale] = to_scr
-        return likely_script_dict, frozenset(representative_locales)
+    likely_script_dict = {
+        # Android's additions for pseudo-locales. These internal codes make
+        # sure that the pseudo-locales would not match other English or
+        # Arabic locales. (We can't use private-use ISO 15924 codes, since
+        # they may be used by apps for other purposes.)
+        "en_XA": "~~~A",
+        "ar_XB": "~~~B",
+        # Removed data from later versions of ICU
+        "ji": "Hebr", # Old code for Yiddish, still used in Java and Android
+    }
+    representative_locales = {
+        # Android's additions
+        "en_Latn_GB", # representative for en_Latn_001
+        "es_Latn_MX", # representative for es_Latn_419
+        "es_Latn_US", # representative for es_Latn_419 (not the best idea,
+        # but Android has been shipping with it for quite a
+        # while. Fortunately, MX < US, so if both exist, MX
+        # would be chosen.)
+    }
+    xml_tree = ElementTree.parse(input_file_name)
+    likely_subtags = xml_tree.find('likelySubtags')
+    for child in likely_subtags:
+        from_locale = child.get('from')
+        to_locale = child.get('to')
+        # print(f'from: {from_locale} to: {to_locale}')
+        from_lang, from_scr, from_region = get_locale_parts(from_locale)
+        _, to_scr, to_region = get_locale_parts(to_locale)
+        if to_locale == "FAIL":
+            continue # "FAIL" cases are not useful here.
+        if from_lang == 'und':
+            continue  # not very useful for our purposes
+        if from_region is None and to_region not in ['001', 'ZZ']:
+            representative_locales.add(to_locale)
+        if from_scr is None:
+            likely_script_dict[from_locale] = to_scr
+
+    return likely_script_dict, frozenset(representative_locales)
 
 
 # From packLanguageOrRegion() in ResourceTypes.cpp
@@ -86,7 +89,7 @@
     elif len(inp) == 2:
         return ord(inp[0]), ord(inp[1])
     else:
-        assert len(inp) == 3
+        assert len(inp) == 3, f'Expects a 3-character string, but "{inp}" '
         base = ord(base)
         first = ord(inp[0]) - base
         second = ord(inp[1]) - base
@@ -161,9 +164,10 @@
     print('});')
 
 
-def read_and_dump_likely_data(icu_data_dir):
+def read_and_dump_likely_data(cldr_source_dir):
     """Read and dump the likely-script data."""
-    likely_subtags_txt = os.path.join(icu_data_dir, 'misc', 'likelySubtags.txt')
+    likely_subtags_txt = os.path.join(cldr_source_dir,
+                                      'common', 'supplemental', 'likelySubtags.xml')
     likely_script_dict, representative_locales = read_likely_subtags(
         likely_subtags_txt)
 
@@ -280,10 +284,11 @@
     icu_data_dir = os.path.join(
         source_root,
         'external', 'icu', 'icu4c', 'source', 'data')
+    cldr_source_dir = os.path.join(source_root, 'external', 'cldr')
 
     print('// Auto-generated by %s' % sys.argv[0])
     print()
-    likely_script_dict = read_and_dump_likely_data(icu_data_dir)
+    likely_script_dict = read_and_dump_likely_data(cldr_source_dir)
     read_and_dump_parent_data(icu_data_dir, likely_script_dict)