Merge "Add KeyguardTransitionBootInteractor." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index ab5d503..0ccdf37 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -1043,12 +1043,20 @@
     name: "device_policy_aconfig_flags",
     package: "android.app.admin.flags",
     container: "system",
+    exportable: true,
     srcs: [
         "core/java/android/app/admin/flags/flags.aconfig",
     ],
 }
 
 java_aconfig_library {
+    name: "device_policy_exported_aconfig_flags_lib",
+    aconfig_declarations: "device_policy_aconfig_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    mode: "exported",
+}
+
+java_aconfig_library {
     name: "device_policy_aconfig_flags_lib",
     aconfig_declarations: "device_policy_aconfig_flags",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl
index 3c6ff28..f2228f9 100644
--- a/core/java/android/app/ITaskStackListener.aidl
+++ b/core/java/android/app/ITaskStackListener.aidl
@@ -145,6 +145,11 @@
     void onTaskSnapshotChanged(int taskId, in TaskSnapshot snapshot);
 
     /**
+     * Called when a task snapshot become invalidated.
+     */
+    void onTaskSnapshotInvalidated(int taskId);
+
+    /**
      * Reports that an Activity received a back key press when there were no additional activities
      * on the back stack.
      *
diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java
index 0290cee..36f61fd 100644
--- a/core/java/android/app/TaskStackListener.java
+++ b/core/java/android/app/TaskStackListener.java
@@ -178,6 +178,9 @@
     }
 
     @Override
+    public void onTaskSnapshotInvalidated(int taskId) { }
+
+    @Override
     public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo)
             throws RemoteException {
     }
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 9ef8b38..46c9e78 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -21,6 +21,7 @@
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.app.admin.flags.Flags;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
@@ -176,6 +177,10 @@
      * provisioned into "affiliated" mode when on a Headless System User Mode device.
      *
      * <p>This mode adds a Profile Owner to all users other than the user the Device Owner is on.
+     *
+     * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
+     * DPCs should set the value of attribute "headless-device-owner-mode" inside the
+     * "headless-system-user" tag as "affiliated".
      */
     public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1;
 
@@ -185,6 +190,10 @@
      *
      * <p>This mode only allows a single secondary user on the device blocking the creation of
      * additional secondary users.
+     *
+     * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
+     * DPCs should set the value of attribute "headless-device-owner-mode" inside the
+     * "headless-system-user" tag as "single_user".
      */
     @FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
     public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2;
@@ -383,17 +392,30 @@
                     }
                     mSupportsTransferOwnership = true;
                 } else if (tagName.equals("headless-system-user")) {
-                    String deviceOwnerModeStringValue =
-                            parser.getAttributeValue(null, "device-owner-mode");
+                    String deviceOwnerModeStringValue = null;
+                    if (Flags.headlessSingleUserCompatibilityFix()) {
+                        deviceOwnerModeStringValue = parser.getAttributeValue(
+                                 null, "headless-device-owner-mode");
+                    }
+                    if (deviceOwnerModeStringValue == null) {
+                        deviceOwnerModeStringValue =
+                                parser.getAttributeValue(null, "device-owner-mode");
+                    }
 
-                    if (deviceOwnerModeStringValue.equalsIgnoreCase("unsupported")) {
+                    if ("unsupported".equalsIgnoreCase(deviceOwnerModeStringValue)) {
                         mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
-                    } else if (deviceOwnerModeStringValue.equalsIgnoreCase("affiliated")) {
+                    } else if ("affiliated".equalsIgnoreCase(deviceOwnerModeStringValue)) {
                         mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
-                    } else if (deviceOwnerModeStringValue.equalsIgnoreCase("single_user")) {
+                    } else if ("single_user".equalsIgnoreCase(deviceOwnerModeStringValue)) {
                         mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
                     } else {
-                        throw new XmlPullParserException("headless-system-user mode must be valid");
+                        if (Flags.headlessSingleUserCompatibilityFix()) {
+                            Log.e(TAG, "Unknown headless-system-user mode: "
+                                    + deviceOwnerModeStringValue);
+                        } else {
+                            throw new XmlPullParserException(
+                                    "headless-system-user mode must be valid");
+                        }
                     }
                 }
             }
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 18914e1..83daa45 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -303,3 +303,24 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "headless_single_user_compatibility_fix"
+    namespace: "enterprise"
+    description: "Fix for compatibility issue introduced from using single_user mode on pre-Android V builds"
+    bug: "338050276"
+    is_exported: true
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "headless_single_min_target_sdk"
+    namespace: "enterprise"
+    description: "Only allow DPCs targeting Android V to provision into single user mode"
+    bug: "338588825"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 205f1e9..45591d7 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -248,3 +248,11 @@
     bug: "316916801"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "package_restart_query_disabled_by_default"
+    namespace: "package_manager_service"
+    description: "Feature flag to register broadcast receiver only support package restart query."
+    bug: "300309050"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java
index de5fc7f..58ef5ef 100644
--- a/core/java/android/view/InputWindowHandle.java
+++ b/core/java/android/view/InputWindowHandle.java
@@ -67,7 +67,7 @@
             InputConfig.SPY,
             InputConfig.INTERCEPTS_STYLUS,
             InputConfig.CLONE,
-            InputConfig.SENSITIVE_FOR_TRACING,
+            InputConfig.SENSITIVE_FOR_PRIVACY,
     })
     public @interface InputConfigFlags {}
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 1d84375..ca8eaf9 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -12696,9 +12696,11 @@
             return;
         }
 
+        boolean traceFrameRate = false;
         try {
             if (mLastPreferredFrameRate != preferredFrameRate) {
-                if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+                traceFrameRate = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+                if (traceFrameRate) {
                     Trace.traceBegin(
                             Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRate "
                                 + preferredFrameRate + " compatibility "
@@ -12713,7 +12715,9 @@
         } catch (Exception e) {
             Log.e(mTag, "Unable to set frame rate", e);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+            if (traceFrameRate) {
+                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+            }
         }
     }
 
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 0bc2430..f22e8f5 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -4365,7 +4365,8 @@
         public static final int INPUT_FEATURE_SPY = 1 << 2;
 
         /**
-         * Input feature used to indicate that this window is sensitive for tracing.
+         * Input feature used to indicate that this window is privacy sensitive. This may be used
+         * to redact input interactions from tracing or screen mirroring.
          * <p>
          * A window that uses {@link LayoutParams#FLAG_SECURE} will automatically be treated as
          * a sensitive for input tracing, but this input feature can be set on windows that don't
@@ -4378,7 +4379,7 @@
          *
          * @hide
          */
-        public static final int INPUT_FEATURE_SENSITIVE_FOR_TRACING = 1 << 3;
+        public static final int INPUT_FEATURE_SENSITIVE_FOR_PRIVACY = 1 << 3;
 
         /**
          * An internal annotation for flags that can be specified to {@link #inputFeatures}.
@@ -4392,7 +4393,7 @@
                 INPUT_FEATURE_NO_INPUT_CHANNEL,
                 INPUT_FEATURE_DISABLE_USER_ACTIVITY,
                 INPUT_FEATURE_SPY,
-                INPUT_FEATURE_SENSITIVE_FOR_TRACING,
+                INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
         })
         public @interface InputFeatureFlags {
         }
diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java
index 7ac553c..3af1dd7 100644
--- a/core/java/com/android/internal/content/PackageMonitor.java
+++ b/core/java/com/android/internal/content/PackageMonitor.java
@@ -22,6 +22,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.Flags;
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Bundle;
@@ -68,7 +69,8 @@
 
     @UnsupportedAppUsage
     public PackageMonitor() {
-        this(true);
+        // If the feature flag is enabled, set mSupportsPackageRestartQuery to false by default
+        this(!Flags.packageRestartQueryDisabledByDefault());
     }
 
     /**
diff --git a/core/res/res/layout/side_fps_toast.xml b/core/res/res/layout/side_fps_toast.xml
index 96860b0..2c35c9b 100644
--- a/core/res/res/layout/side_fps_toast.xml
+++ b/core/res/res/layout/side_fps_toast.xml
@@ -18,28 +18,26 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
-              android:minWidth="350dp"
               android:layout_gravity="center"
+              android:minWidth="350dp"
               android:background="@color/side_fps_toast_background">
     <TextView
-        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
+        android:layout_width="0dp"
+        android:layout_weight="6"
         android:text="@string/fp_power_button_enrollment_title"
-        android:singleLine="true"
-        android:ellipsize="end"
         android:textColor="@color/side_fps_text_color"
         android:paddingLeft="20dp"/>
     <Space
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:layout_weight="1"/>
+        android:layout_width="5dp"
+        android:layout_height="match_parent" />
     <Button
         android:id="@+id/turn_off_screen"
-        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
+        android:layout_width="0dp"
+        android:layout_weight="3"
         android:text="@string/fp_power_button_enrollment_button_text"
-        android:paddingRight="20dp"
         style="?android:attr/buttonBarNegativeButtonStyle"
         android:textColor="@color/side_fps_button_color"
-        android:maxLines="1"/>
+        />
 </LinearLayout>
\ No newline at end of file
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 5e900f7..27b756d 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -842,7 +842,8 @@
             that created the task, and therefore there will only be one instance of this activity
             in a task. In contrast to the {@code singleTask} launch mode, this activity can be
             started in multiple instances in different tasks if the
-            {@code FLAG_ACTIVITY_MULTIPLE_TASK} or {@code FLAG_ACTIVITY_NEW_DOCUMENT} is set.-->
+            {@code FLAG_ACTIVITY_MULTIPLE_TASK} or {@code FLAG_ACTIVITY_NEW_DOCUMENT} is set.
+            This enum value is introduced in API level 31. -->
         <enum name="singleInstancePerTask" value="4" />
     </attr>
     <!-- Specify the orientation an activity should be run in.  If not
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index e38038e..59092d4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -3137,11 +3137,9 @@
     private static EmbeddedActivityWindowInfo translateActivityWindowInfo(
             @NonNull Activity activity, @NonNull ActivityWindowInfo activityWindowInfo) {
         final boolean isEmbedded = activityWindowInfo.isEmbedded();
-        final Rect activityBounds = new Rect(activity.getResources().getConfiguration()
-                .windowConfiguration.getBounds());
         final Rect taskBounds = new Rect(activityWindowInfo.getTaskBounds());
         final Rect activityStackBounds = new Rect(activityWindowInfo.getTaskFragmentBounds());
-        return new EmbeddedActivityWindowInfo(activity, isEmbedded, activityBounds, taskBounds,
+        return new EmbeddedActivityWindowInfo(activity, isEmbedded, taskBounds,
                 activityStackBounds);
     }
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 8bc3a30..a525877 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -1570,8 +1570,6 @@
         mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
 
         final boolean isEmbedded = true;
-        final Rect activityBounds = mActivity.getResources().getConfiguration().windowConfiguration
-                .getBounds();
         final Rect taskBounds = new Rect(0, 0, 1000, 2000);
         final Rect activityStackBounds = new Rect(0, 0, 500, 2000);
         doReturn(isEmbedded).when(mActivityWindowInfo).isEmbedded();
@@ -1579,7 +1577,7 @@
         doReturn(activityStackBounds).when(mActivityWindowInfo).getTaskFragmentBounds();
 
         final EmbeddedActivityWindowInfo expected = new EmbeddedActivityWindowInfo(mActivity,
-                isEmbedded, activityBounds, taskBounds, activityStackBounds);
+                isEmbedded, taskBounds, activityStackBounds);
         assertEquals(expected, mSplitController.getEmbeddedActivityWindowInfo(mActivity));
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 12dce5b..8b2d0dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -45,7 +45,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.util.Preconditions;
-import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
@@ -64,6 +63,9 @@
     private static final String TAG = PipTransition.class.getSimpleName();
     private static final String PIP_TASK_TOKEN = "pip_task_token";
     private static final String PIP_TASK_LEASH = "pip_task_leash";
+    private static final String PIP_START_TX = "pip_start_tx";
+    private static final String PIP_FINISH_TX = "pip_finish_tx";
+    private static final String PIP_DESTINATION_BOUNDS = "pip_dest_bounds";
 
     /**
      * The fixed start delay in ms when fading out the content overlay from bounds animation.
@@ -98,6 +100,8 @@
     private WindowContainerToken mPipTaskToken;
     @Nullable
     private SurfaceControl mPipLeash;
+    @Nullable
+    private Transitions.TransitionFinishCallback mFinishCallback;
 
     public PipTransition(
             Context context,
@@ -223,7 +227,6 @@
             return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback);
         } else if (transition == mResizeTransition) {
             mResizeTransition = null;
-            mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS);
             return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback);
         }
 
@@ -246,31 +249,27 @@
             return false;
         }
         SurfaceControl pipLeash = pipChange.getLeash();
-        Rect destinationBounds = pipChange.getEndAbsBounds();
 
         // Even though the final bounds and crop are applied with finishTransaction since
         // this is a visible change, we still need to handle the app draw coming in. Snapshot
         // covering app draw during collection will be removed by startTransaction. So we make
-        // the crop equal to the final bounds and then scale the leash back to starting bounds.
+        // the crop equal to the final bounds and then let the current
+        // animator scale the leash back to starting bounds.
+        // Note: animator is responsible for applying the startTx but NOT finishTx.
         startTransaction.setWindowCrop(pipLeash, pipChange.getEndAbsBounds().width(),
                 pipChange.getEndAbsBounds().height());
-        startTransaction.setScale(pipLeash,
-                (float) mPipBoundsState.getBounds().width() / destinationBounds.width(),
-                (float) mPipBoundsState.getBounds().height() / destinationBounds.height());
-        startTransaction.apply();
 
-        finishTransaction.setScale(pipLeash,
-                (float) mPipBoundsState.getBounds().width() / destinationBounds.width(),
-                (float) mPipBoundsState.getBounds().height() / destinationBounds.height());
-
-        // We are done with the transition, but will continue animating leash to final bounds.
-        finishCallback.onTransitionFinished(null);
-
-        // Animate the pip leash with the new buffer
-        final int duration = mContext.getResources().getInteger(
-                R.integer.config_pipResizeAnimationDuration);
         // TODO: b/275910498 Couple this routine with a new implementation of the PiP animator.
-        startResizeAnimation(pipLeash, mPipBoundsState.getBounds(), destinationBounds, duration);
+        // Classes interested in continuing the animation would subscribe to this state update
+        // getting info such as endBounds, startTx, and finishTx as an extra Bundle once
+        // animators are in place. Once done state needs to be updated to CHANGED_PIP_BOUNDS.
+        Bundle extra = new Bundle();
+        extra.putParcelable(PIP_START_TX, startTransaction);
+        extra.putParcelable(PIP_FINISH_TX, finishTransaction);
+        extra.putParcelable(PIP_DESTINATION_BOUNDS, pipChange.getEndAbsBounds());
+
+        mFinishCallback = finishCallback;
+        mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS, extra);
         return true;
     }
 
@@ -285,12 +284,17 @@
         WindowContainerToken pipTaskToken = pipChange.getContainer();
         SurfaceControl pipLeash = pipChange.getLeash();
 
+        if (pipTaskToken == null || pipLeash == null) {
+            return false;
+        }
+
         PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
         Rect srcRectHint = params.getSourceRectHint();
         Rect startBounds = pipChange.getStartAbsBounds();
         Rect destinationBounds = pipChange.getEndAbsBounds();
 
         WindowContainerTransaction finishWct = new WindowContainerTransaction();
+        SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
 
         if (PipBoundsAlgorithm.isSourceRectHintValidForEnterPip(srcRectHint, destinationBounds)) {
             final float scale = (float) destinationBounds.width() / srcRectHint.width();
@@ -316,19 +320,17 @@
                     .reparent(overlayLeash, pipLeash)
                     .setLayer(overlayLeash, Integer.MAX_VALUE);
 
-            if (pipTaskToken != null) {
-                SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-                tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
-                                this::onClientDrawAtTransitionEnd)
-                        .setScale(overlayLeash, 1f, 1f)
-                        .setPosition(overlayLeash,
-                                (destinationBounds.width() - overlaySize) / 2f,
-                                (destinationBounds.height() - overlaySize) / 2f);
-                finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
-            }
+            // Overlay needs to be adjusted once a new draw comes in resetting surface transform.
+            tx.setScale(overlayLeash, 1f, 1f);
+            tx.setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f,
+                    (destinationBounds.height() - overlaySize) / 2f);
         }
         startTransaction.apply();
 
+        tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
+                        this::onClientDrawAtTransitionEnd);
+        finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
+
         // Note that finishWct should be free of any actual WM state changes; we are using
         // it for syncing with the client draw after delayed configuration changes are dispatched.
         finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct);
@@ -412,14 +414,6 @@
         return true;
     }
 
-    /**
-     * TODO: b/275910498 Use a new implementation of the PiP animator here.
-     */
-    private void startResizeAnimation(SurfaceControl leash, Rect startBounds,
-            Rect endBounds, int duration) {
-        mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
-    }
-
     //
     // Various helpers to resolve transition requests and infos
     //
@@ -537,6 +531,15 @@
                 mPipTransitionState.mPipTaskToken = null;
                 mPipTransitionState.mPinnedTaskLeash = null;
                 break;
+            case PipTransitionState.CHANGED_PIP_BOUNDS:
+                // Note: this might not be the end of the animation, rather animator just finished
+                // adjusting startTx and finishTx and is ready to finishTransition(). The animator
+                // can still continue playing the leash into the destination bounds after.
+                if (mFinishCallback != null) {
+                    mFinishCallback.onTransitionFinished(null);
+                    mFinishCallback = null;
+                }
+                break;
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
index f7bc622..9a9c59e2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
@@ -257,6 +257,7 @@
     private String stateToString() {
         switch (mState) {
             case UNDEFINED: return "undefined";
+            case SWIPING_TO_PIP: return "swiping_to_pip";
             case ENTERING_PIP: return "entering-pip";
             case ENTERED_PIP: return "entered-pip";
             case CHANGING_PIP_BOUNDS: return "changing-bounds";
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
index b52faba..4c76fb0 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -44,8 +44,15 @@
     /**
      * @hide
      */
-    @IntDef(prefix = { "POLLING_LOOP_TYPE_"}, value = { POLLING_LOOP_TYPE_A, POLLING_LOOP_TYPE_B,
-            POLLING_LOOP_TYPE_F, POLLING_LOOP_TYPE_OFF, POLLING_LOOP_TYPE_ON })
+    @IntDef(prefix = { "POLLING_LOOP_TYPE_"},
+        value = {
+            POLLING_LOOP_TYPE_A,
+            POLLING_LOOP_TYPE_B,
+            POLLING_LOOP_TYPE_F,
+            POLLING_LOOP_TYPE_OFF,
+            POLLING_LOOP_TYPE_ON,
+            POLLING_LOOP_TYPE_UNKNOWN
+        })
     @Retention(RetentionPolicy.SOURCE)
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
     public @interface PollingFrameType {}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt
deleted file mode 100644
index 0db01e8..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.framework.theme
-
-import android.content.Context
-import android.os.Build
-import androidx.annotation.VisibleForTesting
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-
-data class SettingsColorScheme(
-    val categoryTitle: Color = Color.Unspecified,
-    val surface: Color = Color.Unspecified,
-    val surfaceHeader: Color = Color.Unspecified,
-    val secondaryText: Color = Color.Unspecified,
-    val primaryContainer: Color = Color.Unspecified,
-    val onPrimaryContainer: Color = Color.Unspecified,
-)
-
-internal val LocalColorScheme = staticCompositionLocalOf { SettingsColorScheme() }
-
-@Composable
-internal fun settingsColorScheme(isDarkTheme: Boolean): SettingsColorScheme {
-    val context = LocalContext.current
-    return remember(isDarkTheme) {
-        when {
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
-                if (isDarkTheme) dynamicDarkColorScheme(context)
-                else dynamicLightColorScheme(context)
-            }
-            isDarkTheme -> darkColorScheme()
-            else -> lightColorScheme()
-        }
-    }
-}
-
-/**
- * Creates a light dynamic color scheme.
- *
- * Use this function to create a color scheme based off the system wallpaper. If the developer
- * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a
- * light theme variant.
- *
- * @param context The context required to get system resource data.
- */
-@VisibleForTesting
-internal fun dynamicLightColorScheme(context: Context): SettingsColorScheme {
-    val tonalPalette = dynamicTonalPalette(context)
-    return SettingsColorScheme(
-        categoryTitle = tonalPalette.primary40,
-        surface = tonalPalette.neutral99,
-        surfaceHeader = tonalPalette.neutral90,
-        secondaryText = tonalPalette.neutralVariant30,
-        primaryContainer = tonalPalette.primary90,
-        onPrimaryContainer = tonalPalette.neutral10,
-    )
-}
-
-/**
- * Creates a dark dynamic color scheme.
- *
- * Use this function to create a color scheme based off the system wallpaper. If the developer
- * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a dark
- * theme variant.
- *
- * @param context The context required to get system resource data.
- */
-@VisibleForTesting
-internal fun dynamicDarkColorScheme(context: Context): SettingsColorScheme {
-    val tonalPalette = dynamicTonalPalette(context)
-    return SettingsColorScheme(
-        categoryTitle = tonalPalette.primary90,
-        surface = tonalPalette.neutral20,
-        surfaceHeader = tonalPalette.neutral30,
-        secondaryText = tonalPalette.neutralVariant80,
-        primaryContainer = tonalPalette.secondary90,
-        onPrimaryContainer = tonalPalette.neutral10,
-    )
-}
-
-@VisibleForTesting
-internal fun darkColorScheme(): SettingsColorScheme {
-    val tonalPalette = tonalPalette()
-    return SettingsColorScheme(
-        categoryTitle = tonalPalette.primary90,
-        surface = tonalPalette.neutral20,
-        surfaceHeader = tonalPalette.neutral30,
-        secondaryText = tonalPalette.neutralVariant80,
-        primaryContainer = tonalPalette.secondary90,
-        onPrimaryContainer = tonalPalette.neutral10,
-    )
-}
-
-@VisibleForTesting
-internal fun lightColorScheme(): SettingsColorScheme {
-    val tonalPalette = tonalPalette()
-    return SettingsColorScheme(
-        categoryTitle = tonalPalette.primary40,
-        surface = tonalPalette.neutral99,
-        surfaceHeader = tonalPalette.neutral90,
-        secondaryText = tonalPalette.neutralVariant30,
-        primaryContainer = tonalPalette.primary90,
-        onPrimaryContainer = tonalPalette.neutral10,
-    )
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt
index d14b960..d9f82e8 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt
@@ -21,7 +21,6 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.ReadOnlyComposable
 
 /**
  * The Material 3 Theme for Settings.
@@ -35,17 +34,9 @@
         typography = rememberSettingsTypography(),
     ) {
         CompositionLocalProvider(
-            LocalColorScheme provides settingsColorScheme(isDarkTheme),
             LocalContentColor provides MaterialTheme.colorScheme.onSurface,
         ) {
             content()
         }
     }
 }
-
-object SettingsTheme {
-    val colorScheme: SettingsColorScheme
-        @Composable
-        @ReadOnlyComposable
-        get() = LocalColorScheme.current
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
index 979cf3b..70d353d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
@@ -88,9 +88,9 @@
         interactionSource = remember(actionButton) { MutableInteractionSource() },
         shape = RectangleShape,
         colors = ButtonDefaults.filledTonalButtonColors(
-            containerColor = SettingsTheme.colorScheme.surface,
-            contentColor = SettingsTheme.colorScheme.categoryTitle,
-            disabledContainerColor = SettingsTheme.colorScheme.surface,
+            containerColor = MaterialTheme.colorScheme.surface,
+            contentColor = MaterialTheme.colorScheme.primary,
+            disabledContainerColor = MaterialTheme.colorScheme.surface,
         ),
         contentPadding = PaddingValues(horizontal = 4.dp, vertical = 20.dp),
     ) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
index d08d97e..0546719 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
@@ -83,7 +83,7 @@
     Card(
         shape = CornerExtraSmall,
         colors = CardDefaults.cardColors(
-            containerColor = containerColor.takeOrElse { SettingsTheme.colorScheme.surface },
+            containerColor = containerColor.takeOrElse { MaterialTheme.colorScheme.surface },
         ),
         modifier = Modifier
             .fillMaxWidth()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 706bd0a..36cd136 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -74,7 +74,6 @@
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.framework.theme.SettingsDimension
-import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.framework.theme.settingsBackground
 import kotlin.math.abs
 import kotlin.math.max
@@ -142,7 +141,7 @@
 @Composable
 private fun topAppBarColors() = TopAppBarColors(
     containerColor = MaterialTheme.colorScheme.settingsBackground,
-    scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
+    scrolledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
     navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
     titleContentColor = MaterialTheme.colorScheme.onSurface,
     actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
index 6f2c38c..60814bf 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
@@ -51,8 +51,8 @@
             .clip(SettingsShape.CornerMedium)
             .background(
                 color = lerp(
-                    start = SettingsTheme.colorScheme.primaryContainer,
-                    stop = SettingsTheme.colorScheme.surface,
+                    start = MaterialTheme.colorScheme.primaryContainer,
+                    stop = MaterialTheme.colorScheme.surface,
                     fraction = colorFraction,
                 ),
             ),
@@ -61,8 +61,8 @@
             text = title,
             style = MaterialTheme.typography.labelLarge,
             color = lerp(
-                start = SettingsTheme.colorScheme.onPrimaryContainer,
-                stop = SettingsTheme.colorScheme.secondaryText,
+                start = MaterialTheme.colorScheme.onPrimaryContainer,
+                stop = MaterialTheme.colorScheme.onSurface,
                 fraction = colorFraction,
             ),
         )
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
index 6aac5bf3..48cd145 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
@@ -46,7 +46,7 @@
             end = SettingsDimension.itemPaddingEnd,
             bottom = 8.dp,
         ),
-        color = SettingsTheme.colorScheme.categoryTitle,
+        color = MaterialTheme.colorScheme.primary,
         style = MaterialTheme.typography.labelMedium,
     )
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
index 930d0a1..99b2524 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
@@ -37,7 +37,6 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.unit.DpOffset
 import com.android.settingslib.spa.framework.theme.SettingsDimension
-import com.android.settingslib.spa.framework.theme.SettingsTheme
 
 @Composable
 fun CopyableBody(body: String) {
@@ -78,7 +77,7 @@
                 top = SettingsDimension.itemPaddingAround,
                 bottom = SettingsDimension.buttonPaddingVertical,
             ),
-        color = SettingsTheme.colorScheme.categoryTitle,
+        color = MaterialTheme.colorScheme.primary,
         style = MaterialTheme.typography.labelMedium,
     )
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
index d423d9f..6e5f32e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
@@ -47,7 +47,6 @@
         modifier = Modifier
             .padding(vertical = SettingsDimension.paddingTiny)
             .contentDescription(contentDescription),
-        color = MaterialTheme.colorScheme.onSurface,
         style = MaterialTheme.typography.titleMedium.withWeight(useMediumWeight),
     )
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt
deleted file mode 100644
index f3f89e0..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.framework.theme
-
-import android.content.Context
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import androidx.compose.ui.graphics.Color
-
-@RunWith(AndroidJUnit4::class)
-class SettingsColorsTest {
-    private val context: Context = ApplicationProvider.getApplicationContext()
-
-    @Test
-    fun testDynamicTheme() {
-        // The dynamic color could be different in different device, just check basic restrictions:
-        // 1. text color is different with surface color
-        // 2. primary / spinner color is different with its on-item color
-        val ls = dynamicLightColorScheme(context)
-        assertThat(ls.categoryTitle).isNotEqualTo(ls.surface)
-        assertThat(ls.secondaryText).isNotEqualTo(ls.surface)
-        assertThat(ls.primaryContainer).isNotEqualTo(ls.onPrimaryContainer)
-
-        val ds = dynamicDarkColorScheme(context)
-        assertThat(ds.categoryTitle).isNotEqualTo(ds.surface)
-        assertThat(ds.secondaryText).isNotEqualTo(ds.surface)
-        assertThat(ds.primaryContainer).isNotEqualTo(ds.onPrimaryContainer)
-    }
-
-    @Test
-    fun testStaticTheme() {
-        val ls = lightColorScheme()
-        assertThat(ls.categoryTitle).isEqualTo(Color(red = 103, green = 80, blue = 164))
-        assertThat(ls.surface).isEqualTo(Color(red = 255, green = 251, blue = 254))
-        assertThat(ls.surfaceHeader).isEqualTo(Color(red = 230, green = 225, blue = 229))
-        assertThat(ls.secondaryText).isEqualTo(Color(red = 73, green = 69, blue = 79))
-        assertThat(ls.primaryContainer).isEqualTo(Color(red = 234, green = 221, blue = 255))
-        assertThat(ls.onPrimaryContainer).isEqualTo(Color(red = 28, green = 27, blue = 31))
-
-        val ds = darkColorScheme()
-        assertThat(ds.categoryTitle).isEqualTo(Color(red = 234, green = 221, blue = 255))
-        assertThat(ds.surface).isEqualTo(Color(red = 49, green = 48, blue = 51))
-        assertThat(ds.surfaceHeader).isEqualTo(Color(red = 72, green = 70, blue = 73))
-        assertThat(ds.secondaryText).isEqualTo(Color(red = 202, green = 196, blue = 208))
-        assertThat(ds.primaryContainer).isEqualTo(Color(red = 232, green = 222, blue = 248))
-        assertThat(ds.onPrimaryContainer).isEqualTo(Color(red = 28, green = 27, blue = 31))
-    }
-}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
index 68da143..bededf0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.Dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.viewmodel.compose.viewModel
 import com.android.settingslib.spa.framework.compose.LifecycleEffect
 import com.android.settingslib.spa.framework.compose.LogCompositions
@@ -49,7 +50,6 @@
 import com.android.settingslib.spaprivileged.model.app.AppRecord
 import com.android.settingslib.spaprivileged.model.app.IAppListViewModel
 import com.android.settingslib.spaprivileged.model.app.userId
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.MutableStateFlow
 
 private const val TAG = "AppList"
@@ -95,9 +95,9 @@
     LogCompositions(TAG, config.userIds.toString())
     val viewModel = viewModelSupplier()
     Column(Modifier.fillMaxSize()) {
-        val optionsState = viewModel.spinnerOptionsFlow.collectAsState(null, Dispatchers.IO)
+        val optionsState = viewModel.spinnerOptionsFlow.collectAsStateWithLifecycle(null)
         SpinnerOptions(optionsState, viewModel.optionFlow)
-        val appListData = viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO)
+        val appListData = viewModel.appListDataFlow.collectAsStateWithLifecycle(null)
         listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage)
     }
 }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
index 5a6c0a1..dd7c036 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
@@ -27,8 +27,6 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.testutils.delay
-import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -71,9 +69,8 @@
                 DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {}
             }
         }
-        composeTestRule.delay()
 
-        assertThat(registeredBroadcastReceiver).isNotNull()
+        composeTestRule.waitUntil { registeredBroadcastReceiver != null }
     }
 
     @Test
@@ -91,9 +88,8 @@
         }
 
         registeredBroadcastReceiver!!.onReceive(context, Intent())
-        composeTestRule.delay()
 
-        assertThat(onReceiveIsCalled).isTrue()
+        composeTestRule.waitUntil { onReceiveIsCalled }
     }
 
     private companion object {
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt
index 70b38fe..cd747cc1 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt
@@ -102,7 +102,8 @@
         delay(100)
         value = true
 
-        assertThat(listDeferred.await()).containsExactly(false, true).inOrder()
+        assertThat(listDeferred.await())
+            .containsAtLeast(false, true).inOrder()
     }
 
     private companion object {
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt
index 29a89be..ecc92f8 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt
@@ -102,7 +102,8 @@
         delay(100)
         value = true
 
-        assertThat(listDeferred.await()).containsExactly(false, true).inOrder()
+        assertThat(listDeferred.await())
+            .containsAtLeast(false, true).inOrder()
     }
 
     private companion object {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 4e52c77..cb6a930 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -176,6 +176,22 @@
     }
 
     /**
+     * Sync device status of the pair of the hearing aid if needed.
+     *
+     * @param device the remote device
+     */
+    public synchronized void syncDeviceWithinHearingAidSetIfNeeded(CachedBluetoothDevice device,
+            int state, int profileId) {
+        if (profileId == BluetoothProfile.HAP_CLIENT
+                || profileId == BluetoothProfile.HEARING_AID
+                || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) {
+            if (state == BluetoothProfile.STATE_CONNECTED) {
+                mHearingAidDeviceManager.syncDeviceIfNeeded(device);
+            }
+        }
+    }
+
+    /**
      * Search for existing sub device {@link CachedBluetoothDevice}.
      *
      * @param device the address of the Bluetooth device
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index 1069b71..9bf42f9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -15,6 +15,8 @@
  */
 package com.android.settingslib.bluetooth;
 
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothHapClient;
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
@@ -108,6 +110,10 @@
         return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
     }
 
+    private boolean isValidGroupId(int groupId) {
+        return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+    }
+
     private CachedBluetoothDevice getCachedDevice(long hiSyncId) {
         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
@@ -258,6 +264,27 @@
         }
     }
 
+    void syncDeviceIfNeeded(CachedBluetoothDevice device) {
+        final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
+        final HapClientProfile hap = profileManager.getHapClientProfile();
+        // Sync preset if device doesn't support synchronization on the remote side
+        if (hap != null && !hap.supportsSynchronizedPresets(device.getDevice())) {
+            final CachedBluetoothDevice mainDevice = findMainDevice(device);
+            if (mainDevice != null) {
+                int mainPresetIndex = hap.getActivePresetIndex(mainDevice.getDevice());
+                int presetIndex = hap.getActivePresetIndex(device.getDevice());
+                if (mainPresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE
+                        && mainPresetIndex != presetIndex) {
+                    if (DEBUG) {
+                        Log.d(TAG, "syncing preset from " + presetIndex + "->"
+                                + mainPresetIndex + ", device=" + device);
+                    }
+                    hap.selectPreset(device.getDevice(), mainPresetIndex);
+                }
+            }
+        }
+    }
+
     private void setAudioRoutingConfig(CachedBluetoothDevice device) {
         AudioDeviceAttributes hearingDeviceAttributes =
                 mRoutingHelper.getMatchedHearingDeviceAttributes(device);
@@ -326,7 +353,19 @@
     }
 
     CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
+        if (device == null || mCachedDevices == null) {
+            return null;
+        }
+
         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
+            if (isValidGroupId(cachedDevice.getGroupId())) {
+                Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
+                for (CachedBluetoothDevice memberDevice : memberSet) {
+                    if (memberDevice != null && memberDevice.equals(device)) {
+                        return cachedDevice;
+                    }
+                }
+            }
             if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
                 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
                 if (subDevice != null && subDevice.equals(device)) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 4055986..8dfeb55 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -408,6 +408,8 @@
             boolean needDispatchProfileConnectionState = true;
             if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
                     || cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+                mDeviceManager.syncDeviceWithinHearingAidSetIfNeeded(cachedDevice, newState,
+                        mProfile.getProfileId());
                 needDispatchProfileConnectionState = !mDeviceManager
                         .onProfileConnectionStateChangedIfProcessed(cachedDevice, newState,
                         mProfile.getProfileId());
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
index 68f471d..d198136 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
@@ -45,14 +45,13 @@
             .buffer(capacity = Channel.CONFLATED)
 
 /** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */
-val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?>
+val MediaSessionManager.defaultRemoteSessionChanged: Flow<MediaSession.Token?>
     get() =
         callbackFlow {
                 val callback =
                     object : MediaSessionManager.RemoteSessionCallback {
-                        override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) {
-                            launch { send(sessionToken) }
-                        }
+                        override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) =
+                            Unit
 
                         override fun onDefaultRemoteSessionChanged(
                             sessionToken: MediaSession.Token?
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
index e4ac9fe..195ccfc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
@@ -21,6 +21,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.bluetooth.headsetAudioModeChanges
 import com.android.settingslib.media.session.activeMediaChanges
+import com.android.settingslib.media.session.defaultRemoteSessionChanged
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.settingslib.volume.shared.model.AudioManagerEvent
 import kotlin.coroutines.CoroutineContext
@@ -59,6 +60,9 @@
 
     override val activeSessions: StateFlow<List<MediaController>> =
         merge(
+                mediaSessionManager.defaultRemoteSessionChanged.map {
+                    mediaSessionManager.getActiveSessions(null)
+                },
                 mediaSessionManager.activeMediaChanges.filterNotNull(),
                 localBluetoothManager?.headsetAudioModeChanges?.map {
                     mediaSessionManager.getActiveSessions(null)
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index aa5a298..1f0da90 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -72,14 +72,18 @@
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
 
-    private final static long HISYNCID1 = 10;
-    private final static long HISYNCID2 = 11;
-    private final static String DEVICE_NAME_1 = "TestName_1";
-    private final static String DEVICE_NAME_2 = "TestName_2";
-    private final static String DEVICE_ALIAS_1 = "TestAlias_1";
-    private final static String DEVICE_ALIAS_2 = "TestAlias_2";
-    private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11";
-    private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22";
+    private static final long HISYNCID1 = 10;
+    private static final long HISYNCID2 = 11;
+    private static final int GROUP_ID_1 = 20;
+    private static final int GROUP_ID_2 = 21;
+    private static final int PRESET_INDEX_1 = 1;
+    private static final int PRESET_INDEX_2 = 2;
+    private static final String DEVICE_NAME_1 = "TestName_1";
+    private static final String DEVICE_NAME_2 = "TestName_2";
+    private static final String DEVICE_ALIAS_1 = "TestAlias_1";
+    private static final String DEVICE_ALIAS_2 = "TestAlias_2";
+    private static final String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11";
+    private static final String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22";
     private final BluetoothClass DEVICE_CLASS =
             createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE);
     private final Context mContext = ApplicationProvider.getApplicationContext();
@@ -706,14 +710,73 @@
     }
 
     @Test
-    public void findMainDevice() {
+    public void findMainDevice_sameHiSyncId() {
         when(mCachedDevice1.getHiSyncId()).thenReturn(HISYNCID1);
         when(mCachedDevice2.getHiSyncId()).thenReturn(HISYNCID1);
         mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
         mCachedDevice1.setSubDevice(mCachedDevice2);
 
-        assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).
-                isEqualTo(mCachedDevice1);
+        assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).isEqualTo(
+                mCachedDevice1);
+    }
+
+    @Test
+    public void findMainDevice_sameGroupId() {
+        when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+        when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2);
+        mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+        mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+        assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).isEqualTo(
+                mCachedDevice1);
+    }
+
+    @Test
+    public void syncDeviceWithinSet_synchronized_differentPresetIndex_shouldNotSync() {
+        when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1);
+        when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_2);
+        when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(true);
+        when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(true);
+        when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+        when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2);
+        mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+        mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+        mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice1);
+
+        verify(mHapClientProfile, never()).selectPreset(any(), anyInt());
+    }
+
+    @Test
+    public void syncDeviceWithinSet_unsynchronized_samePresetIndex_shouldNotSync() {
+        when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1);
+        when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_1);
+        when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(false);
+        when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(false);
+        when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+        when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2);
+        mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+        mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+        mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice1);
+
+        verify(mHapClientProfile, never()).selectPreset(any(), anyInt());
+    }
+
+    @Test
+    public void syncDeviceWithinSet_unsynchronized_differentPresetIndex_shouldSync() {
+        when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1);
+        when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_2);
+        when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(false);
+        when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(false);
+        when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+        when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2);
+        mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+        mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+        mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice2);
+
+        verify(mHapClientProfile).selectPreset(mDevice2, PRESET_INDEX_1);
     }
 
     private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java
index cef0835..6ff90ba 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java
@@ -28,6 +28,7 @@
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothPan;
@@ -55,7 +56,9 @@
 @RunWith(RobolectricTestRunner.class)
 @Config(shadows = {ShadowBluetoothAdapter.class})
 public class LocalBluetoothProfileManagerTest {
-    private final static long HISYNCID = 10;
+    private static final long HISYNCID = 10;
+
+    private static final int GROUP_ID = 1;
     @Mock
     private LocalBluetoothManager mBtManager;
     @Mock
@@ -201,7 +204,8 @@
      * CachedBluetoothDeviceManager method
      */
     @Test
-    public void stateChangedHandler_receiveHAPConnectionStateChanged_shouldDispatchDeviceManager() {
+    public void
+            stateChangedHandler_receiveHearingAidConnectionStateChanged_dispatchDeviceManager() {
         mShadowBluetoothAdapter.setSupportedProfiles(generateList(
                 new int[] {BluetoothProfile.HEARING_AID}));
         mProfileManager.updateLocalProfiles();
@@ -219,6 +223,28 @@
     }
 
     /**
+     * Verify BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED with uuid intent will dispatch
+     * to {@link CachedBluetoothDeviceManager} method
+     */
+    @Test
+    public void stateChangedHandler_receiveHapClientConnectionStateChanged_dispatchDeviceManager() {
+        mShadowBluetoothAdapter.setSupportedProfiles(generateList(
+                new int[] {BluetoothProfile.HAP_CLIENT}));
+        mProfileManager.updateLocalProfiles();
+        when(mCachedBluetoothDevice.getGroupId()).thenReturn(GROUP_ID);
+
+        mIntent = new Intent(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
+        mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+        mIntent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTING);
+        mIntent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
+
+        mContext.sendBroadcast(mIntent);
+
+        verify(mDeviceManager).syncDeviceWithinHearingAidSetIfNeeded(mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED, BluetoothProfile.HAP_CLIENT);
+    }
+
+    /**
      * Verify BluetoothPan.ACTION_CONNECTION_STATE_CHANGED intent with uuid will dispatch to
      * profile connection state changed callback
      */
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 65c5708..ed62ce7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -362,6 +362,7 @@
         "device_state_flags_lib",
         "kotlinx_coroutines_android",
         "kotlinx_coroutines",
+        "kotlinx_coroutines_guava",
         "//frameworks/libs/systemui:iconloader_base",
         "SystemUI-tags",
         "SystemUI-proto",
@@ -382,6 +383,7 @@
         "androidx.compose.material_material-icons-extended",
         "androidx.activity_activity-compose",
         "androidx.compose.animation_animation-graphics",
+        "device_policy_aconfig_flags_lib",
     ],
     libs: [
         "keepanno-annotations",
@@ -541,6 +543,7 @@
         "androidx.activity_activity-compose",
         "androidx.compose.animation_animation-graphics",
         "TraceurCommon",
+        "kotlinx_coroutines_guava",
     ],
 }
 
@@ -622,6 +625,7 @@
         "//frameworks/libs/systemui:compilelib",
         "SystemUI-tests-base",
         "androidx.compose.runtime_runtime",
+        "SystemUI-core",
     ],
     libs: [
         "keepanno-annotations",
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 796e391..d2e5a13 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -4,13 +4,13 @@
 
 dsandler@android.com
 
-aaronjli@google.com
 achalke@google.com
 acul@google.com
 adamcohen@google.com
 aioana@google.com
 alexflo@google.com
 andonian@google.com
+amiko@google.com
 aroederer@google.com
 arteiro@google.com
 asc@google.com
@@ -39,7 +39,6 @@
 hyunyoungs@google.com
 ikateryna@google.com
 iyz@google.com
-jamesoleary@google.com
 jbolinger@google.com
 jdemeulenaere@google.com
 jeffdq@google.com
@@ -82,6 +81,7 @@
 pomini@google.com
 princedonkor@google.com
 rahulbanerjee@google.com
+rgl@google.com
 roosa@google.com
 saff@google.com
 santie@google.com
@@ -110,6 +110,3 @@
 yuandizhou@google.com
 yurilin@google.com
 zakcohen@google.com
-
-#Android TV
-rgl@google.com
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt
new file mode 100644
index 0000000..3d7401d
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene
+
+import com.android.systemui.qs.ui.composable.QuickSettingsShadeScene
+import com.android.systemui.scene.shared.model.Scene
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+
+@Module
+interface QuickSettingsShadeSceneModule {
+
+    @Binds @IntoSet fun quickSettingsShade(scene: QuickSettingsShadeScene): Scene
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
index 2ba78cf..fdf82ca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
@@ -30,11 +30,15 @@
  */
 fun NotificationScrimNestedScrollConnection(
     scrimOffset: () -> Float,
-    onScrimOffsetChanged: (Float) -> Unit,
+    snapScrimOffset: (Float) -> Unit,
+    animateScrimOffset: (Float) -> Unit,
     minScrimOffset: () -> Float,
     maxScrimOffset: Float,
     contentHeight: () -> Float,
     minVisibleScrimHeight: () -> Float,
+    isCurrentGestureOverscroll: () -> Boolean,
+    onStart: (Float) -> Unit = {},
+    onStop: (Float) -> Unit = {},
 ): PriorityNestedScrollConnection {
     return PriorityNestedScrollConnection(
         orientation = Orientation.Vertical,
@@ -49,7 +53,7 @@
         // scrolling down and content is done scrolling to top. After that, the scrim
         // needs to collapse; collapse the scrim until it is at the maxScrimOffset.
         canStartPostScroll = { offsetAvailable, _ ->
-            offsetAvailable > 0 && scrimOffset() < maxScrimOffset
+            offsetAvailable > 0 && (scrimOffset() < maxScrimOffset || isCurrentGestureOverscroll())
         },
         canStartPostFling = { false },
         canContinueScroll = {
@@ -57,7 +61,7 @@
             minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
         },
         canScrollOnFling = true,
-        onStart = { /* do nothing */},
+        onStart = { offsetAvailable -> onStart(offsetAvailable) },
         onScroll = { offsetAvailable ->
             val currentHeight = scrimOffset()
             val amountConsumed =
@@ -68,10 +72,16 @@
                     val amountLeft = minScrimOffset() - currentHeight
                     offsetAvailable.coerceAtLeast(amountLeft)
                 }
-            onScrimOffsetChanged(currentHeight + amountConsumed)
+            snapScrimOffset(currentHeight + amountConsumed)
             amountConsumed
         },
         // Don't consume the velocity on pre/post fling
-        onStop = { 0f },
+        onStop = { velocityAvailable ->
+            onStop(velocityAvailable)
+            if (scrimOffset() < minScrimOffset()) {
+                animateScrimOffset(minScrimOffset())
+            }
+            0f
+        },
     )
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 6e987bd..16ae5b1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.notifications.ui.composable
 
 import android.util.Log
+import androidx.compose.animation.core.Animatable
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Box
@@ -39,8 +40,8 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -77,6 +78,7 @@
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
+import kotlinx.coroutines.launch
 
 object Notifications {
     object Elements {
@@ -159,11 +161,13 @@
     shouldPunchHoleBehindScrim: Boolean,
     modifier: Modifier = Modifier,
 ) {
+    val coroutineScope = rememberCoroutineScope()
     val density = LocalDensity.current
     val screenCornerRadius = LocalScreenCornerRadius.current
     val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius)
     val scrollState = rememberScrollState()
     val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f)
+    val isCurrentGestureOverscroll = viewModel.isCurrentGestureOverscroll.collectAsState(false)
     val expansionFraction by viewModel.expandFraction.collectAsState(0f)
 
     val navBarHeight =
@@ -180,7 +184,7 @@
     // When fully expanded (scrimOffset = minScrimOffset), its top bound is at minScrimStartY,
     // which is equal to the height of the Shade Header. Thus, when the scrim is fully expanded, the
     // entire height of the scrim is visible on screen.
-    val scrimOffset = remember { mutableStateOf(0f) }
+    val scrimOffset = remember { Animatable(0f) }
 
     // set the bounds to null when the scrim disappears
     DisposableEffect(Unit) { onDispose { viewModel.onScrimBoundsChanged(null) } }
@@ -204,7 +208,7 @@
     // expanded, reset scrim offset.
     LaunchedEffect(stackHeight, scrimOffset) {
         snapshotFlow { stackHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f }
-            .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f }
+            .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.snapTo(0f) }
     }
 
     // if we receive scroll delta from NSSL, offset the scrim and placeholder accordingly.
@@ -214,7 +218,7 @@
                 val minOffset = minScrimOffset()
                 if (scrimOffset.value > minOffset) {
                     val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f)
-                    scrimOffset.value = (scrimOffset.value - delta).coerceAtLeast(minOffset)
+                    scrimOffset.snapTo((scrimOffset.value - delta).coerceAtLeast(minOffset))
                     if (remainingDelta > 0f) {
                         scrollState.scrollBy(remainingDelta)
                     }
@@ -296,20 +300,30 @@
                 modifier =
                     Modifier.verticalNestedScrollToScene(
                             topBehavior = NestedScrollBehavior.EdgeWithPreview,
+                            isExternalOverscrollGesture = { isCurrentGestureOverscroll.value }
                         )
                         .nestedScroll(
                             remember(
                                 scrimOffset,
                                 maxScrimTop,
                                 minScrimTop,
+                                isCurrentGestureOverscroll,
                             ) {
                                 NotificationScrimNestedScrollConnection(
                                     scrimOffset = { scrimOffset.value },
-                                    onScrimOffsetChanged = { scrimOffset.value = it },
+                                    snapScrimOffset = { value ->
+                                        coroutineScope.launch { scrimOffset.snapTo(value) }
+                                    },
+                                    animateScrimOffset = { value ->
+                                        coroutineScope.launch { scrimOffset.animateTo(value) }
+                                    },
                                     minScrimOffset = minScrimOffset,
                                     maxScrimOffset = 0f,
                                     contentHeight = { stackHeight.value },
                                     minVisibleScrimHeight = minVisibleScrimHeight,
+                                    isCurrentGestureOverscroll = {
+                                        isCurrentGestureOverscroll.value
+                                    },
                                 )
                             }
                         )
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
index 1f03408..1c675e3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
@@ -26,10 +26,10 @@
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.ComposableScene
 import com.android.systemui.shade.ui.composable.OverlayShade
-import com.android.systemui.shade.ui.viewmodel.NotificationsShadeSceneViewModel
 import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.StateFlow
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
new file mode 100644
index 0000000..636c6c3
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.ui.composable
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.shade.ui.composable.OverlayShade
+import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+@SysUISingleton
+class QuickSettingsShadeScene
+@Inject
+constructor(
+    viewModel: QuickSettingsShadeSceneViewModel,
+    private val overlayShadeViewModel: OverlayShadeViewModel,
+) : ComposableScene {
+
+    override val key = Scenes.QuickSettingsShade
+
+    override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
+        viewModel.destinationScenes
+
+    @Composable
+    override fun SceneScope.Content(
+        modifier: Modifier,
+    ) {
+        OverlayShade(
+            viewModel = overlayShadeViewModel,
+            modifier = modifier,
+            horizontalArrangement = Arrangement.End,
+        ) {
+            Text(
+                text = "Quick settings grid",
+                modifier = Modifier.padding(QuickSettingsShade.Dimensions.Padding)
+            )
+        }
+    }
+}
+
+object QuickSettingsShade {
+    object Dimensions {
+        val Padding = 16.dp
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt
index 6f2ed81..ded63a1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt
@@ -86,7 +86,10 @@
             modifier =
                 Modifier.fillMaxWidth().height(80.dp).semantics {
                     liveRegion = LiveRegionMode.Polite
-                    this.onClick(label = clickLabel) { false }
+                    this.onClick(label = clickLabel) {
+                        viewModel.onBarClick(null)
+                        true
+                    }
                 },
             color = MaterialTheme.colorScheme.surface,
             shape = RoundedCornerShape(28.dp),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index 9f5ab3c..a46f4e5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -66,7 +66,9 @@
 
                 // provide a not animated value to the a11y because it fails to announce the
                 // settled value when it changes rapidly.
-                progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
+                if (state.isEnabled) {
+                    progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
+                }
                 setProgress { targetValue ->
                     val targetDirection =
                         when {
diff --git a/packages/SystemUI/compose/scene/OWNERS b/packages/SystemUI/compose/scene/OWNERS
index 33a59c2..dac37ee 100644
--- a/packages/SystemUI/compose/scene/OWNERS
+++ b/packages/SystemUI/compose/scene/OWNERS
@@ -2,12 +2,13 @@
 
 # Bug component: 1184816
 
+amiko@google.com
 jdemeulenaere@google.com
 omarmt@google.com
 
 # SysUI Dr No's.
 # Don't send reviews here.
-dsandler@android.com
 cinek@google.com
+dsandler@android.com
 juliacr@google.com
 pixel@google.com
\ No newline at end of file
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 6b289f3..b5e9313 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -47,8 +47,11 @@
     }
 
     return when (transitionState) {
-        is TransitionState.Idle -> animate(layoutState, target, transitionKey)
+        is TransitionState.Idle ->
+            animate(layoutState, target, transitionKey, isInitiatedByUserInput = false)
         is TransitionState.Transition -> {
+            val isInitiatedByUserInput = transitionState.isInitiatedByUserInput
+
             // A transition is currently running: first check whether `transition.toScene` or
             // `transition.fromScene` is the same as our target scene, in which case the transition
             // can be accelerated or reversed to end up in the target state.
@@ -68,8 +71,14 @@
                 } else {
                     // The transition is in progress: start the canned animation at the same
                     // progress as it was in.
-                    // TODO(b/290184746): Also take the current velocity into account.
-                    animate(layoutState, target, transitionKey, startProgress = progress)
+                    animate(
+                        layoutState,
+                        target,
+                        transitionKey,
+                        isInitiatedByUserInput,
+                        initialProgress = progress,
+                        initialVelocity = transitionState.progressVelocity,
+                    )
                 }
             } else if (transitionState.fromScene == target) {
                 // There is a transition from [target] to another scene: simply animate the same
@@ -83,19 +92,52 @@
                     layoutState.finishTransition(transitionState, target)
                     null
                 } else {
-                    // TODO(b/290184746): Also take the current velocity into account.
                     animate(
                         layoutState,
                         target,
                         transitionKey,
-                        startProgress = progress,
+                        isInitiatedByUserInput,
+                        initialProgress = progress,
+                        initialVelocity = transitionState.progressVelocity,
                         reversed = true,
                     )
                 }
             } else {
                 // Generic interruption; the current transition is neither from or to [target].
-                // TODO(b/290930950): Better handle interruptions here.
-                animate(layoutState, target, transitionKey)
+                val interruptionResult =
+                    layoutState.transitions.interruptionHandler.onInterruption(
+                        transitionState,
+                        target,
+                    )
+                        ?: DefaultInterruptionHandler.onInterruption(transitionState, target)
+
+                val animateFrom = interruptionResult.animateFrom
+                if (
+                    animateFrom != transitionState.toScene &&
+                        animateFrom != transitionState.fromScene
+                ) {
+                    error(
+                        "InterruptionResult.animateFrom must be either the fromScene " +
+                            "(${transitionState.fromScene.debugName}) or the toScene " +
+                            "(${transitionState.toScene.debugName}) of the interrupted transition."
+                    )
+                }
+
+                // If we were A => B and that we are now animating A => C, add a transition B => A
+                // to the list of transitions so that B "disappears back to A".
+                val chain = interruptionResult.chain
+                if (chain && animateFrom != transitionState.currentScene) {
+                    animateToScene(layoutState, animateFrom, transitionKey = null)
+                }
+
+                animate(
+                    layoutState,
+                    target,
+                    transitionKey,
+                    isInitiatedByUserInput,
+                    fromScene = animateFrom,
+                    chain = chain,
+                )
             }
         }
     }
@@ -103,32 +145,31 @@
 
 private fun CoroutineScope.animate(
     layoutState: BaseSceneTransitionLayoutState,
-    target: SceneKey,
+    targetScene: SceneKey,
     transitionKey: TransitionKey?,
-    startProgress: Float = 0f,
+    isInitiatedByUserInput: Boolean,
+    initialProgress: Float = 0f,
+    initialVelocity: Float = 0f,
     reversed: Boolean = false,
+    fromScene: SceneKey = layoutState.transitionState.currentScene,
+    chain: Boolean = true,
 ): TransitionState.Transition {
-    val fromScene = layoutState.transitionState.currentScene
-    val isUserInput =
-        (layoutState.transitionState as? TransitionState.Transition)?.isInitiatedByUserInput
-            ?: false
-
     val targetProgress = if (reversed) 0f else 1f
     val transition =
         if (reversed) {
             OneOffTransition(
-                fromScene = target,
+                fromScene = targetScene,
                 toScene = fromScene,
-                currentScene = target,
-                isInitiatedByUserInput = isUserInput,
+                currentScene = targetScene,
+                isInitiatedByUserInput = isInitiatedByUserInput,
                 isUserInputOngoing = false,
             )
         } else {
             OneOffTransition(
                 fromScene = fromScene,
-                toScene = target,
-                currentScene = target,
-                isInitiatedByUserInput = isUserInput,
+                toScene = targetScene,
+                currentScene = targetScene,
+                isInitiatedByUserInput = isInitiatedByUserInput,
                 isUserInputOngoing = false,
             )
         }
@@ -136,7 +177,7 @@
     // Change the current layout state to start this new transition. This will compute the
     // TransformationSpec associated to this transition, which we need to initialize the Animatable
     // that will actually animate it.
-    layoutState.startTransition(transition, transitionKey)
+    layoutState.startTransition(transition, transitionKey, chain)
 
     // The transition now contains the transformation spec that we should use to instantiate the
     // Animatable.
@@ -144,19 +185,19 @@
     val visibilityThreshold =
         (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
     val animatable =
-        Animatable(startProgress, visibilityThreshold = visibilityThreshold).also {
+        Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also {
             transition.animatable = it
         }
 
     // Animate the progress to its target value.
     transition.job =
-        launch { animatable.animateTo(targetProgress, animationSpec) }
+        launch { animatable.animateTo(targetProgress, animationSpec, initialVelocity) }
             .apply {
                 invokeOnCompletion {
                     // Settle the state to Idle(target). Note that this will do nothing if this
                     // transition was replaced/interrupted by another one, and this also runs if
                     // this coroutine is cancelled, i.e. if [this] coroutine scope is cancelled.
-                    layoutState.finishTransition(transition, target)
+                    layoutState.finishTransition(transition, targetScene)
                 }
             }
 
@@ -185,6 +226,9 @@
     override val progress: Float
         get() = animatable.value
 
+    override val progressVelocity: Float
+        get() = animatable.velocity
+
     override fun finish(): Job = job
 }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index f78ed2f..6758990 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -579,6 +579,18 @@
             return offset / distance
         }
 
+    override val progressVelocity: Float
+        get() {
+            val animatable = offsetAnimation?.animatable ?: return 0f
+            val distance = distance()
+            if (distance == DistanceUnspecified) {
+                return 0f
+            }
+
+            val velocityInDistanceUnit = animatable.velocity
+            return velocityInDistanceUnit / distance.absoluteValue
+        }
+
     override val isInitiatedByUserInput = true
 
     override var bouncingScene: SceneKey? = null
@@ -865,6 +877,7 @@
     private val orientation: Orientation,
     private val topOrLeftBehavior: NestedScrollBehavior,
     private val bottomOrRightBehavior: NestedScrollBehavior,
+    private val isExternalOverscrollGesture: () -> Boolean,
 ) {
     private val layoutState = layoutImpl.state
     private val draggableHandler = layoutImpl.draggableHandler(orientation)
@@ -920,7 +933,8 @@
         return PriorityNestedScrollConnection(
             orientation = orientation,
             canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
-                canChangeScene = offsetBeforeStart == 0f
+                canChangeScene =
+                    if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
 
                 val canInterceptSwipeTransition =
                     canChangeScene &&
@@ -950,7 +964,8 @@
                         else -> return@PriorityNestedScrollConnection false
                     }
 
-                val isZeroOffset = offsetBeforeStart == 0f
+                val isZeroOffset =
+                    if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
 
                 val canStart =
                     when (behavior) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index ca64323..20742ee 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -329,10 +329,9 @@
 
     if (transition == null && previousTransition != null) {
         // The transition was just finished.
-        element.sceneStates.values.forEach { sceneState ->
-            sceneState.offsetInterruptionDelta = Offset.Zero
-            sceneState.scaleInterruptionDelta = Scale.Zero
-            sceneState.alphaInterruptionDelta = 0f
+        element.sceneStates.values.forEach {
+            it.clearValuesBeforeInterruption()
+            it.clearInterruptionDeltas()
         }
     }
 
@@ -375,12 +374,22 @@
         sceneState.scaleBeforeInterruption = lastScale
         sceneState.alphaBeforeInterruption = lastAlpha
 
-        sceneState.offsetInterruptionDelta = Offset.Zero
-        sceneState.scaleInterruptionDelta = Scale.Zero
-        sceneState.alphaInterruptionDelta = 0f
+        sceneState.clearInterruptionDeltas()
     }
 }
 
+private fun Element.SceneState.clearInterruptionDeltas() {
+    offsetInterruptionDelta = Offset.Zero
+    scaleInterruptionDelta = Scale.Zero
+    alphaInterruptionDelta = 0f
+}
+
+private fun Element.SceneState.clearValuesBeforeInterruption() {
+    offsetBeforeInterruption = Offset.Unspecified
+    scaleBeforeInterruption = Scale.Unspecified
+    alphaBeforeInterruption = Element.AlphaUnspecified
+}
+
 /**
  * Compute what [value] should be if we take the
  * [interruption progress][TransitionState.Transition.interruptionProgress] of [transition] into
@@ -744,7 +753,11 @@
         // No need to place the element in this scene if we don't want to draw it anyways.
         if (!shouldPlaceElement(layoutImpl, scene, element, transition)) {
             sceneState.lastOffset = Offset.Unspecified
-            sceneState.offsetBeforeInterruption = Offset.Unspecified
+            sceneState.lastScale = Scale.Unspecified
+            sceneState.lastAlpha = Element.AlphaUnspecified
+
+            sceneState.clearValuesBeforeInterruption()
+            sceneState.clearInterruptionDeltas()
             return
         }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt
new file mode 100644
index 0000000..54c64fd
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+/**
+ * A handler to specify how a transition should be interrupted.
+ *
+ * @see DefaultInterruptionHandler
+ * @see SceneTransitionsBuilder.interruptionHandler
+ */
+interface InterruptionHandler {
+    /**
+     * This function is called when [interrupted] is interrupted: it is currently animating between
+     * [interrupted.fromScene] and [interrupted.toScene], and we will now animate to
+     * [newTargetScene].
+     *
+     * If this returns `null`, then the [default behavior][DefaultInterruptionHandler] will be used:
+     * we will animate from [interrupted.currentScene] and chaining will be enabled (see
+     * [InterruptionResult] for more information about chaining).
+     *
+     * @see InterruptionResult
+     */
+    fun onInterruption(
+        interrupted: TransitionState.Transition,
+        newTargetScene: SceneKey,
+    ): InterruptionResult?
+}
+
+/**
+ * The result of an interruption that specifies how we should handle a transition A => B now that we
+ * have to animate to C.
+ *
+ * For instance, if the interrupted transition was A => B and currentScene = B:
+ * - animateFrom = B && chain = true => there will be 2 transitions running in parallel, A => B and
+ *   B => C.
+ * - animateFrom = A && chain = true => there will be 2 transitions running in parallel, B => A and
+ *   A => C.
+ * - animateFrom = B && chain = false => there will be 1 transition running, B => C.
+ * - animateFrom = A && chain = false => there will be 1 transition running, A => C.
+ */
+class InterruptionResult(
+    /**
+     * The scene we should animate from when transitioning to C.
+     *
+     * Important: This **must** be either [TransitionState.Transition.fromScene] or
+     * [TransitionState.Transition.toScene] of the transition that was interrupted.
+     */
+    val animateFrom: SceneKey,
+
+    /**
+     * Whether chaining is enabled, i.e. if the new transition to C should run in parallel with the
+     * previous one(s) or if it should be the only remaining transition that is running.
+     */
+    val chain: Boolean = true,
+)
+
+/**
+ * The default interruption handler: we animate from [TransitionState.Transition.currentScene] and
+ * chaining is enabled.
+ */
+object DefaultInterruptionHandler : InterruptionHandler {
+    override fun onInterruption(
+        interrupted: TransitionState.Transition,
+        newTargetScene: SceneKey,
+    ): InterruptionResult {
+        return InterruptionResult(
+            animateFrom = interrupted.currentScene,
+            chain = true,
+        )
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
index 5a2f85a..1fa6b3f7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
@@ -75,6 +75,7 @@
     orientation: Orientation,
     topOrLeftBehavior: NestedScrollBehavior,
     bottomOrRightBehavior: NestedScrollBehavior,
+    isExternalOverscrollGesture: () -> Boolean,
 ) =
     this then
         NestedScrollToSceneElement(
@@ -82,6 +83,7 @@
             orientation = orientation,
             topOrLeftBehavior = topOrLeftBehavior,
             bottomOrRightBehavior = bottomOrRightBehavior,
+            isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
 
 private data class NestedScrollToSceneElement(
@@ -89,6 +91,7 @@
     private val orientation: Orientation,
     private val topOrLeftBehavior: NestedScrollBehavior,
     private val bottomOrRightBehavior: NestedScrollBehavior,
+    private val isExternalOverscrollGesture: () -> Boolean,
 ) : ModifierNodeElement<NestedScrollToSceneNode>() {
     override fun create() =
         NestedScrollToSceneNode(
@@ -96,6 +99,7 @@
             orientation = orientation,
             topOrLeftBehavior = topOrLeftBehavior,
             bottomOrRightBehavior = bottomOrRightBehavior,
+            isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
 
     override fun update(node: NestedScrollToSceneNode) {
@@ -104,6 +108,7 @@
             orientation = orientation,
             topOrLeftBehavior = topOrLeftBehavior,
             bottomOrRightBehavior = bottomOrRightBehavior,
+            isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
     }
 
@@ -121,6 +126,7 @@
     orientation: Orientation,
     topOrLeftBehavior: NestedScrollBehavior,
     bottomOrRightBehavior: NestedScrollBehavior,
+    isExternalOverscrollGesture: () -> Boolean,
 ) : DelegatingNode() {
     private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
         scenePriorityNestedScrollConnection(
@@ -128,6 +134,7 @@
             orientation = orientation,
             topOrLeftBehavior = topOrLeftBehavior,
             bottomOrRightBehavior = bottomOrRightBehavior,
+            isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
 
     private var nestedScrollNode: DelegatableNode =
@@ -150,6 +157,7 @@
         orientation: Orientation,
         topOrLeftBehavior: NestedScrollBehavior,
         bottomOrRightBehavior: NestedScrollBehavior,
+        isExternalOverscrollGesture: () -> Boolean,
     ) {
         // Clean up the old nested scroll connection
         priorityNestedScrollConnection.reset()
@@ -162,6 +170,7 @@
                 orientation = orientation,
                 topOrLeftBehavior = topOrLeftBehavior,
                 bottomOrRightBehavior = bottomOrRightBehavior,
+                isExternalOverscrollGesture = isExternalOverscrollGesture,
             )
         nestedScrollNode =
             nestedScrollModifierNode(
@@ -177,11 +186,13 @@
     orientation: Orientation,
     topOrLeftBehavior: NestedScrollBehavior,
     bottomOrRightBehavior: NestedScrollBehavior,
+    isExternalOverscrollGesture: () -> Boolean,
 ) =
     NestedScrollHandlerImpl(
             layoutImpl = layoutImpl,
             orientation = orientation,
             topOrLeftBehavior = topOrLeftBehavior,
             bottomOrRightBehavior = bottomOrRightBehavior,
+            isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
         .connection
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 339868c..6fef33c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -141,23 +141,27 @@
     override fun Modifier.horizontalNestedScrollToScene(
         leftBehavior: NestedScrollBehavior,
         rightBehavior: NestedScrollBehavior,
+        isExternalOverscrollGesture: () -> Boolean,
     ): Modifier =
         nestedScrollToScene(
             layoutImpl = layoutImpl,
             orientation = Orientation.Horizontal,
             topOrLeftBehavior = leftBehavior,
             bottomOrRightBehavior = rightBehavior,
+            isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
 
     override fun Modifier.verticalNestedScrollToScene(
         topBehavior: NestedScrollBehavior,
-        bottomBehavior: NestedScrollBehavior
+        bottomBehavior: NestedScrollBehavior,
+        isExternalOverscrollGesture: () -> Boolean,
     ): Modifier =
         nestedScrollToScene(
             layoutImpl = layoutImpl,
             orientation = Orientation.Vertical,
             topOrLeftBehavior = topBehavior,
             bottomOrRightBehavior = bottomBehavior,
+            isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
 
     override fun Modifier.noResizeDuringTransitions(): Modifier {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index c7c874c..11e711a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -250,6 +250,7 @@
     fun Modifier.horizontalNestedScrollToScene(
         leftBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
         rightBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+        isExternalOverscrollGesture: () -> Boolean = { false },
     ): Modifier
 
     /**
@@ -262,6 +263,7 @@
     fun Modifier.verticalNestedScrollToScene(
         topBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
         bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+        isExternalOverscrollGesture: () -> Boolean = { false },
     ): Modifier
 
     /**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 5fda77a..7f94f0d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -227,6 +227,9 @@
          */
         abstract val progress: Float
 
+        /** The current velocity of [progress], in progress units. */
+        abstract val progressVelocity: Float
+
         /** Whether the transition was triggered by user input rather than being programmatic. */
         abstract val isInitiatedByUserInput: Boolean
 
@@ -422,13 +425,18 @@
     }
 
     /**
-     * Start a new [transition], instantly interrupting any ongoing transition if there was one.
+     * Start a new [transition].
+     *
+     * If [chain] is `true`, then the transitions will simply be added to [currentTransitions] and
+     * will run in parallel to the current transitions. If [chain] is `false`, then the list of
+     * [currentTransitions] will be cleared and [transition] will be the only running transition.
      *
      * Important: you *must* call [finishTransition] once the transition is finished.
      */
     internal fun startTransition(
         transition: TransitionState.Transition,
         transitionKey: TransitionKey?,
+        chain: Boolean = true,
     ) {
         // Compute the [TransformationSpec] when the transition starts.
         val fromScene = transition.fromScene
@@ -471,26 +479,10 @@
                     finishTransition(currentState, currentState.currentScene)
                 }
 
-                // Check that we don't have too many concurrent transitions.
-                if (transitionStates.size >= MAX_CONCURRENT_TRANSITIONS) {
-                    Log.wtf(
-                        TAG,
-                        buildString {
-                            appendLine("Potential leak detected in SceneTransitionLayoutState!")
-                            appendLine(
-                                "  Some transition(s) never called STLState.finishTransition()."
-                            )
-                            appendLine("  Transitions (size=${transitionStates.size}):")
-                            transitionStates.fastForEach { state ->
-                                val transition = state as TransitionState.Transition
-                                val from = transition.fromScene
-                                val to = transition.toScene
-                                val indicator =
-                                    if (finishedTransitions.contains(transition)) "x" else " "
-                                appendLine("  [$indicator] $from => $to ($transition)")
-                            }
-                        }
-                    )
+                val tooManyTransitions = transitionStates.size >= MAX_CONCURRENT_TRANSITIONS
+                val clearCurrentTransitions = !chain || tooManyTransitions
+                if (clearCurrentTransitions) {
+                    if (tooManyTransitions) logTooManyTransitions()
 
                     // Force finish all transitions.
                     while (currentTransitions.isNotEmpty()) {
@@ -511,6 +503,24 @@
         }
     }
 
+    private fun logTooManyTransitions() {
+        Log.wtf(
+            TAG,
+            buildString {
+                appendLine("Potential leak detected in SceneTransitionLayoutState!")
+                appendLine("  Some transition(s) never called STLState.finishTransition().")
+                appendLine("  Transitions (size=${transitionStates.size}):")
+                transitionStates.fastForEach { state ->
+                    val transition = state as TransitionState.Transition
+                    val from = transition.fromScene
+                    val to = transition.toScene
+                    val indicator = if (finishedTransitions.contains(transition)) "x" else " "
+                    appendLine("  [$indicator] $from => $to ($transition)")
+                }
+            }
+        )
+    }
+
     private fun cancelActiveTransitionLinks() {
         for ((link, linkedTransition) in activeTransitionLinks) {
             link.target.finishTransition(linkedTransition, linkedTransition.currentScene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index b466143..0f6a1d2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -44,6 +44,7 @@
     internal val defaultSwipeSpec: SpringSpec<Float>,
     internal val transitionSpecs: List<TransitionSpecImpl>,
     internal val overscrollSpecs: List<OverscrollSpecImpl>,
+    internal val interruptionHandler: InterruptionHandler,
 ) {
     private val transitionCache =
         mutableMapOf<
@@ -145,6 +146,7 @@
                 defaultSwipeSpec = DefaultSwipeSpec,
                 transitionSpecs = emptyList(),
                 overscrollSpecs = emptyList(),
+                interruptionHandler = DefaultInterruptionHandler,
             )
     }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 6bc397e..a4682ff 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -40,6 +40,12 @@
     var defaultSwipeSpec: SpringSpec<Float>
 
     /**
+     * The [InterruptionHandler] used when transitions are interrupted. Defaults to
+     * [DefaultInterruptionHandler].
+     */
+    var interruptionHandler: InterruptionHandler
+
+    /**
      * Define the default animation to be played when transitioning [to] the specified scene, from
      * any scene. For the animation specification to apply only when transitioning between two
      * specific scenes, use [from] instead.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 1c9080f..802ab1f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -47,12 +47,14 @@
     return SceneTransitions(
         impl.defaultSwipeSpec,
         impl.transitionSpecs,
-        impl.transitionOverscrollSpecs
+        impl.transitionOverscrollSpecs,
+        impl.interruptionHandler,
     )
 }
 
 private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
     override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec
+    override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler
 
     val transitionSpecs = mutableListOf<TransitionSpecImpl>()
     val transitionOverscrollSpecs = mutableListOf<OverscrollSpecImpl>()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
index 73393a1..79f126d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
@@ -45,5 +45,8 @@
     override val progress: Float
         get() = originalTransition.progress
 
+    override val progressVelocity: Float
+        get() = originalTransition.progressVelocity
+
     override fun finish(): Job = originalTransition.finish()
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 1fd1bf4..8625482 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -32,12 +32,11 @@
 import com.android.compose.animation.scene.TestScenes.SceneA
 import com.android.compose.animation.scene.TestScenes.SceneB
 import com.android.compose.animation.scene.TestScenes.SceneC
-import com.android.compose.animation.scene.TransitionState.Idle
 import com.android.compose.animation.scene.TransitionState.Transition
+import com.android.compose.animation.scene.subjects.assertThat
 import com.android.compose.test.MonotonicClockTestScope
 import com.android.compose.test.runMonotonicClockTest
 import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.cancelAndJoin
 import kotlinx.coroutines.launch
@@ -103,12 +102,16 @@
         val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical)
         val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal)
 
-        fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) =
+        fun nestedScrollConnection(
+            nestedScrollBehavior: NestedScrollBehavior,
+            isExternalOverscrollGesture: Boolean = false
+        ) =
             NestedScrollHandlerImpl(
                     layoutImpl = layoutImpl,
                     orientation = draggableHandler.orientation,
                     topOrLeftBehavior = nestedScrollBehavior,
                     bottomOrRightBehavior = nestedScrollBehavior,
+                    isExternalOverscrollGesture = { isExternalOverscrollGesture }
                 )
                 .connection
 
@@ -145,10 +148,8 @@
         }
 
         fun assertIdle(currentScene: SceneKey) {
-            assertThat(transitionState).isInstanceOf(Idle::class.java)
-            assertWithMessage("currentScene does not match")
-                .that(transitionState.currentScene)
-                .isEqualTo(currentScene)
+            assertThat(transitionState).isIdle()
+            assertThat(transitionState).hasCurrentScene(currentScene)
         }
 
         fun assertTransition(
@@ -158,34 +159,12 @@
             progress: Float? = null,
             isUserInputOngoing: Boolean? = null
         ) {
-            assertThat(transitionState).isInstanceOf(Transition::class.java)
-            val transition = transitionState as Transition
-
-            if (currentScene != null)
-                assertWithMessage("currentScene does not match")
-                    .that(transition.currentScene)
-                    .isEqualTo(currentScene)
-
-            if (fromScene != null)
-                assertWithMessage("fromScene does not match")
-                    .that(transition.fromScene)
-                    .isEqualTo(fromScene)
-
-            if (toScene != null)
-                assertWithMessage("toScene does not match")
-                    .that(transition.toScene)
-                    .isEqualTo(toScene)
-
-            if (progress != null)
-                assertWithMessage("progress does not match")
-                    .that(transition.progress)
-                    .isWithin(0f) // returns true when comparing 0.0f with -0.0f
-                    .of(progress)
-
-            if (isUserInputOngoing != null)
-                assertWithMessage("isUserInputOngoing does not match")
-                    .that(transition.isUserInputOngoing)
-                    .isEqualTo(isUserInputOngoing)
+            val transition = assertThat(transitionState).isTransition()
+            currentScene?.let { assertThat(transition).hasCurrentScene(it) }
+            fromScene?.let { assertThat(transition).hasFromScene(it) }
+            toScene?.let { assertThat(transition).hasToScene(it) }
+            progress?.let { assertThat(transition).hasProgress(it) }
+            isUserInputOngoing?.let { assertThat(transition).hasIsUserInputOngoing(it) }
         }
 
         fun onDragStarted(
@@ -801,6 +780,26 @@
     }
 
     @Test
+    fun flingAfterScrollStartedByExternalOverscrollGesture() = runGestureTest {
+        val nestedScroll =
+            nestedScrollConnection(
+                nestedScrollBehavior = EdgeWithPreview,
+                isExternalOverscrollGesture = true
+            )
+
+        // scroll not consumed in child
+        nestedScroll.scroll(
+            available = downOffset(fractionOfScreen = 0.1f),
+        )
+
+        // scroll offsetY10 is all available for parents
+        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
+        assertTransition(SceneA)
+
+        nestedScroll.preFling(available = Velocity(0f, velocityThreshold))
+    }
+
+    @Test
     fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
         nestedScroll.preFling(available = Velocity(0f, velocityThreshold))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 92e1b2c..e19dc96 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -20,7 +20,6 @@
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.rememberScrollableState
 import androidx.compose.foundation.gestures.scrollable
@@ -43,7 +42,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.approachLayout
@@ -64,6 +62,7 @@
 import com.android.compose.animation.scene.TestScenes.SceneA
 import com.android.compose.animation.scene.TestScenes.SceneB
 import com.android.compose.animation.scene.TestScenes.SceneC
+import com.android.compose.animation.scene.subjects.assertThat
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -78,7 +77,6 @@
     @get:Rule val rule = createComposeRule()
 
     @Composable
-    @OptIn(ExperimentalComposeUiApi::class)
     private fun SceneScope.Element(
         key: ElementKey,
         size: Dp,
@@ -496,7 +494,6 @@
     }
 
     @Test
-    @OptIn(ExperimentalFoundationApi::class)
     fun elementModifierNodeIsRecycledInLazyLayouts() = runTest {
         val nPages = 2
         val pagerState = PagerState(currentPage = 0) { nPages }
@@ -654,8 +651,7 @@
             }
         }
 
-        assertThat(state.currentTransition).isNull()
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(state.transitionState).isIdle()
 
         // Swipe by half of verticalSwipeDistance.
         rule.onRoot().performTouchInput {
@@ -691,9 +687,9 @@
 
         val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
         fooElement.assertTopPositionInRootIsEqualTo(0.dp)
-        val transition = state.currentTransition
+        val transition = assertThat(state.transitionState).isTransition()
         assertThat(transition).isNotNull()
-        assertThat(transition!!.progress).isEqualTo(0.5f)
+        assertThat(transition).hasProgress(0.5f)
         assertThat(animatedFloat).isEqualTo(50f)
 
         rule.onRoot().performTouchInput {
@@ -702,8 +698,8 @@
         }
 
         // Scroll 150% (Scene B overscroll by 50%)
-        assertThat(transition.progress).isEqualTo(1.5f)
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+        assertThat(transition).hasProgress(1.5f)
+        assertThat(transition).hasOverscrollSpec()
         fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
         // animatedFloat cannot overflow (canOverflow = false)
         assertThat(animatedFloat).isEqualTo(100f)
@@ -714,8 +710,8 @@
         }
 
         // Scroll 250% (Scene B overscroll by 150%)
-        assertThat(transition.progress).isEqualTo(2.5f)
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+        assertThat(transition).hasProgress(2.5f)
+        assertThat(transition).hasOverscrollSpec()
         fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
         assertThat(animatedFloat).isEqualTo(100f)
     }
@@ -766,8 +762,7 @@
             }
         }
 
-        assertThat(state.currentTransition).isNull()
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(state.transitionState).isIdle()
         val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
         fooElement.assertTopPositionInRootIsEqualTo(0.dp)
 
@@ -779,10 +774,9 @@
             moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
         }
 
-        val transition = state.currentTransition
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
-        assertThat(transition).isNotNull()
-        assertThat(transition!!.progress).isEqualTo(-0.5f)
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasOverscrollSpec()
+        assertThat(transition).hasProgress(-0.5f)
         fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
 
         rule.onRoot().performTouchInput {
@@ -791,8 +785,8 @@
         }
 
         // Scroll 150% (Scene B overscroll by 50%)
-        assertThat(transition.progress).isEqualTo(-1.5f)
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+        assertThat(transition).hasProgress(-1.5f)
+        assertThat(transition).hasOverscrollSpec()
         fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
     }
 
@@ -825,13 +819,12 @@
             moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
         }
 
-        val transition = state.currentTransition
-        assertThat(transition).isNotNull()
+        val transition = assertThat(state.transitionState).isTransition()
         assertThat(animatedFloat).isEqualTo(100f)
 
         // Scroll 150% (100% scroll + 50% overscroll)
-        assertThat(transition!!.progress).isEqualTo(1.5f)
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+        assertThat(transition).hasProgress(1.5f)
+        assertThat(transition).hasOverscrollSpec()
         fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f)
         assertThat(animatedFloat).isEqualTo(100f)
 
@@ -841,8 +834,8 @@
         }
 
         // Scroll 250% (100% scroll + 150% overscroll)
-        assertThat(transition.progress).isEqualTo(2.5f)
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+        assertThat(transition).hasProgress(2.5f)
+        assertThat(transition).hasOverscrollSpec()
         fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f)
         assertThat(animatedFloat).isEqualTo(100f)
     }
@@ -882,13 +875,11 @@
             moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
         }
 
-        val transition = state.currentTransition
-        assertThat(transition).isNotNull()
-        transition as TransitionState.HasOverscrollProperties
+        val transition = assertThat(state.transitionState).isTransition()
 
         // Scroll 150% (100% scroll + 50% overscroll)
-        assertThat(transition.progress).isEqualTo(1.5f)
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+        assertThat(transition).hasProgress(1.5f)
+        assertThat(transition).hasOverscrollSpec()
         fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f))
         assertThat(animatedFloat).isEqualTo(100f)
 
@@ -900,8 +891,8 @@
         rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f }
 
         assertThat(transition.progress).isLessThan(1f)
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
-        assertThat(transition.bouncingScene).isEqualTo(transition.toScene)
+        assertThat(transition).hasOverscrollSpec()
+        assertThat(transition).hasBouncingScene(transition.toScene)
         assertThat(animatedFloat).isEqualTo(100f)
     }
 
@@ -980,13 +971,13 @@
 
         val transitions = state.currentTransitions
         assertThat(transitions).hasSize(2)
-        assertThat(transitions[0].fromScene).isEqualTo(SceneA)
-        assertThat(transitions[0].toScene).isEqualTo(SceneB)
-        assertThat(transitions[0].progress).isEqualTo(0f)
+        assertThat(transitions[0]).hasFromScene(SceneA)
+        assertThat(transitions[0]).hasToScene(SceneB)
+        assertThat(transitions[0]).hasProgress(0f)
 
-        assertThat(transitions[1].fromScene).isEqualTo(SceneB)
-        assertThat(transitions[1].toScene).isEqualTo(SceneC)
-        assertThat(transitions[1].progress).isEqualTo(0f)
+        assertThat(transitions[1]).hasFromScene(SceneB)
+        assertThat(transitions[1]).hasToScene(SceneC)
+        assertThat(transitions[1]).hasProgress(0f)
 
         // First frame: both are at x = 0dp. For the whole transition, Foo is at y = 0dp and Bar is
         // at y = layoutSize - elementSoze = 100dp.
@@ -1049,24 +1040,30 @@
             Box(modifier.element(TestElements.Foo).size(fooSize))
         }
 
+        lateinit var layoutImpl: SceneTransitionLayoutImpl
         rule.setContent {
-            SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+            SceneTransitionLayoutForTesting(
+                state,
+                Modifier.size(layoutSize),
+                onLayoutImpl = { layoutImpl = it },
+            ) {
                 // In scene A, Foo is aligned at the TopStart.
                 scene(SceneA) {
                     Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) }
                 }
 
+                // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming
+                // from B. We put it before (below) scene B so that we can check that interruptions
+                // values and deltas are properly cleared once all transitions are done.
+                scene(SceneC) {
+                    Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) }
+                }
+
                 // In scene B, Foo is aligned at the TopEnd, so it moves horizontally when coming
                 // from A.
                 scene(SceneB) {
                     Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopEnd)) }
                 }
-
-                // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming
-                // from B.
-                scene(SceneC) {
-                    Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) }
-                }
             }
         }
 
@@ -1115,7 +1112,7 @@
         // Interruption progress is at 100% and bToC is at 0%, so Foo should be at the same offset
         // as right before the interruption.
         rule
-            .onNode(isElement(TestElements.Foo, SceneC))
+            .onNode(isElement(TestElements.Foo, SceneB))
             .assertPositionInRootIsEqualTo(offsetInAToB.x, offsetInAToB.y)
 
         // Move the transition forward at 30% and set the interruption progress to 50%.
@@ -1130,7 +1127,7 @@
                 )
         rule.waitForIdle()
         rule
-            .onNode(isElement(TestElements.Foo, SceneC))
+            .onNode(isElement(TestElements.Foo, SceneB))
             .assertPositionInRootIsEqualTo(
                 offsetInBToCWithInterruption.x,
                 offsetInBToCWithInterruption.y,
@@ -1140,7 +1137,24 @@
         bToCProgress = 1f
         interruptionProgress = 0f
         rule
-            .onNode(isElement(TestElements.Foo, SceneC))
+            .onNode(isElement(TestElements.Foo, SceneB))
             .assertPositionInRootIsEqualTo(offsetInC.x, offsetInC.y)
+
+        // Manually finish the transition.
+        state.finishTransition(aToB, SceneB)
+        state.finishTransition(bToC, SceneC)
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+
+        // The interruption values should be unspecified and deltas should be set to zero.
+        val foo = layoutImpl.elements.getValue(TestElements.Foo)
+        assertThat(foo.sceneStates.keys).containsExactly(SceneC)
+        val stateInC = foo.sceneStates.getValue(SceneC)
+        assertThat(stateInC.offsetBeforeInterruption).isEqualTo(Offset.Unspecified)
+        assertThat(stateInC.scaleBeforeInterruption).isEqualTo(Scale.Unspecified)
+        assertThat(stateInC.alphaBeforeInterruption).isEqualTo(Element.AlphaUnspecified)
+        assertThat(stateInC.offsetInterruptionDelta).isEqualTo(Offset.Zero)
+        assertThat(stateInC.scaleInterruptionDelta).isEqualTo(Scale.Zero)
+        assertThat(stateInC.alphaInterruptionDelta).isEqualTo(0f)
     }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
new file mode 100644
index 0000000..85d4165
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.compose.animation.scene
+
+import androidx.compose.animation.core.tween
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.TestScenes.SceneC
+import com.android.compose.animation.scene.subjects.assertThat
+import com.android.compose.test.runMonotonicClockTest
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class InterruptionHandlerTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun default() = runMonotonicClockTest {
+        val state =
+            MutableSceneTransitionLayoutState(
+                SceneA,
+                transitions { /* default interruption handler */},
+            )
+
+        state.setTargetScene(SceneB, coroutineScope = this)
+        state.setTargetScene(SceneC, coroutineScope = this)
+
+        assertThat(state.currentTransitions)
+            .comparingElementsUsing(FromToCurrentTriple)
+            .containsExactly(
+                // A to B.
+                Triple(SceneA, SceneB, SceneB),
+
+                // B to C.
+                Triple(SceneB, SceneC, SceneC),
+            )
+            .inOrder()
+    }
+
+    @Test
+    fun chainingDisabled() = runMonotonicClockTest {
+        val state =
+            MutableSceneTransitionLayoutState(
+                SceneA,
+                transitions {
+                    // Handler that animates from currentScene (default) but disables chaining.
+                    interruptionHandler =
+                        object : InterruptionHandler {
+                            override fun onInterruption(
+                                interrupted: TransitionState.Transition,
+                                newTargetScene: SceneKey
+                            ): InterruptionResult {
+                                return InterruptionResult(
+                                    animateFrom = interrupted.currentScene,
+                                    chain = false,
+                                )
+                            }
+                        }
+                },
+            )
+
+        state.setTargetScene(SceneB, coroutineScope = this)
+        state.setTargetScene(SceneC, coroutineScope = this)
+
+        assertThat(state.currentTransitions)
+            .comparingElementsUsing(FromToCurrentTriple)
+            .containsExactly(
+                // B to C.
+                Triple(SceneB, SceneC, SceneC),
+            )
+            .inOrder()
+    }
+
+    @Test
+    fun animateFromOtherScene() = runMonotonicClockTest {
+        val duration = 500
+        val state =
+            MutableSceneTransitionLayoutState(
+                SceneA,
+                transitions {
+                    // Handler that animates from the scene that is not currentScene.
+                    interruptionHandler =
+                        object : InterruptionHandler {
+                            override fun onInterruption(
+                                interrupted: TransitionState.Transition,
+                                newTargetScene: SceneKey
+                            ): InterruptionResult {
+                                return InterruptionResult(
+                                    animateFrom =
+                                        if (interrupted.currentScene == interrupted.toScene) {
+                                            interrupted.fromScene
+                                        } else {
+                                            interrupted.toScene
+                                        }
+                                )
+                            }
+                        }
+
+                    from(SceneA, to = SceneB) { spec = tween(duration) }
+                },
+            )
+
+        // Animate to B and advance the transition a little bit so that progress > visibility
+        // threshold and that reversing from B back to A won't immediately snap to A.
+        state.setTargetScene(SceneB, coroutineScope = this)
+        testScheduler.advanceTimeBy(duration / 2L)
+
+        state.setTargetScene(SceneC, coroutineScope = this)
+
+        assertThat(state.currentTransitions)
+            .comparingElementsUsing(FromToCurrentTriple)
+            .containsExactly(
+                // Initial transition A to B. This transition will never be consumed by anyone given
+                // that it has the same (from, to) pair as the next transition.
+                Triple(SceneA, SceneB, SceneB),
+
+                // Initial transition reversed, B back to A.
+                Triple(SceneA, SceneB, SceneA),
+
+                // A to C.
+                Triple(SceneA, SceneC, SceneC),
+            )
+            .inOrder()
+    }
+
+    @Test
+    fun animateToFromScene() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {})
+
+        // Fake a transition from A to B that has a non 0 velocity.
+        val progressVelocity = 1f
+        val aToB =
+            transition(
+                from = SceneA,
+                to = SceneB,
+                current = { SceneB },
+                // Progress must be > visibility threshold otherwise we will directly snap to A.
+                progress = { 0.5f },
+                progressVelocity = { progressVelocity },
+                onFinish = { launch {} },
+            )
+        state.startTransition(aToB, transitionKey = null)
+
+        // Animate back to A. The previous transition is reversed, i.e. it has the same (from, to)
+        // pair, and its velocity is used when animating the progress back to 0.
+        val bToA = checkNotNull(state.setTargetScene(SceneA, coroutineScope = this))
+        testScheduler.runCurrent()
+        assertThat(bToA).hasFromScene(SceneA)
+        assertThat(bToA).hasToScene(SceneB)
+        assertThat(bToA).hasCurrentScene(SceneA)
+        assertThat(bToA).hasProgressVelocity(progressVelocity)
+    }
+
+    @Test
+    fun animateToToScene() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {})
+
+        // Fake a transition from A to B with current scene = A that has a non 0 velocity.
+        val progressVelocity = -1f
+        val aToB =
+            transition(
+                from = SceneA,
+                to = SceneB,
+                current = { SceneA },
+                progressVelocity = { progressVelocity },
+                onFinish = { launch {} },
+            )
+        state.startTransition(aToB, transitionKey = null)
+
+        // Animate to B. The previous transition is reversed, i.e. it has the same (from, to) pair,
+        // and its velocity is used when animating the progress to 1.
+        val bToA = checkNotNull(state.setTargetScene(SceneB, coroutineScope = this))
+        testScheduler.runCurrent()
+        assertThat(bToA).hasFromScene(SceneA)
+        assertThat(bToA).hasToScene(SceneB)
+        assertThat(bToA).hasCurrentScene(SceneB)
+        assertThat(bToA).hasProgressVelocity(progressVelocity)
+    }
+
+    companion object {
+        val FromToCurrentTriple =
+            Correspondence.transforming(
+                { transition: TransitionState.Transition? ->
+                    Triple(transition?.fromScene, transition?.toScene, transition?.currentScene)
+                },
+                "(from, to, current) triple"
+            )
+    }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
index 224ffe2..9523896 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
@@ -43,6 +43,7 @@
 import androidx.compose.ui.test.performClick
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.subjects.assertThat
 import com.android.compose.test.assertSizeIsEqualTo
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -157,8 +158,8 @@
                             fromSceneZIndex: Float,
                             toSceneZIndex: Float
                         ): SceneKey {
-                            assertThat(transition.fromScene).isEqualTo(TestScenes.SceneA)
-                            assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+                            assertThat(transition).hasFromScene(TestScenes.SceneA)
+                            assertThat(transition).hasToScene(TestScenes.SceneB)
                             assertThat(fromSceneZIndex).isEqualTo(0)
                             assertThat(toSceneZIndex).isEqualTo(1)
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 93e94f8..f29d0a7 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -25,6 +25,7 @@
 import com.android.compose.animation.scene.TestScenes.SceneB
 import com.android.compose.animation.scene.TestScenes.SceneC
 import com.android.compose.animation.scene.TestScenes.SceneD
+import com.android.compose.animation.scene.subjects.assertThat
 import com.android.compose.animation.scene.transition.link.StateLink
 import com.android.compose.test.runMonotonicClockTest
 import com.google.common.truth.Truth.assertThat
@@ -322,8 +323,8 @@
         // Go back to A.
         state.setTargetScene(SceneA, coroutineScope = this)
         testScheduler.advanceUntilIdle()
-        assertThat(state.currentTransition).isNull()
-        assertThat(state.transitionState.currentScene).isEqualTo(SceneA)
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneA)
 
         // Specific transition from A to B.
         assertThat(
@@ -477,23 +478,24 @@
                         overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
                     }
             )
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasNoOverscrollSpec()
 
         // overscroll for SceneA is NOT defined
         progress.value = -0.1f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
 
         // scroll from SceneA to SceneB
         progress.value = 0.5f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
 
         progress.value = 1f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
 
         // overscroll for SceneB is defined
         progress.value = 1.1f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
-        assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneB)
+        val overscrollSpec = assertThat(transition).hasOverscrollSpec()
+        assertThat(overscrollSpec.scene).isEqualTo(SceneB)
     }
 
     @Test
@@ -507,23 +509,25 @@
                         overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
                     }
             )
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasNoOverscrollSpec()
 
         // overscroll for SceneA is defined
         progress.value = -0.1f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
-        assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneA)
+        val overscrollSpec = assertThat(transition).hasOverscrollSpec()
+        assertThat(overscrollSpec.scene).isEqualTo(SceneA)
 
         // scroll from SceneA to SceneB
         progress.value = 0.5f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
 
         progress.value = 1f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
 
         // overscroll for SceneB is NOT defined
         progress.value = 1.1f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
     }
 
     @Test
@@ -534,22 +538,24 @@
                 progress = { progress.value },
                 sceneTransitions = transitions {}
             )
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasNoOverscrollSpec()
 
         // overscroll for SceneA is NOT defined
         progress.value = -0.1f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
 
         // scroll from SceneA to SceneB
         progress.value = 0.5f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
 
         progress.value = 1f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
 
         // overscroll for SceneB is NOT defined
         progress.value = 1.1f
-        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+        assertThat(transition).hasNoOverscrollSpec()
     }
 
     @Test
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 7836581..692c18b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -51,6 +51,7 @@
 import com.android.compose.animation.scene.TestScenes.SceneA
 import com.android.compose.animation.scene.TestScenes.SceneB
 import com.android.compose.animation.scene.TestScenes.SceneC
+import com.android.compose.animation.scene.subjects.assertThat
 import com.android.compose.test.assertSizeIsEqualTo
 import com.android.compose.test.subjects.DpOffsetSubject
 import com.android.compose.test.subjects.assertThat
@@ -147,34 +148,34 @@
         rule.onNodeWithText("SceneA").assertIsDisplayed()
         rule.onNodeWithText("SceneB").assertDoesNotExist()
         rule.onNodeWithText("SceneC").assertDoesNotExist()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         // Change to scene B. Only that scene is displayed.
         currentScene = SceneB
         rule.onNodeWithText("SceneA").assertDoesNotExist()
         rule.onNodeWithText("SceneB").assertIsDisplayed()
         rule.onNodeWithText("SceneC").assertDoesNotExist()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneB)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
     }
 
     @Test
     fun testBack() {
         rule.setContent { TestContent() }
 
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         rule.activity.onBackPressed()
         rule.waitForIdle()
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneB)
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
     }
 
     @Test
     fun testTransitionState() {
         rule.setContent { TestContent() }
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         // We will advance the clock manually.
         rule.mainClock.autoAdvance = false
@@ -182,45 +183,38 @@
         // Change the current scene. Until composition is triggered, this won't change the layout
         // state.
         currentScene = SceneB
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         // On the next frame, we will recompose because currentScene changed, which will start the
         // transition (i.e. it will change the transitionState to be a Transition) in a
         // LaunchedEffect.
         rule.mainClock.advanceTimeByFrame()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
-        val transition = layoutState.transitionState as TransitionState.Transition
-        assertThat(transition.fromScene).isEqualTo(SceneA)
-        assertThat(transition.toScene).isEqualTo(SceneB)
-        assertThat(transition.progress).isEqualTo(0f)
+        val transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneB)
+        assertThat(transition).hasProgress(0f)
 
         // Then, on the next frame, the animator we started gets its initial value and clock
         // starting time. We are now at progress = 0f.
         rule.mainClock.advanceTimeByFrame()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
-            .isEqualTo(0f)
+        assertThat(transition).hasProgress(0f)
 
         // The test transition lasts 480ms. 240ms after the start of the transition, we are at
         // progress = 0.5f.
         rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
-            .isEqualTo(0.5f)
+        assertThat(transition).hasProgress(0.5f)
 
         // (240-16) ms later, i.e. one frame before the transition is finished, we are at
         // progress=(480-16)/480.
         rule.mainClock.advanceTimeBy(TestTransitionDuration / 2 - 16)
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
-            .isEqualTo((TestTransitionDuration - 16) / 480f)
+        assertThat(transition).hasProgress((TestTransitionDuration - 16) / 480f)
 
         // one frame (16ms) later, the transition is finished and we are in the idle state in scene
         // B.
         rule.mainClock.advanceTimeByFrame()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneB)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
     }
 
     @Test
@@ -261,8 +255,8 @@
         // 100.dp. We pause at the middle of the transition, so it should now be 75.dp given that we
         // use a linear interpolator. Foo was at (x = layoutSize - 50dp, y = 0) in SceneA and is
         // going to (x = 0, y = 0), so the offset should now be half what it was.
-        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
-            .isEqualTo(0.5f)
+        var transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
         sharedFoo.assertWidthIsEqualTo(75.dp)
         sharedFoo.assertHeightIsEqualTo(75.dp)
         sharedFoo.assertPositionInRootIsEqualTo(
@@ -290,8 +284,8 @@
         val expectedSize = 100.dp + (150.dp - 100.dp) * interpolatedProgress
 
         sharedFoo = rule.onNode(isElement(TestElements.Foo, SceneC))
-        assertThat((layoutState.transitionState as TransitionState.Transition).progress)
-            .isEqualTo(interpolatedProgress)
+        transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasProgress(interpolatedProgress)
         sharedFoo.assertWidthIsEqualTo(expectedSize)
         sharedFoo.assertHeightIsEqualTo(expectedSize)
         sharedFoo.assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
@@ -305,16 +299,16 @@
 
         // Wait for the transition to C to finish.
         rule.mainClock.advanceTimeBy(TestTransitionDuration)
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneC)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneC)
 
         // Go back to scene A. This should happen instantly (once the animation started, i.e. after
         // 2 frames) given that we use a snap() animation spec.
         currentScene = SceneA
         rule.mainClock.advanceTimeByFrame()
         rule.mainClock.advanceTimeByFrame()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
     }
 
     @Test
@@ -384,7 +378,9 @@
         rule.mainClock.advanceTimeByFrame()
         rule.mainClock.advanceTimeBy(duration / 2)
         rule.waitForIdle()
-        assertThat(state.currentTransition?.progress).isEqualTo(0.5f)
+
+        var transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
 
         // A and B are composed.
         rule.onNodeWithTag("aRoot").assertExists()
@@ -396,7 +392,9 @@
         rule.mainClock.advanceTimeByFrame()
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
-        assertThat(state.currentTransition?.progress).isEqualTo(0f)
+
+        transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0f)
 
         // A, B and C are composed.
         rule.onNodeWithTag("aRoot").assertExists()
@@ -405,7 +403,7 @@
 
         // Let A => B finish.
         rule.mainClock.advanceTimeBy(duration / 2L)
-        assertThat(state.currentTransition?.progress).isEqualTo(0.5f)
+        assertThat(transition).hasProgress(0.5f)
         rule.waitForIdle()
 
         // B and C are composed.
@@ -416,8 +414,8 @@
         // Let B => C finish.
         rule.mainClock.advanceTimeBy(duration / 2L)
         rule.mainClock.advanceTimeByFrame()
-        assertThat(state.currentTransition).isNull()
         rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
 
         // Only C is composed.
         rule.onNodeWithTag("aRoot").assertDoesNotExist()
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index f034c18..1dd9322 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -38,6 +38,9 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.subjects.assertThat
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -65,7 +68,7 @@
     @get:Rule val rule = createComposeRule()
 
     private fun layoutState(
-        initialScene: SceneKey = TestScenes.SceneA,
+        initialScene: SceneKey = SceneA,
         transitions: SceneTransitions = EmptyTestTransitions,
     ) = MutableSceneTransitionLayoutState(initialScene, transitions)
 
@@ -80,22 +83,21 @@
             modifier = Modifier.size(LayoutWidth, LayoutHeight).testTag(TestElements.Foo.debugName),
         ) {
             scene(
-                TestScenes.SceneA,
+                SceneA,
                 userActions =
                     if (swipesEnabled())
                         mapOf(
-                            Swipe.Left to TestScenes.SceneB,
+                            Swipe.Left to SceneB,
                             Swipe.Down to TestScenes.SceneC,
-                            Swipe.Up to TestScenes.SceneB,
+                            Swipe.Up to SceneB,
                         )
                     else emptyMap(),
             ) {
                 Box(Modifier.fillMaxSize())
             }
             scene(
-                TestScenes.SceneB,
-                userActions =
-                    if (swipesEnabled()) mapOf(Swipe.Right to TestScenes.SceneA) else emptyMap(),
+                SceneB,
+                userActions = if (swipesEnabled()) mapOf(Swipe.Right to SceneA) else emptyMap(),
             ) {
                 Box(Modifier.fillMaxSize())
             }
@@ -104,11 +106,10 @@
                 userActions =
                     if (swipesEnabled())
                         mapOf(
-                            Swipe.Down to TestScenes.SceneA,
-                            Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
-                            Swipe(SwipeDirection.Right, fromSource = Edge.Left) to
-                                TestScenes.SceneB,
-                            Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB,
+                            Swipe.Down to SceneA,
+                            Swipe(SwipeDirection.Down, pointerCount = 2) to SceneB,
+                            Swipe(SwipeDirection.Right, fromSource = Edge.Left) to SceneB,
+                            Swipe(SwipeDirection.Down, fromSource = Edge.Top) to SceneB,
                         )
                     else emptyMap(),
             ) {
@@ -129,8 +130,8 @@
             TestContent(layoutState)
         }
 
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         // Drag left (i.e. from right to left) by 55dp. We pick 55dp here because 56dp is the
         // positional threshold from which we commit the gesture.
@@ -144,31 +145,27 @@
 
         // We should be at a progress = 55dp / LayoutWidth given that we use the layout size in
         // the gesture axis as swipe distance.
-        var transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneA)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
-        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
-        assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
-        assertThat(transition.isInitiatedByUserInput).isTrue()
+        var transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneB)
+        assertThat(transition).hasCurrentScene(SceneA)
+        assertThat(transition).hasProgress(55.dp / LayoutWidth)
+        assertThat(transition).isInitiatedByUserInput()
 
         // Release the finger. We should now be animating back to A (currentScene = SceneA) given
         // that 55dp < positional threshold.
         rule.onRoot().performTouchInput { up() }
-        transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneA)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
-        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
-        assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
-        assertThat(transition.isInitiatedByUserInput).isTrue()
+        transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneB)
+        assertThat(transition).hasCurrentScene(SceneA)
+        assertThat(transition).hasProgress(55.dp / LayoutWidth)
+        assertThat(transition).isInitiatedByUserInput()
 
         // Wait for the animation to finish. We should now be in scene A.
         rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         // Now we do the same but vertically and with a drag distance of 56dp, which is >=
         // positional threshold.
@@ -178,31 +175,27 @@
         }
 
         // Drag is in progress, so currentScene = SceneA and progress = 56dp / LayoutHeight
-        transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneA)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
-        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
-        assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
-        assertThat(transition.isInitiatedByUserInput).isTrue()
+        transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(TestScenes.SceneC)
+        assertThat(transition).hasCurrentScene(SceneA)
+        assertThat(transition).hasProgress(56.dp / LayoutHeight)
+        assertThat(transition).isInitiatedByUserInput()
 
         // Release the finger. We should now be animating to C (currentScene = SceneC) given
         // that 56dp >= positional threshold.
         rule.onRoot().performTouchInput { up() }
-        transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneA)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
-        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
-        assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
-        assertThat(transition.isInitiatedByUserInput).isTrue()
+        transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(TestScenes.SceneC)
+        assertThat(transition).hasCurrentScene(TestScenes.SceneC)
+        assertThat(transition).hasProgress(56.dp / LayoutHeight)
+        assertThat(transition).isInitiatedByUserInput()
 
         // Wait for the animation to finish. We should now be in scene C.
         rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
     }
 
     @Test
@@ -216,8 +209,8 @@
             TestContent(layoutState)
         }
 
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         // Swipe left (i.e. from right to left) using a velocity of 124 dp/s. We pick 124 dp/s here
         // because 125 dp/s is the velocity threshold from which we commit the gesture. We also use
@@ -233,18 +226,16 @@
 
         // We should be animating back to A (currentScene = SceneA) given that 124 dp/s < velocity
         // threshold.
-        var transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneA)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
-        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
-        assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+        var transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneB)
+        assertThat(transition).hasCurrentScene(SceneA)
+        assertThat(transition).hasProgress(55.dp / LayoutWidth)
 
         // Wait for the animation to finish. We should now be in scene A.
         rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         // Now we do the same but vertically and with a swipe velocity of 126dp, which is >
         // velocity threshold. Note that in theory we could have used 125 dp (= velocity threshold)
@@ -259,18 +250,16 @@
         }
 
         // We should be animating to C (currentScene = SceneC).
-        transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneA)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
-        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
-        assertThat(transition.progress).isEqualTo(55.dp / LayoutHeight)
+        transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(TestScenes.SceneC)
+        assertThat(transition).hasCurrentScene(TestScenes.SceneC)
+        assertThat(transition).hasProgress(55.dp / LayoutHeight)
 
         // Wait for the animation to finish. We should now be in scene C.
         rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
     }
 
     @Test
@@ -286,8 +275,8 @@
             TestContent(layoutState)
         }
 
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
 
         // Swipe down with two fingers.
         rule.onRoot().performTouchInput {
@@ -298,18 +287,16 @@
         }
 
         // We are transitioning to B because we used 2 fingers.
-        val transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneC)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+        val transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(TestScenes.SceneC)
+        assertThat(transition).hasToScene(SceneB)
 
         // Release the fingers and wait for the animation to end. We are back to C because we only
         // swiped 10dp.
         rule.onRoot().performTouchInput { repeat(2) { i -> up(pointerId = i) } }
         rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
     }
 
     @Test
@@ -325,8 +312,8 @@
             TestContent(layoutState)
         }
 
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
 
         // Swipe down from the top edge.
         rule.onRoot().performTouchInput {
@@ -335,18 +322,16 @@
         }
 
         // We are transitioning to B (and not A) because we started from the top edge.
-        var transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneC)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+        var transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(TestScenes.SceneC)
+        assertThat(transition).hasToScene(SceneB)
 
         // Release the fingers and wait for the animation to end. We are back to C because we only
         // swiped 10dp.
         rule.onRoot().performTouchInput { up() }
         rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
 
         // Swipe right from the left edge.
         rule.onRoot().performTouchInput {
@@ -355,18 +340,16 @@
         }
 
         // We are transitioning to B (and not A) because we started from the left edge.
-        transition = layoutState.transitionState
-        assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
-        assertThat((transition as TransitionState.Transition).fromScene)
-            .isEqualTo(TestScenes.SceneC)
-        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+        transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasFromScene(TestScenes.SceneC)
+        assertThat(transition).hasToScene(SceneB)
 
         // Release the fingers and wait for the animation to end. We are back to C because we only
         // swiped 10dp.
         rule.onRoot().performTouchInput { up() }
         rule.waitForIdle()
-        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
     }
 
     @Test
@@ -380,7 +363,7 @@
             layoutState(
                 transitions =
                     transitions {
-                        from(TestScenes.SceneA, to = TestScenes.SceneB) {
+                        from(SceneA, to = SceneB) {
                             distance = FixedDistance(verticalSwipeDistance)
                         }
                     }
@@ -395,12 +378,12 @@
                 modifier = Modifier.size(LayoutWidth, LayoutHeight)
             ) {
                 scene(
-                    TestScenes.SceneA,
-                    userActions = mapOf(Swipe.Down to TestScenes.SceneB),
+                    SceneA,
+                    userActions = mapOf(Swipe.Down to SceneB),
                 ) {
                     Spacer(Modifier.fillMaxSize())
                 }
-                scene(TestScenes.SceneB) { Spacer(Modifier.fillMaxSize()) }
+                scene(SceneB) { Spacer(Modifier.fillMaxSize()) }
             }
         }
 
@@ -413,9 +396,9 @@
         }
 
         // We should be at 50%
-        val transition = layoutState.currentTransition
+        val transition = assertThat(layoutState.transitionState).isTransition()
         assertThat(transition).isNotNull()
-        assertThat(transition!!.progress).isEqualTo(0.5f)
+        assertThat(transition).hasProgress(0.5f)
     }
 
     @Test
@@ -434,15 +417,14 @@
         }
 
         // We should still correctly compute that we are swiping down to scene C.
-        var transition = layoutState.currentTransition
-        assertThat(transition).isNotNull()
-        assertThat(transition?.toScene).isEqualTo(TestScenes.SceneC)
+        var transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasToScene(TestScenes.SceneC)
 
         // Release the finger, animating back to scene A.
         rule.onRoot().performTouchInput { up() }
         rule.waitForIdle()
-        assertThat(layoutState.currentTransition).isNull()
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         // Swipe up by exactly touchSlop, so that the drag overSlop is 0f.
         rule.onRoot().performTouchInput {
@@ -451,15 +433,14 @@
         }
 
         // We should still correctly compute that we are swiping up to scene B.
-        transition = layoutState.currentTransition
-        assertThat(transition).isNotNull()
-        assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
+        transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasToScene(SceneB)
 
         // Release the finger, animating back to scene A.
         rule.onRoot().performTouchInput { up() }
         rule.waitForIdle()
-        assertThat(layoutState.currentTransition).isNull()
-        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
 
         // Swipe left by exactly touchSlop, so that the drag overSlop is 0f.
         rule.onRoot().performTouchInput {
@@ -468,14 +449,13 @@
         }
 
         // We should still correctly compute that we are swiping down to scene B.
-        transition = layoutState.currentTransition
-        assertThat(transition).isNotNull()
-        assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
+        transition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(transition).hasToScene(SceneB)
     }
 
     @Test
     fun swipeEnabledLater() {
-        val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+        val layoutState = MutableSceneTransitionLayoutState(SceneA)
         var swipesEnabled by mutableStateOf(false)
         var touchSlop = 0f
         rule.setContent {
@@ -509,34 +489,32 @@
     fun transitionKey() {
         val transitionkey = TransitionKey(debugName = "foo")
         val state =
-            MutableSceneTransitionLayoutState(
-                TestScenes.SceneA,
+            MutableSceneTransitionLayoutStateImpl(
+                SceneA,
                 transitions {
-                    from(TestScenes.SceneA, to = TestScenes.SceneB) { fade(TestElements.Foo) }
-                    from(TestScenes.SceneA, to = TestScenes.SceneB, key = transitionkey) {
+                    from(SceneA, to = SceneB) { fade(TestElements.Foo) }
+                    from(SceneA, to = SceneB, key = transitionkey) {
                         fade(TestElements.Foo)
                         fade(TestElements.Bar)
                     }
                 }
             )
-                as MutableSceneTransitionLayoutStateImpl
 
         var touchSlop = 0f
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
             SceneTransitionLayout(state, Modifier.size(LayoutWidth, LayoutHeight)) {
                 scene(
-                    TestScenes.SceneA,
+                    SceneA,
                     userActions =
                         mapOf(
-                            Swipe.Down to TestScenes.SceneB,
-                            Swipe.Up to
-                                UserActionResult(TestScenes.SceneB, transitionKey = transitionkey)
+                            Swipe.Down to SceneB,
+                            Swipe.Up to UserActionResult(SceneB, transitionKey = transitionkey)
                         )
                 ) {
                     Box(Modifier.fillMaxSize())
                 }
-                scene(TestScenes.SceneB) { Box(Modifier.fillMaxSize()) }
+                scene(SceneB) { Box(Modifier.fillMaxSize()) }
             }
         }
 
@@ -546,12 +524,12 @@
             moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
         }
 
-        assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
+        assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue()
         assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1)
 
         // Move the pointer up to swipe to scene B using the new transition.
         rule.onRoot().performTouchInput { moveBy(Offset(0f, -1.dp.toPx()), delayMillis = 1_000) }
-        assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
+        assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue()
         assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2)
     }
 
@@ -567,19 +545,17 @@
                     // the difference between the bottom of the scene and the bottom of the element,
                     // so that we use the offset and size of the element as well as the size of the
                     // scene.
-                    val fooSize = TestElements.Foo.targetSize(TestScenes.SceneB) ?: return 0f
-                    val fooOffset = TestElements.Foo.targetOffset(TestScenes.SceneB) ?: return 0f
-                    val sceneSize = TestScenes.SceneB.targetSize() ?: return 0f
+                    val fooSize = TestElements.Foo.targetSize(SceneB) ?: return 0f
+                    val fooOffset = TestElements.Foo.targetOffset(SceneB) ?: return 0f
+                    val sceneSize = SceneB.targetSize() ?: return 0f
                     return sceneSize.height - fooOffset.y - fooSize.height
                 }
             }
 
         val state =
             MutableSceneTransitionLayoutState(
-                TestScenes.SceneA,
-                transitions {
-                    from(TestScenes.SceneA, to = TestScenes.SceneB) { distance = swipeDistance }
-                }
+                SceneA,
+                transitions { from(SceneA, to = SceneB) { distance = swipeDistance } }
             )
 
         val layoutSize = 200.dp
@@ -591,10 +567,10 @@
             touchSlop = LocalViewConfiguration.current.touchSlop
 
             SceneTransitionLayout(state, Modifier.size(layoutSize)) {
-                scene(TestScenes.SceneA, userActions = mapOf(Swipe.Up to TestScenes.SceneB)) {
+                scene(SceneA, userActions = mapOf(Swipe.Up to SceneB)) {
                     Box(Modifier.fillMaxSize())
                 }
-                scene(TestScenes.SceneB) {
+                scene(SceneB) {
                     Box(Modifier.fillMaxSize()) {
                         Box(Modifier.offset(y = fooYOffset).element(TestElements.Foo).size(fooSize))
                     }
@@ -611,7 +587,9 @@
         }
 
         rule.waitForIdle()
-        assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
-        assertThat(state.currentTransition!!.progress).isWithin(0.01f).of(0.5f)
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasFromScene(SceneA)
+        assertThat(transition).hasToScene(SceneB)
+        assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
     }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
index c49a5b8..a609be4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
@@ -29,6 +29,7 @@
     to: SceneKey,
     current: () -> SceneKey = { from },
     progress: () -> Float = { 0f },
+    progressVelocity: () -> Float = { 0f },
     interruptionProgress: () -> Float = { 100f },
     isInitiatedByUserInput: Boolean = false,
     isUserInputOngoing: Boolean = false,
@@ -42,6 +43,8 @@
             get() = current()
         override val progress: Float
             get() = progress()
+        override val progressVelocity: Float
+            get() = progressVelocity()
 
         override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput
         override val isUserInputOngoing: Boolean = isUserInputOngoing
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
new file mode 100644
index 0000000..3489892
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.compose.animation.scene.subjects
+
+import com.android.compose.animation.scene.OverscrollSpec
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionState
+import com.google.common.truth.Fact.simpleFact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth
+
+/** Assert on a [TransitionState]. */
+fun assertThat(state: TransitionState): TransitionStateSubject {
+    return Truth.assertAbout(TransitionStateSubject.transitionStates()).that(state)
+}
+
+/** Assert on a [TransitionState.Transition]. */
+fun assertThat(transitions: TransitionState.Transition): TransitionSubject {
+    return Truth.assertAbout(TransitionSubject.transitions()).that(transitions)
+}
+
+class TransitionStateSubject
+private constructor(
+    metadata: FailureMetadata,
+    private val actual: TransitionState,
+) : Subject(metadata, actual) {
+    fun hasCurrentScene(sceneKey: SceneKey) {
+        check("currentScene").that(actual.currentScene).isEqualTo(sceneKey)
+    }
+
+    fun isIdle(): TransitionState.Idle {
+        if (actual !is TransitionState.Idle) {
+            failWithActual(simpleFact("expected to be TransitionState.Idle"))
+        }
+
+        return actual as TransitionState.Idle
+    }
+
+    fun isTransition(): TransitionState.Transition {
+        if (actual !is TransitionState.Transition) {
+            failWithActual(simpleFact("expected to be TransitionState.Transition"))
+        }
+
+        return actual as TransitionState.Transition
+    }
+
+    companion object {
+        fun transitionStates() = Factory { metadata, actual: TransitionState ->
+            TransitionStateSubject(metadata, actual)
+        }
+    }
+}
+
+class TransitionSubject
+private constructor(
+    metadata: FailureMetadata,
+    private val actual: TransitionState.Transition,
+) : Subject(metadata, actual) {
+    fun hasCurrentScene(sceneKey: SceneKey) {
+        check("currentScene").that(actual.currentScene).isEqualTo(sceneKey)
+    }
+
+    fun hasFromScene(sceneKey: SceneKey) {
+        check("fromScene").that(actual.fromScene).isEqualTo(sceneKey)
+    }
+
+    fun hasToScene(sceneKey: SceneKey) {
+        check("toScene").that(actual.toScene).isEqualTo(sceneKey)
+    }
+
+    fun hasProgress(progress: Float, tolerance: Float = 0f) {
+        check("progress").that(actual.progress).isWithin(tolerance).of(progress)
+    }
+
+    fun hasProgressVelocity(progressVelocity: Float, tolerance: Float = 0f) {
+        check("progressVelocity")
+            .that(actual.progressVelocity)
+            .isWithin(tolerance)
+            .of(progressVelocity)
+    }
+
+    fun isInitiatedByUserInput() {
+        check("isInitiatedByUserInput").that(actual.isInitiatedByUserInput).isTrue()
+    }
+
+    fun hasIsUserInputOngoing(isUserInputOngoing: Boolean) {
+        check("isUserInputOngoing").that(actual.isUserInputOngoing).isEqualTo(isUserInputOngoing)
+    }
+
+    fun hasOverscrollSpec(): OverscrollSpec {
+        check("currentOverscrollSpec").that(actual.currentOverscrollSpec).isNotNull()
+        return actual.currentOverscrollSpec!!
+    }
+
+    fun hasNoOverscrollSpec() {
+        check("currentOverscrollSpec").that(actual.currentOverscrollSpec).isNull()
+    }
+
+    fun hasBouncingScene(scene: SceneKey) {
+        if (actual !is TransitionState.HasOverscrollProperties) {
+            failWithActual(simpleFact("expected to be TransitionState.HasOverscrollProperties"))
+        }
+
+        check("bouncingScene")
+            .that((actual as TransitionState.HasOverscrollProperties).bouncingScene)
+            .isEqualTo(scene)
+    }
+
+    companion object {
+        fun transitions() = Factory { metadata, actual: TransitionState.Transition ->
+            TransitionSubject(metadata, actual)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 447c280..c8717d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -17,6 +17,8 @@
 
 package com.android.keyguard
 
+import android.app.admin.DevicePolicyManager
+import android.app.admin.flags.Flags as DevicePolicyFlags
 import android.content.res.Configuration
 import android.media.AudioManager
 import android.telephony.TelephonyManager
@@ -148,6 +150,7 @@
     @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate
     @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
     @Mock private lateinit var postureController: DevicePostureController
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
 
     @Captor
     private lateinit var swipeListenerArgumentCaptor:
@@ -273,6 +276,7 @@
                 mSelectedUserInteractor,
                 deviceProvisionedController,
                 faceAuthAccessibilityDelegate,
+                devicePolicyManager,
                 keyguardTransitionInteractor,
                 { primaryBouncerInteractor },
             ) {
@@ -934,6 +938,45 @@
         verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any())
     }
 
+    @Test
+    fun showAlmostAtWipeDialog_calledOnMainUser_setsCorrectUserType() {
+        mSetFlagsRule.enableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
+        val mainUserId = 10
+
+        underTest.showMessageForFailedUnlockAttempt(
+            /* userId = */ mainUserId,
+            /* expiringUserId = */ mainUserId,
+            /* mainUserId = */ mainUserId,
+            /* remainingBeforeWipe = */ 1,
+            /* failedAttempts = */ 1
+        )
+
+        verify(view)
+            .showAlmostAtWipeDialog(any(), any(), eq(KeyguardSecurityContainer.USER_TYPE_PRIMARY))
+    }
+
+    @Test
+    fun showAlmostAtWipeDialog_calledOnNonMainUser_setsCorrectUserType() {
+        mSetFlagsRule.enableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
+        val secondaryUserId = 10
+        val mainUserId = 0
+
+        underTest.showMessageForFailedUnlockAttempt(
+            /* userId = */ secondaryUserId,
+            /* expiringUserId = */ secondaryUserId,
+            /* mainUserId = */ mainUserId,
+            /* remainingBeforeWipe = */ 1,
+            /* failedAttempts = */ 1
+        )
+
+        verify(view)
+            .showAlmostAtWipeDialog(
+                any(),
+                any(),
+                eq(KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER)
+            )
+    }
+
     private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener
         get() {
             underTest.onViewAttached()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 81878aa..0c5e726 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.authentication.domain.interactor
 
 import android.app.admin.DevicePolicyManager
+import android.app.admin.flags.Flags as DevicePolicyFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
@@ -32,6 +34,8 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -410,12 +414,16 @@
         }
 
     @Test
+    @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
     fun upcomingWipe() =
         testScope.runTest {
             val upcomingWipe by collectLastValue(underTest.upcomingWipe)
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
             val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
             val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+            kosmos.fakeUserRepository.asMainUser()
+            kosmos.fakeAuthenticationRepository.profileWithMinFailedUnlockAttemptsForWipe =
+                FakeUserRepository.MAIN_USER_ID
 
             underTest.authenticate(correctPin)
             assertThat(upcomingWipe).isNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 20beabb..2546f27 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.coroutines.FlowValue
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
@@ -144,6 +145,7 @@
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val testScope = kosmos.testScope
     private val fakeUserRepository = kosmos.fakeUserRepository
+    private val fakeExecutor = kosmos.fakeExecutor
     private lateinit var authStatus: FlowValue<FaceAuthenticationStatus?>
     private lateinit var detectStatus: FlowValue<FaceDetectionStatus?>
     private lateinit var authRunning: FlowValue<Boolean?>
@@ -220,12 +222,12 @@
             testScope.backgroundScope,
             testDispatcher,
             testDispatcher,
+            fakeExecutor,
             sessionTracker,
             uiEventLogger,
             FaceAuthenticationLogger(logcatLogBuffer("DeviceEntryFaceAuthRepositoryLog")),
             biometricSettingsRepository,
             deviceEntryFingerprintAuthRepository,
-            trustRepository,
             keyguardRepository,
             powerInteractor,
             keyguardInteractor,
@@ -292,6 +294,7 @@
     fun faceLockoutStatusIsPropagated() =
         testScope.runTest {
             initCollectors()
+            fakeExecutor.runAllReady()
             verify(faceManager).addLockoutResetCallback(faceLockoutResetCallback.capture())
             allPreconditionsToRunFaceAuthAreTrue()
 
@@ -1177,6 +1180,7 @@
     }
 
     private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() {
+        fakeExecutor.runAllReady()
         verify(faceManager, atLeastOnce())
             .addLockoutResetCallback(faceLockoutResetCallback.capture())
         trustRepository.setCurrentUserTrusted(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
similarity index 97%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
index abc684c..5661bd3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.shade.ui.viewmodel
+package com.android.systemui.notifications.ui.viewmodel
 
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -32,6 +32,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneViewModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
similarity index 94%
copy from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
copy to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
index abc684c..034c2e9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.shade.ui.viewmodel
+package com.android.systemui.qs.ui.viewmodel
 
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -32,6 +32,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.ui.viewmodel.quickSettingsShadeSceneViewModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -46,14 +47,14 @@
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 @EnableSceneContainer
-class NotificationsShadeSceneViewModelTest : SysuiTestCase() {
+class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val sceneInteractor = kosmos.sceneInteractor
     private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
 
-    private val underTest = kosmos.notificationsShadeSceneViewModel
+    private val underTest = kosmos.quickSettingsShadeSceneViewModel
 
     @Test
     fun upTransitionSceneKey_deviceLocked_lockscreen() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 66f7416..d6e3879 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -20,7 +20,7 @@
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -79,12 +79,12 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
 import java.util.List;
 import java.util.concurrent.Executor;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 @RunWith(ParameterizedAndroidJunit4.class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
@@ -341,7 +341,7 @@
         verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
         assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) != 0).isTrue();
         assertThat(
-                (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING)
+                (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY)
                         != 0)
                 .isTrue();
     }
@@ -353,7 +353,7 @@
         verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
         assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) == 0).isTrue();
         assertThat(
-                (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING)
+                (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY)
                         == 0)
                 .isTrue();
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
new file mode 100644
index 0000000..35e4047
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notifications.ui.composable.NotificationScrimNestedScrollConnection
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() {
+    private var isStarted = false
+    private var scrimOffset = 0f
+    private var contentHeight = 0f
+    private var isCurrentGestureOverscroll = false
+
+    private val scrollConnection =
+        NotificationScrimNestedScrollConnection(
+            scrimOffset = { scrimOffset },
+            snapScrimOffset = { _ -> },
+            animateScrimOffset = { _ -> },
+            minScrimOffset = { MIN_SCRIM_OFFSET },
+            maxScrimOffset = MAX_SCRIM_OFFSET,
+            contentHeight = { contentHeight },
+            minVisibleScrimHeight = { MIN_VISIBLE_SCRIM_HEIGHT },
+            isCurrentGestureOverscroll = { isCurrentGestureOverscroll },
+            onStart = { isStarted = true },
+            onStop = { isStarted = false },
+        )
+
+    @Test
+    fun onScrollUp_canStartPreScroll_contentNotExpanded_ignoreScroll() = runTest {
+        contentHeight = COLLAPSED_CONTENT_HEIGHT
+
+        val offsetConsumed =
+            scrollConnection.onPreScroll(
+                available = Offset(x = 0f, y = -1f),
+                source = NestedScrollSource.Drag,
+            )
+
+        assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+        assertThat(isStarted).isEqualTo(false)
+    }
+
+    @Test
+    fun onScrollUp_canStartPreScroll_contentExpandedAtMinOffset_ignoreScroll() = runTest {
+        contentHeight = EXPANDED_CONTENT_HEIGHT
+        scrimOffset = MIN_SCRIM_OFFSET
+
+        val offsetConsumed =
+            scrollConnection.onPreScroll(
+                available = Offset(x = 0f, y = -1f),
+                source = NestedScrollSource.Drag,
+            )
+
+        assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+        assertThat(isStarted).isEqualTo(false)
+    }
+
+    @Test
+    fun onScrollUp_canStartPreScroll_contentExpanded_consumeScroll() = runTest {
+        contentHeight = EXPANDED_CONTENT_HEIGHT
+
+        val availableOffset = Offset(x = 0f, y = -1f)
+        val offsetConsumed =
+            scrollConnection.onPreScroll(
+                available = availableOffset,
+                source = NestedScrollSource.Drag,
+            )
+
+        assertThat(offsetConsumed).isEqualTo(availableOffset)
+        assertThat(isStarted).isEqualTo(true)
+    }
+
+    @Test
+    fun onScrollUp_canStartPreScroll_contentExpanded_consumeScrollWithRemainder() = runTest {
+        contentHeight = EXPANDED_CONTENT_HEIGHT
+        scrimOffset = MIN_SCRIM_OFFSET + 1
+
+        val availableOffset = Offset(x = 0f, y = -2f)
+        val consumableOffset = Offset(x = 0f, y = -1f)
+        val offsetConsumed =
+            scrollConnection.onPreScroll(
+                available = availableOffset,
+                source = NestedScrollSource.Drag,
+            )
+
+        assertThat(offsetConsumed).isEqualTo(consumableOffset)
+        assertThat(isStarted).isEqualTo(true)
+    }
+
+    @Test
+    fun onScrollUp_canStartPostScroll_ignoreScroll() = runTest {
+        val offsetConsumed =
+            scrollConnection.onPostScroll(
+                consumed = Offset.Zero,
+                available = Offset(x = 0f, y = -1f),
+                source = NestedScrollSource.Drag,
+            )
+
+        assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+        assertThat(isStarted).isEqualTo(false)
+    }
+
+    @Test
+    fun onScrollDown_canStartPreScroll_ignoreScroll() = runTest {
+        val offsetConsumed =
+            scrollConnection.onPreScroll(
+                available = Offset(x = 0f, y = 1f),
+                source = NestedScrollSource.Drag,
+            )
+
+        assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+        assertThat(isStarted).isEqualTo(false)
+    }
+
+    @Test
+    fun onScrollDown_canStartPostScroll_consumeScroll() = runTest {
+        scrimOffset = MIN_SCRIM_OFFSET
+
+        val availableOffset = Offset(x = 0f, y = 1f)
+        val offsetConsumed =
+            scrollConnection.onPostScroll(
+                consumed = Offset.Zero,
+                available = availableOffset,
+                source = NestedScrollSource.Drag
+            )
+
+        assertThat(offsetConsumed).isEqualTo(availableOffset)
+        assertThat(isStarted).isEqualTo(true)
+    }
+
+    @Test
+    fun onScrollDown_canStartPostScroll_consumeScrollWithRemainder() = runTest {
+        scrimOffset = MAX_SCRIM_OFFSET - 1
+
+        val availableOffset = Offset(x = 0f, y = 2f)
+        val consumableOffset = Offset(x = 0f, y = 1f)
+        val offsetConsumed =
+            scrollConnection.onPostScroll(
+                consumed = Offset.Zero,
+                available = availableOffset,
+                source = NestedScrollSource.Drag
+            )
+
+        assertThat(offsetConsumed).isEqualTo(consumableOffset)
+        assertThat(isStarted).isEqualTo(true)
+    }
+
+    @Test
+    fun canStartPostScroll_atMaxOffset_ignoreScroll() = runTest {
+        scrimOffset = MAX_SCRIM_OFFSET
+
+        val offsetConsumed =
+            scrollConnection.onPostScroll(
+                consumed = Offset.Zero,
+                available = Offset(x = 0f, y = 1f),
+                source = NestedScrollSource.Drag
+            )
+
+        assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+        assertThat(isStarted).isEqualTo(false)
+    }
+
+    @Test
+    fun canStartPostScroll_externalOverscrollGesture_startButIgnoreScroll() = runTest {
+        scrimOffset = MAX_SCRIM_OFFSET
+        isCurrentGestureOverscroll = true
+
+        val offsetConsumed =
+            scrollConnection.onPostScroll(
+                consumed = Offset.Zero,
+                available = Offset(x = 0f, y = 1f),
+                source = NestedScrollSource.Drag
+            )
+
+        assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+        assertThat(isStarted).isEqualTo(true)
+    }
+
+    @Test
+    fun canContinueScroll_inBetweenMinMaxOffset_true() = runTest {
+        scrimOffset = (MIN_SCRIM_OFFSET + MAX_SCRIM_OFFSET) / 2f
+        contentHeight = EXPANDED_CONTENT_HEIGHT
+        scrollConnection.onPreScroll(
+            available = Offset(x = 0f, y = -1f),
+            source = NestedScrollSource.Drag
+        )
+
+        assertThat(isStarted).isEqualTo(true)
+
+        scrollConnection.onPreScroll(
+            available = Offset(x = 0f, y = 1f),
+            source = NestedScrollSource.Drag
+        )
+
+        assertThat(isStarted).isEqualTo(true)
+    }
+
+    @Test
+    fun canContinueScroll_atMaxOffset_false() = runTest {
+        scrimOffset = MAX_SCRIM_OFFSET
+        contentHeight = EXPANDED_CONTENT_HEIGHT
+        scrollConnection.onPreScroll(
+            available = Offset(x = 0f, y = -1f),
+            source = NestedScrollSource.Drag
+        )
+
+        assertThat(isStarted).isEqualTo(true)
+
+        scrollConnection.onPreScroll(
+            available = Offset(x = 0f, y = 1f),
+            source = NestedScrollSource.Drag
+        )
+
+        assertThat(isStarted).isEqualTo(false)
+    }
+
+    companion object {
+        const val MIN_SCRIM_OFFSET = -100f
+        const val MAX_SCRIM_OFFSET = 0f
+
+        const val EXPANDED_CONTENT_HEIGHT = 200f
+        const val COLLAPSED_CONTENT_HEIGHT = 40f
+
+        const val MIN_VISIBLE_SCRIM_HEIGHT = 50f
+    }
+}
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 632196c..2af2602 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
@@ -21,6 +21,7 @@
 import android.media.AudioDeviceInfo
 import android.media.AudioDevicePort
 import android.media.AudioManager
+import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.R
@@ -54,6 +55,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 class AudioOutputInteractorTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt
new file mode 100644
index 0000000..9e86ced
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+
+import android.media.AudioAttributes
+import android.media.VolumeProvider
+import android.media.session.MediaController
+import android.media.session.PlaybackState
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.data.repository.FakeLocalMediaRepository
+import com.android.systemui.volume.localMediaController
+import com.android.systemui.volume.localMediaRepositoryFactory
+import com.android.systemui.volume.localPlaybackInfo
+import com.android.systemui.volume.localPlaybackStateBuilder
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaOutputInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
+import com.android.systemui.volume.panel.shared.model.Result
+import com.android.systemui.volume.remoteMediaController
+import com.android.systemui.volume.remotePlaybackInfo
+import com.android.systemui.volume.remotePlaybackStateBuilder
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class MediaOutputInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var underTest: MediaOutputInteractor
+
+    @Before
+    fun setUp() =
+        with(kosmos) {
+            localMediaRepositoryFactory.setLocalMediaRepository(
+                "local.test.pkg",
+                FakeLocalMediaRepository().apply {
+                    updateCurrentConnectedDevice(
+                        mock { whenever(name).thenReturn("local_media_device") }
+                    )
+                },
+            )
+            localMediaRepositoryFactory.setLocalMediaRepository(
+                "remote.test.pkg",
+                FakeLocalMediaRepository().apply {
+                    updateCurrentConnectedDevice(
+                        mock { whenever(name).thenReturn("remote_media_device") }
+                    )
+                },
+            )
+
+            underTest = kosmos.mediaOutputInteractor
+        }
+
+    @Test
+    fun noActiveMediaDeviceSessions_nulls() =
+        with(kosmos) {
+            testScope.runTest {
+                mediaControllerRepository.setActiveSessions(emptyList())
+
+                val activeMediaDeviceSessions by
+                    collectLastValue(underTest.activeMediaDeviceSessions)
+                runCurrent()
+
+                assertThat(activeMediaDeviceSessions!!.local).isNull()
+                assertThat(activeMediaDeviceSessions!!.remote).isNull()
+            }
+        }
+
+    @Test
+    fun activeMediaDeviceSessions_areParsed() =
+        with(kosmos) {
+            testScope.runTest {
+                mediaControllerRepository.setActiveSessions(
+                    listOf(localMediaController, remoteMediaController)
+                )
+
+                val activeMediaDeviceSessions by
+                    collectLastValue(underTest.activeMediaDeviceSessions)
+                runCurrent()
+
+                with(activeMediaDeviceSessions!!.local!!) {
+                    assertThat(packageName).isEqualTo("local.test.pkg")
+                    assertThat(appLabel).isEqualTo("local_media_controller_label")
+                    assertThat(canAdjustVolume).isTrue()
+                }
+                with(activeMediaDeviceSessions!!.remote!!) {
+                    assertThat(packageName).isEqualTo("remote.test.pkg")
+                    assertThat(appLabel).isEqualTo("remote_media_controller_label")
+                    assertThat(canAdjustVolume).isTrue()
+                }
+            }
+        }
+
+    @Test
+    fun activeMediaDeviceSessions_volumeControlFixed_cantAdjustVolume() =
+        with(kosmos) {
+            testScope.runTest {
+                localPlaybackInfo =
+                    MediaController.PlaybackInfo(
+                        MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                        VolumeProvider.VOLUME_CONTROL_FIXED,
+                        0,
+                        0,
+                        AudioAttributes.Builder().build(),
+                        "",
+                    )
+                remotePlaybackInfo =
+                    MediaController.PlaybackInfo(
+                        MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                        VolumeProvider.VOLUME_CONTROL_FIXED,
+                        0,
+                        0,
+                        AudioAttributes.Builder().build(),
+                        "",
+                    )
+                mediaControllerRepository.setActiveSessions(
+                    listOf(localMediaController, remoteMediaController)
+                )
+
+                val activeMediaDeviceSessions by
+                    collectLastValue(underTest.activeMediaDeviceSessions)
+                runCurrent()
+
+                assertThat(activeMediaDeviceSessions!!.local!!.canAdjustVolume).isFalse()
+                assertThat(activeMediaDeviceSessions!!.remote!!.canAdjustVolume).isFalse()
+            }
+        }
+
+    @Test
+    fun activeLocalAndRemoteSession_defaultSession_local() =
+        with(kosmos) {
+            testScope.runTest {
+                localPlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f)
+                remotePlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f)
+                mediaControllerRepository.setActiveSessions(
+                    listOf(localMediaController, remoteMediaController)
+                )
+
+                val defaultActiveMediaSession by
+                    collectLastValue(underTest.defaultActiveMediaSession)
+                val currentDevice by collectLastValue(underTest.currentConnectedDevice)
+                runCurrent()
+
+                with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) {
+                    assertThat(packageName).isEqualTo("local.test.pkg")
+                    assertThat(appLabel).isEqualTo("local_media_controller_label")
+                    assertThat(canAdjustVolume).isTrue()
+                }
+                assertThat(currentDevice!!.name).isEqualTo("local_media_device")
+            }
+        }
+
+    @Test
+    fun activeRemoteSession_defaultSession_remote() =
+        with(kosmos) {
+            testScope.runTest {
+                localPlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f)
+                remotePlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f)
+                mediaControllerRepository.setActiveSessions(
+                    listOf(localMediaController, remoteMediaController)
+                )
+
+                val defaultActiveMediaSession by
+                    collectLastValue(underTest.defaultActiveMediaSession)
+                val currentDevice by collectLastValue(underTest.currentConnectedDevice)
+                runCurrent()
+
+                with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) {
+                    assertThat(packageName).isEqualTo("remote.test.pkg")
+                    assertThat(appLabel).isEqualTo("remote_media_controller_label")
+                    assertThat(canAdjustVolume).isTrue()
+                }
+                assertThat(currentDevice!!.name).isEqualTo("remote_media_device")
+            }
+        }
+
+    @Test
+    fun inactiveLocalAndRemoteSession_defaultSession_local() =
+        with(kosmos) {
+            testScope.runTest {
+                localPlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f)
+                remotePlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f)
+                mediaControllerRepository.setActiveSessions(
+                    listOf(localMediaController, remoteMediaController)
+                )
+
+                val defaultActiveMediaSession by
+                    collectLastValue(underTest.defaultActiveMediaSession)
+                val currentDevice by collectLastValue(underTest.currentConnectedDevice)
+                runCurrent()
+
+                with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) {
+                    assertThat(packageName).isEqualTo("local.test.pkg")
+                    assertThat(appLabel).isEqualTo("local_media_controller_label")
+                    assertThat(canAdjustVolume).isTrue()
+                }
+                assertThat(currentDevice!!.name).isEqualTo("local_media_device")
+            }
+        }
+}
diff --git a/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml b/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml
index a5b44e5..0a1f2a8 100644
--- a/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml
+++ b/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml
@@ -16,6 +16,6 @@
 
 <shape xmlns:android = "http://schemas.android.com/apk/res/android">
     <size
-        android:width = "@dimen/overlay_action_chip_margin_start"
+        android:width = "@dimen/shelf_action_chip_margin_start"
         android:height = "0dp"/>
 </shape>
diff --git a/packages/SystemUI/res/layout/clipboard_overlay2.xml b/packages/SystemUI/res/layout/clipboard_overlay2.xml
new file mode 100644
index 0000000..33ad2cd
--- /dev/null
+++ b/packages/SystemUI/res/layout/clipboard_overlay2.xml
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<com.android.systemui.clipboardoverlay.ClipboardOverlayView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/clipboard_ui"
+    android:theme="@style/FloatingOverlay"
+    android:alpha="0"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:contentDescription="@string/clipboard_overlay_window_name">
+    <FrameLayout
+        android:id="@+id/actions_container_background"
+        android:visibility="gone"
+        android:layout_height="0dp"
+        android:layout_width="0dp"
+        android:elevation="4dp"
+        android:background="@drawable/shelf_action_chip_container_background"
+        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/actions_container"
+        app:layout_constraintEnd_toEndOf="@+id/actions_container"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+    <HorizontalScrollView
+        android:id="@+id/actions_container"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
+        android:paddingEnd="@dimen/overlay_action_container_padding_end"
+        android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+        android:elevation="4dp"
+        android:scrollbars="none"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintWidth_percent="1.0"
+        app:layout_constraintWidth_max="wrap"
+        app:layout_constraintStart_toEndOf="@+id/preview_border"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
+        <LinearLayout
+            android:id="@+id/actions"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingStart="@dimen/shelf_action_chip_margin_start"
+            android:showDividers="middle"
+            android:divider="@drawable/shelf_action_chip_divider"
+            android:animateLayoutChanges="true">
+            <include layout="@layout/shelf_action_chip"
+                     android:id="@+id/share_chip"/>
+            <include layout="@layout/shelf_action_chip"
+                     android:id="@+id/remote_copy_chip"/>
+        </LinearLayout>
+    </HorizontalScrollView>
+    <View
+        android:id="@+id/preview_border"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginStart="@dimen/overlay_preview_container_margin"
+        android:layout_marginTop="@dimen/overlay_border_width_neg"
+        android:layout_marginEnd="@dimen/overlay_border_width_neg"
+        android:layout_marginBottom="@dimen/overlay_preview_container_margin"
+        android:elevation="7dp"
+        android:background="@drawable/overlay_border"
+        app:layout_constraintStart_toStartOf="@id/actions_container_background"
+        app:layout_constraintTop_toTopOf="@id/clipboard_preview"
+        app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
+        app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/>
+    <FrameLayout
+        android:id="@+id/clipboard_preview"
+        android:layout_width="@dimen/clipboard_preview_size"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/overlay_border_width"
+        android:layout_marginBottom="@dimen/overlay_border_width"
+        android:layout_gravity="center"
+        android:elevation="7dp"
+        android:background="@drawable/overlay_preview_background"
+        android:clipChildren="true"
+        android:clipToOutline="true"
+        android:clipToPadding="true"
+        app:layout_constraintStart_toStartOf="@id/preview_border"
+        app:layout_constraintBottom_toBottomOf="@id/preview_border">
+        <TextView android:id="@+id/text_preview"
+                  android:textFontWeight="500"
+                  android:padding="8dp"
+                  android:gravity="center|start"
+                  android:ellipsize="end"
+                  android:autoSizeTextType="uniform"
+                  android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font"
+                  android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font"
+                  android:textColor="?attr/overlayButtonTextColor"
+                  android:textColorLink="?attr/overlayButtonTextColor"
+                  android:background="?androidprv:attr/colorAccentSecondary"
+                  android:layout_width="@dimen/clipboard_preview_size"
+                  android:layout_height="@dimen/clipboard_preview_size"/>
+        <ImageView
+            android:id="@+id/image_preview"
+            android:scaleType="fitCenter"
+            android:adjustViewBounds="true"
+            android:contentDescription="@string/clipboard_image_preview"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+        <TextView
+            android:id="@+id/hidden_preview"
+            android:visibility="gone"
+            android:textFontWeight="500"
+            android:padding="8dp"
+            android:gravity="center"
+            android:textSize="14sp"
+            android:textColor="?attr/overlayButtonTextColor"
+            android:background="?androidprv:attr/colorAccentSecondary"
+            android:layout_width="@dimen/clipboard_preview_size"
+            android:layout_height="@dimen/clipboard_preview_size"/>
+    </FrameLayout>
+    <LinearLayout
+        android:id="@+id/minimized_preview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:elevation="7dp"
+        android:padding="8dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+        android:background="@drawable/clipboard_minimized_background">
+        <ImageView
+            android:src="@drawable/ic_content_paste"
+            android:tint="?attr/overlayButtonTextColor"
+            android:layout_width="24dp"
+            android:layout_height="24dp"/>
+        <ImageView
+            android:src="@*android:drawable/ic_chevron_end"
+            android:tint="?attr/overlayButtonTextColor"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:paddingEnd="-8dp"
+            android:paddingStart="-4dp"/>
+    </LinearLayout>
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/clipboard_content_top"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:barrierDirection="top"
+        app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/clipboard_content_end"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:barrierDirection="end"
+        app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
+    <FrameLayout
+        android:id="@+id/dismiss_button"
+        android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+        android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+        android:elevation="10dp"
+        android:visibility="gone"
+        android:alpha="0"
+        app:layout_constraintStart_toEndOf="@id/clipboard_content_end"
+        app:layout_constraintEnd_toEndOf="@id/clipboard_content_end"
+        app:layout_constraintTop_toTopOf="@id/clipboard_content_top"
+        app:layout_constraintBottom_toTopOf="@id/clipboard_content_top"
+        android:contentDescription="@string/clipboard_dismiss_description">
+        <ImageView
+            android:id="@+id/dismiss_image"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_margin="@dimen/overlay_dismiss_button_margin"
+            android:background="@drawable/circular_background"
+            android:backgroundTint="?androidprv:attr/materialColorPrimaryFixedDim"
+            android:tint="?androidprv:attr/materialColorOnPrimaryFixed"
+            android:padding="4dp"
+            android:src="@drawable/ic_close"/>
+    </FrameLayout>
+</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 6bfd088..2ba72e3 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -635,9 +635,13 @@
         58.0001 29.2229,56.9551 26.8945,55.195
     </string>
 
-    <!-- The time (in ms) needed to trigger the lock icon view's long-press affordance -->
+    <!-- The time (in ms) needed to trigger the device entry icon view's long-press affordance -->
     <integer name="config_lockIconLongPress" translatable="false">200</integer>
 
+    <!-- The time (in ms) needed to trigger the device entry icon view's long-press affordance
+         when the device supports an under-display fingerprint sensor -->
+    <integer name="config_udfpsDeviceEntryIconLongPress" translatable="false">100</integer>
+
     <!-- package name of a built-in camera app to use to restrict implicit intent resolution
          when the double-press power gesture is used. Ignored if empty. -->
     <string translatable="false" name="config_cameraGesturePackage"></string>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a7a6d5b..a1daebd 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -430,6 +430,7 @@
     <dimen name="overlay_button_corner_radius">16dp</dimen>
     <!-- Margin between successive chips -->
     <dimen name="overlay_action_chip_margin_start">8dp</dimen>
+    <dimen name="shelf_action_chip_margin_start">12dp</dimen>
     <dimen name="overlay_action_chip_padding_vertical">12dp</dimen>
     <dimen name="overlay_action_chip_icon_size">24sp</dimen>
     <!-- Padding on each side of the icon for icon-only chips -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
index c613afb..473719fa 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
@@ -141,6 +141,7 @@
         private static final int ON_TASK_DESCRIPTION_CHANGED = 21;
         private static final int ON_ACTIVITY_ROTATION = 22;
         private static final int ON_LOCK_TASK_MODE_CHANGED = 23;
+        private static final int ON_TASK_SNAPSHOT_INVALIDATED = 24;
 
         /**
          * List of {@link TaskStackChangeListener} registered from {@link #addListener}.
@@ -272,6 +273,12 @@
         }
 
         @Override
+        public void onTaskSnapshotInvalidated(int taskId) {
+            mHandler.obtainMessage(ON_TASK_SNAPSHOT_INVALIDATED, taskId, 0 /* unused */)
+                    .sendToTarget();
+        }
+
+        @Override
         public void onTaskCreated(int taskId, ComponentName componentName) {
             mHandler.obtainMessage(ON_TASK_CREATED, taskId, 0, componentName).sendToTarget();
         }
@@ -496,6 +503,15 @@
                         }
                         break;
                     }
+                    case ON_TASK_SNAPSHOT_INVALIDATED: {
+                        Trace.beginSection("onTaskSnapshotInvalidated");
+                        final ThumbnailData thumbnail = new ThumbnailData();
+                        for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
+                            mTaskStackListeners.get(i).onTaskSnapshotChanged(msg.arg1, thumbnail);
+                        }
+                        Trace.endSection();
+                        break;
+                    }
                 }
             }
             if (msg.obj instanceof SomeArgs) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 91fb688..87a90b5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -35,12 +35,14 @@
 
 import android.app.ActivityManager;
 import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
 import android.content.Intent;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.media.AudioManager;
 import android.metrics.LogMaker;
+import android.os.Looper;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.telephony.TelephonyManager;
@@ -96,12 +98,15 @@
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.GlobalSettings;
 
+import com.google.common.util.concurrent.ListenableFuture;
+
 import dagger.Lazy;
 
 import kotlinx.coroutines.Job;
 
 import java.io.File;
 import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -134,6 +139,7 @@
     private final BouncerMessageInteractor mBouncerMessageInteractor;
     private int mTranslationY;
     private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private final DevicePolicyManager mDevicePolicyManager;
     // Whether the volume keys should be handled by keyguard. If true, then
     // they will be handled here for specific media types such as music, otherwise
     // the audio service will bring up the volume dialog.
@@ -460,6 +466,7 @@
             SelectedUserInteractor selectedUserInteractor,
             DeviceProvisionedController deviceProvisionedController,
             FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
+            DevicePolicyManager devicePolicyManager,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
             Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor,
             Provider<DeviceEntryInteractor> deviceEntryInteractor
@@ -495,6 +502,7 @@
         mKeyguardTransitionInteractor = keyguardTransitionInteractor;
         mDeviceProvisionedController = deviceProvisionedController;
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
+        mDevicePolicyManager = devicePolicyManager;
     }
 
     @Override
@@ -1105,35 +1113,36 @@
 
         if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts);
 
-        final DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager();
         final int failedAttemptsBeforeWipe =
-                dpm.getMaximumFailedPasswordsForWipe(null, userId);
+                mDevicePolicyManager.getMaximumFailedPasswordsForWipe(null, userId);
 
         final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0
                 ? (failedAttemptsBeforeWipe - failedAttempts)
                 : Integer.MAX_VALUE; // because DPM returns 0 if no restriction
         if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) {
-            // The user has installed a DevicePolicyManager that requests a user/profile to be wiped
-            // N attempts. Once we get below the grace period, we post this dialog every time as a
-            // clear warning until the deletion fires.
-            // Check which profile has the strictest policy for failed password attempts
-            final int expiringUser = dpm.getProfileWithMinimumFailedPasswordsForWipe(userId);
-            int userType = USER_TYPE_PRIMARY;
-            if (expiringUser == userId) {
-                // TODO: http://b/23522538
-                if (expiringUser != UserHandle.USER_SYSTEM) {
-                    userType = USER_TYPE_SECONDARY_USER;
+            // The user has installed a DevicePolicyManager that requests a
+            // user/profile to be wiped N attempts. Once we get below the grace period,
+            // we post this dialog every time as a clear warning until the deletion
+            // fires. Check which profile has the strictest policy for failed password
+            // attempts.
+            final int expiringUser =
+                    mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(userId);
+            ListenableFuture<Integer> getMainUserIdFuture =
+                    mSelectedUserInteractor.getMainUserIdAsync();
+            getMainUserIdFuture.addListener(() -> {
+                Looper.prepare();
+                Integer mainUser;
+                try {
+                    mainUser = getMainUserIdFuture.get();
+                } catch (InterruptedException | ExecutionException e) {
+                    // Nothing we can, keep using the system user as the primary
+                    // user.
+                    mainUser = null;
                 }
-            } else if (expiringUser != UserHandle.USER_NULL) {
-                userType = USER_TYPE_WORK_PROFILE;
-            } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY
-            if (remainingBeforeWipe > 0) {
-                mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType);
-            } else {
-                // Too many attempts. The device will be wiped shortly.
-                Slog.i(TAG, "Too many unlock attempts; user " + expiringUser + " will be wiped!");
-                mView.showWipeDialog(failedAttempts, userType);
-            }
+                showMessageForFailedUnlockAttempt(
+                        userId, expiringUser, mainUser, remainingBeforeWipe, failedAttempts);
+                Looper.loop();
+            }, ThreadUtils.getBackgroundExecutor());
         }
         mLockPatternUtils.reportFailedPasswordAttempt(userId);
         if (timeoutMs > 0) {
@@ -1145,6 +1154,35 @@
         }
     }
 
+    @VisibleForTesting
+    void showMessageForFailedUnlockAttempt(int userId, int expiringUserId, Integer mainUserId,
+            int remainingBeforeWipe, int failedAttempts) {
+        int userType = USER_TYPE_PRIMARY;
+        if (expiringUserId == userId) {
+            int primaryUser = UserHandle.USER_SYSTEM;
+            if (Flags.headlessSingleUserFixes()) {
+                if (mainUserId != null) {
+                    primaryUser = mainUserId;
+                }
+            }
+            // TODO: http://b/23522538
+            if (expiringUserId != primaryUser) {
+                userType = USER_TYPE_SECONDARY_USER;
+            }
+        } else if (expiringUserId != UserHandle.USER_NULL) {
+            userType = USER_TYPE_WORK_PROFILE;
+        } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY
+        if (remainingBeforeWipe > 0) {
+            mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe,
+                    userType);
+        } else {
+            // Too many attempts. The device will be wiped shortly.
+            Slog.i(TAG, "Too many unlock attempts; user " + expiringUserId
+                    + " will be wiped!");
+            mView.showWipeDialog(failedAttempts, userType);
+        }
+    }
+
     private void getCurrentSecurityController(
             KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback) {
         mSecurityViewFlipperController
diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
index 57c1fd0..42896a4 100644
--- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
@@ -569,6 +569,11 @@
         return true;
     }
 
+    /** Finish the current expand motion without accounting for velocity. */
+    public void finishExpanding() {
+        finishExpanding(false, 0);
+    }
+
     /**
      * Finish the current expand motion
      * @param forceAbort whether the expansion should be forcefully aborted and returned to the old
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 5df7fc9..fcba425 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.authentication.domain.interactor
 
+import android.app.admin.flags.Flags
 import android.os.UserHandle
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockPatternView
@@ -288,9 +289,15 @@
     private suspend fun getWipeTarget(): WipeTarget {
         // Check which profile has the strictest policy for failed authentication attempts.
         val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe()
+        val primaryUser =
+            if (Flags.headlessSingleUserFixes()) {
+                selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM
+            } else {
+                UserHandle.USER_SYSTEM
+            }
         return when (userToBeWiped) {
             selectedUserInteractor.getSelectedUserId() ->
-                if (userToBeWiped == UserHandle.USER_SYSTEM) {
+                if (userToBeWiped == primaryUser) {
                     WipeTarget.WholeDevice
                 } else {
                     WipeTarget.User
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
index b269967..8efc66d 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -18,6 +18,8 @@
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
+import static com.android.systemui.Flags.screenshotShelfUi2;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -25,6 +27,7 @@
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.Nullable;
+import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.content.Context;
 import android.content.res.Resources;
@@ -36,6 +39,7 @@
 import android.graphics.drawable.Icon;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.util.MathUtils;
 import android.util.TypedValue;
 import android.view.DisplayCutout;
@@ -58,9 +62,15 @@
 import com.android.systemui.screenshot.DraggableConstraintLayout;
 import com.android.systemui.screenshot.FloatingWindowUtil;
 import com.android.systemui.screenshot.OverlayActionChip;
+import com.android.systemui.screenshot.ui.binder.ActionButtonViewBinder;
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance;
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel;
 
 import java.util.ArrayList;
 
+import kotlin.Unit;
+import kotlin.jvm.functions.Function0;
+
 /**
  * Handles the visual elements and animations for the clipboard overlay.
  */
@@ -85,7 +95,7 @@
 
     private final DisplayMetrics mDisplayMetrics;
     private final AccessibilityManager mAccessibilityManager;
-    private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
+    private final ArrayList<View> mActionChips = new ArrayList<>();
 
     private View mClipboardPreview;
     private ImageView mImagePreview;
@@ -93,11 +103,12 @@
     private TextView mHiddenPreview;
     private LinearLayout mMinimizedPreview;
     private View mPreviewBorder;
-    private OverlayActionChip mShareChip;
-    private OverlayActionChip mRemoteCopyChip;
+    private View mShareChip;
+    private View mRemoteCopyChip;
     private View mActionContainerBackground;
     private View mDismissButton;
     private LinearLayout mActionContainer;
+    private ClipboardOverlayCallbacks mClipboardCallbacks;
 
     public ClipboardOverlayView(Context context) {
         this(context, null);
@@ -128,17 +139,7 @@
         mRemoteCopyChip = requireViewById(R.id.remote_copy_chip);
         mDismissButton = requireViewById(R.id.dismiss_button);
 
-        mShareChip.setAlpha(1);
-        mRemoteCopyChip.setAlpha(1);
-        mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
-
-        mRemoteCopyChip.setIcon(
-                Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
-        mShareChip.setIcon(
-                Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
-
-        mRemoteCopyChip.setContentDescription(
-                mContext.getString(R.string.clipboard_send_nearby_description));
+        bindDefaultActionChips();
 
         mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
             int availableHeight = mTextPreview.getHeight()
@@ -149,15 +150,68 @@
         super.onFinishInflate();
     }
 
+    private void bindDefaultActionChips() {
+        if (screenshotShelfUi2()) {
+            ActionButtonViewBinder.INSTANCE.bind(mRemoteCopyChip,
+                    ActionButtonViewModel.Companion.withNextId(
+                            new ActionButtonAppearance(
+                                    Icon.createWithResource(mContext,
+                                            R.drawable.ic_baseline_devices_24).loadDrawable(
+                                            mContext),
+                                    null,
+                                    mContext.getString(R.string.clipboard_send_nearby_description)),
+                            new Function0<>() {
+                                @Override
+                                public Unit invoke() {
+                                    if (mClipboardCallbacks != null) {
+                                        mClipboardCallbacks.onRemoteCopyButtonTapped();
+                                    }
+                                    return null;
+                                }
+                            }));
+            ActionButtonViewBinder.INSTANCE.bind(mShareChip,
+                    ActionButtonViewModel.Companion.withNextId(
+                            new ActionButtonAppearance(
+                                    Icon.createWithResource(mContext,
+                                            R.drawable.ic_screenshot_share).loadDrawable(mContext),
+                                    null, mContext.getString(com.android.internal.R.string.share)),
+                            new Function0<>() {
+                                @Override
+                                public Unit invoke() {
+                                    if (mClipboardCallbacks != null) {
+                                        mClipboardCallbacks.onShareButtonTapped();
+                                    }
+                                    return null;
+                                }
+                            }));
+        } else {
+            mShareChip.setAlpha(1);
+            mRemoteCopyChip.setAlpha(1);
+
+            ((ImageView) mRemoteCopyChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon(
+                    Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24));
+            ((ImageView) mShareChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon(
+                    Icon.createWithResource(mContext, R.drawable.ic_screenshot_share));
+
+            mShareChip.setContentDescription(
+                    mContext.getString(com.android.internal.R.string.share));
+            mRemoteCopyChip.setContentDescription(
+                    mContext.getString(R.string.clipboard_send_nearby_description));
+        }
+    }
+
     @Override
     public void setCallbacks(SwipeDismissCallbacks callbacks) {
         super.setCallbacks(callbacks);
         ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks;
-        mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped());
+        if (!screenshotShelfUi2()) {
+            mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped());
+            mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
+        }
         mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
-        mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
         mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
         mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped());
+        mClipboardCallbacks = clipboardCallbacks;
     }
 
     void setEditAccessibilityAction(boolean editable) {
@@ -285,7 +339,7 @@
     }
 
     void resetActionChips() {
-        for (OverlayActionChip chip : mActionChips) {
+        for (View chip : mActionChips) {
             mActionContainer.removeView(chip);
         }
         mActionChips.clear();
@@ -437,7 +491,12 @@
 
     void setActionChip(RemoteAction action, Runnable onFinish) {
         mActionContainerBackground.setVisibility(View.VISIBLE);
-        OverlayActionChip chip = constructActionChip(action, onFinish);
+        View chip;
+        if (screenshotShelfUi2()) {
+            chip = constructShelfActionChip(action, onFinish);
+        } else {
+            chip = constructActionChip(action, onFinish);
+        }
         mActionContainer.addView(chip);
         mActionChips.add(chip);
     }
@@ -450,6 +509,27 @@
         v.setVisibility(View.VISIBLE);
     }
 
+    private View constructShelfActionChip(RemoteAction action, Runnable onFinish) {
+        View chip = LayoutInflater.from(mContext).inflate(
+                R.layout.shelf_action_chip, mActionContainer, false);
+        ActionButtonViewBinder.INSTANCE.bind(chip, ActionButtonViewModel.Companion.withNextId(
+                new ActionButtonAppearance(action.getIcon().loadDrawable(mContext),
+                        action.getTitle(), action.getTitle()), new Function0<>() {
+                    @Override
+                    public Unit invoke() {
+                        try {
+                            action.getActionIntent().send();
+                            onFinish.run();
+                        } catch (PendingIntent.CanceledException e) {
+                            Log.e(TAG, "Failed to send intent");
+                        }
+                        return null;
+                    }
+                }));
+
+        return chip;
+    }
+
     private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) {
         OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
                 R.layout.overlay_action_chip, mActionContainer, false);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
index ff9fba4..740a93e 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
@@ -18,6 +18,8 @@
 
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
+import static com.android.systemui.Flags.screenshotShelfUi2;
+
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import android.content.Context;
@@ -57,8 +59,13 @@
      */
     @Provides
     static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) {
-        return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
-                R.layout.clipboard_overlay, null);
+        if (screenshotShelfUi2()) {
+            return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
+                    R.layout.clipboard_overlay2, null);
+        } else {
+            return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
+                    R.layout.clipboard_overlay, null);
+        }
     }
 
     @Qualifier
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
index 0781451..85e2bdb 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
@@ -37,7 +37,7 @@
 class LongPressHandlingView(
     context: Context,
     attrs: AttributeSet?,
-    private val longPressDuration: () -> Long,
+    longPressDuration: () -> Long,
 ) :
     View(
         context,
@@ -89,6 +89,12 @@
         )
     }
 
+    var longPressDuration: () -> Long
+        get() = interactionHandler.longPressDuration
+        set(longPressDuration) {
+            interactionHandler.longPressDuration = longPressDuration
+        }
+
     fun setLongPressHandlingEnabled(isEnabled: Boolean) {
         interactionHandler.isLongPressHandlingEnabled = isEnabled
     }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
index a742e8d..d3fc610 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
@@ -34,7 +34,7 @@
     /** Callback reporting the a single tap gesture was detected at the given coordinates. */
     private val onSingleTapDetected: () -> Unit,
     /** Time for the touch to be considered a long-press in ms */
-    private val longPressDuration: () -> Long,
+    var longPressDuration: () -> Long,
 ) {
     sealed class MotionEventModel {
         object Other : MotionEventModel()
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
index ba45a51..30a56a2 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -46,7 +46,6 @@
 import com.android.systemui.keyguard.data.repository.FaceAuthTableLog
 import com.android.systemui.keyguard.data.repository.FaceDetectTableLog
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.keyguard.data.repository.TrustRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -64,6 +63,7 @@
 import com.google.errorprone.annotations.CompileTimeConstant
 import java.io.PrintWriter
 import java.util.Arrays
+import java.util.concurrent.Executor
 import java.util.stream.Collectors
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -150,12 +150,12 @@
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Background private val backgroundExecutor: Executor,
     private val sessionTracker: SessionTracker,
     private val uiEventsLogger: UiEventLogger,
     private val faceAuthLogger: FaceAuthenticationLogger,
     private val biometricSettingsRepository: BiometricSettingsRepository,
     private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
-    trustRepository: TrustRepository,
     private val keyguardRepository: KeyguardRepository,
     private val powerInteractor: PowerInteractor,
     private val keyguardInteractor: KeyguardInteractor,
@@ -235,7 +235,10 @@
         }
 
     init {
-        faceManager?.addLockoutResetCallback(faceLockoutResetCallback)
+        backgroundExecutor.execute {
+            faceManager?.addLockoutResetCallback(faceLockoutResetCallback)
+            faceAuthLogger.addLockoutResetCallbackDone()
+        }
         faceAcquiredInfoIgnoreList =
             Arrays.stream(
                     context.resources.getIntArray(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index 4a726ae..a49b3ae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -65,23 +65,43 @@
      * @param blueprintId
      * @return whether the transition has succeeded.
      */
+    fun applyBlueprint(index: Int): Boolean {
+        ArrayList(blueprintIdMap.values)[index]?.let {
+            applyBlueprint(it)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Emits the blueprint value to the collectors.
+     *
+     * @param blueprintId
+     * @return whether the transition has succeeded.
+     */
     fun applyBlueprint(blueprintId: String?): Boolean {
         val blueprint = blueprintIdMap[blueprintId]
-        if (blueprint == null) {
+        return if (blueprint != null) {
+            applyBlueprint(blueprint)
+            true
+        } else {
             Log.e(
                 TAG,
                 "Could not find blueprint with id: $blueprintId. " +
                     "Perhaps it was not added to KeyguardBlueprintModule?"
             )
-            return false
+            false
         }
+    }
 
+    /** Emits the blueprint value to the collectors. */
+    fun applyBlueprint(blueprint: KeyguardBlueprint?) {
         if (blueprint == this.blueprint.value) {
-            return true
+            refreshBlueprint()
+            return
         }
 
-        this.blueprint.value = blueprint
-        return true
+        blueprint?.let { this.blueprint.value = it }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index cf995fa..da4f85e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -82,17 +82,12 @@
     }
 
     /**
-     * Transitions to a blueprint, or refreshes it if already applied.
+     * Transitions to a blueprint.
      *
      * @param blueprintId
      * @return whether the transition has succeeded.
      */
-    fun transitionOrRefreshBlueprint(blueprintId: String): Boolean {
-        if (blueprintId == blueprint.value.id) {
-            refreshBlueprint()
-            return true
-        }
-
+    fun transitionToBlueprint(blueprintId: String): Boolean {
         return keyguardBlueprintRepository.applyBlueprint(blueprintId)
     }
 
@@ -102,7 +97,7 @@
      * @param blueprintId
      * @return whether the transition has succeeded.
      */
-    fun transitionToBlueprint(blueprintId: String): Boolean {
+    fun transitionToBlueprint(blueprintId: Int): Boolean {
         return keyguardBlueprintRepository.applyBlueprint(blueprintId)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
index 20b7b2a..82255a0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
@@ -31,6 +31,7 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
 
 /**
  * Distance over which the surface behind the keyguard is animated in during a Y-translation
@@ -102,8 +103,11 @@
      */
     private val isNotificationLaunchAnimationRunningOnKeyguard =
         notificationLaunchInteractor.isLaunchAnimationRunning
-            .sample(transitionInteractor.finishedKeyguardState)
-            .map { it != KeyguardState.GONE }
+            .sample(transitionInteractor.finishedKeyguardState, ::Pair)
+            .map { (animationRunning, finishedState) ->
+                animationRunning && finishedState != KeyguardState.GONE
+            }
+            .onStart { emit(false) }
 
     /**
      * Whether we're animating the surface, or a notification launch animation is running (which
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index db47bfb..4f00495 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -94,6 +95,24 @@
                         longPressHandlingView.setLongPressHandlingEnabled(isEnabled)
                     }
                 }
+                launch("$TAG#viewModel.isUdfpsSupported") {
+                    viewModel.isUdfpsSupported.collect { udfpsSupported ->
+                        longPressHandlingView.longPressDuration =
+                            if (udfpsSupported) {
+                                {
+                                    view.resources
+                                        .getInteger(R.integer.config_udfpsDeviceEntryIconLongPress)
+                                        .toLong()
+                                }
+                            } else {
+                                {
+                                    view.resources
+                                        .getInteger(R.integer.config_lockIconLongPress)
+                                        .toLong()
+                                }
+                            }
+                    }
+                }
                 launch("$TAG#viewModel.accessibilityDelegateHint") {
                     viewModel.accessibilityDelegateHint.collect { hint ->
                         view.accessibilityHintType = hint
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
index abd79ab..b9a79dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
@@ -118,6 +118,7 @@
             }
 
             override fun destroy() {
+                view.setOnApplyWindowInsetsListener(null)
                 disposableHandle.dispose()
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
index 5713a15..35b2598 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
@@ -40,10 +40,7 @@
     attrs: AttributeSet?,
     defStyleAttrs: Int = 0,
 ) : FrameLayout(context, attrs, defStyleAttrs) {
-    val longPressHandlingView: LongPressHandlingView =
-        LongPressHandlingView(context, attrs) {
-            context.resources.getInteger(R.integer.config_lockIconLongPress).toLong()
-        }
+    val longPressHandlingView: LongPressHandlingView = LongPressHandlingView(context, attrs)
     val iconView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_fg }
     val bgView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_bg }
     val aodFpDrawable: LottieDrawable = LottieDrawable()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
index 962cdf1..ce7ec0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
@@ -46,14 +46,15 @@
                 return
             }
 
-            when {
-                arg.isDigitsOnly() -> pw.println("Invalid argument! Use string ids.")
-                keyguardBlueprintInteractor.transitionOrRefreshBlueprint(arg) ->
-                    pw.println("Transition succeeded!")
-                else -> {
-                    pw.println("Invalid argument! To see available blueprint ids, run:")
-                    pw.println("$ adb shell cmd statusbar blueprint help")
-                }
+            if (
+                arg.isDigitsOnly() && keyguardBlueprintInteractor.transitionToBlueprint(arg.toInt())
+            ) {
+                pw.println("Transition succeeded!")
+            } else if (keyguardBlueprintInteractor.transitionToBlueprint(arg)) {
+                pw.println("Transition succeeded!")
+            } else {
+                pw.println("Invalid argument! To see available blueprint ids, run:")
+                pw.println("$ adb shell cmd statusbar blueprint help")
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index 45b8257..9146c60 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.ui.view.layout.sections
 
 import android.content.res.Resources
+import android.view.WindowInsets
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
@@ -25,15 +26,19 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.RIGHT
 import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE
+import com.android.systemui.animation.view.LaunchableImageView
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.KeyguardIndicationController
 import com.android.systemui.statusbar.VibratorHelper
+import dagger.Lazy
 import javax.inject.Inject
 
 class DefaultShortcutsSection
@@ -46,11 +51,29 @@
     private val falsingManager: FalsingManager,
     private val indicationController: KeyguardIndicationController,
     private val vibratorHelper: VibratorHelper,
+    private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
 ) : BaseShortcutSection() {
+
+    // Amount to increase the bottom margin by to avoid colliding with inset
+    private var safeInsetBottom = 0
+
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (KeyguardBottomAreaRefactor.isEnabled) {
             addLeftShortcut(constraintLayout)
             addRightShortcut(constraintLayout)
+
+            constraintLayout
+                .requireViewById<LaunchableImageView>(R.id.start_button)
+                .setOnApplyWindowInsetsListener { _, windowInsets ->
+                    val tempSafeInset = windowInsets?.displayCutout?.safeInsetBottom ?: 0
+                    if (safeInsetBottom != tempSafeInset) {
+                        safeInsetBottom = tempSafeInset
+                        keyguardBlueprintInteractor
+                            .get()
+                            .refreshBlueprint(IntraBlueprintTransition.Type.DefaultTransition)
+                    }
+                    WindowInsets.CONSUMED
+                }
         }
     }
 
@@ -91,12 +114,24 @@
             constrainWidth(R.id.start_button, width)
             constrainHeight(R.id.start_button, height)
             connect(R.id.start_button, LEFT, PARENT_ID, LEFT, horizontalOffsetMargin)
-            connect(R.id.start_button, BOTTOM, PARENT_ID, BOTTOM, verticalOffsetMargin)
+            connect(
+                R.id.start_button,
+                BOTTOM,
+                PARENT_ID,
+                BOTTOM,
+                verticalOffsetMargin + safeInsetBottom
+            )
 
             constrainWidth(R.id.end_button, width)
             constrainHeight(R.id.end_button, height)
             connect(R.id.end_button, RIGHT, PARENT_ID, RIGHT, horizontalOffsetMargin)
-            connect(R.id.end_button, BOTTOM, PARENT_ID, BOTTOM, verticalOffsetMargin)
+            connect(
+                R.id.end_button,
+                BOTTOM,
+                PARENT_ID,
+                BOTTOM,
+                verticalOffsetMargin + safeInsetBottom
+            )
 
             // The constraint set visibility for start and end button are default visible, set to
             // ignore so the view's own initial visibility (invisible) is used
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 2c1e75e..d8b5013 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
@@ -100,7 +100,13 @@
 
                 // 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 quickSettingsIfSingleShade,
+                swipeDownFromTop(pointerCount = 2) to
+                    // TODO(b/338577208): Remove 'Dual' once we add Dual Shade invocation zones.
+                    if (shadeMode is ShadeMode.Dual) {
+                        Scenes.QuickSettingsShade
+                    } else {
+                        quickSettingsIfSingleShade
+                    },
 
                 // Swiping down, not from the edge, always navigates to the shade scene.
                 swipeDown(pointerCount = 1) to shadeSceneKey,
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
index 9e6c552..b276f53 100644
--- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -201,6 +201,10 @@
         )
     }
 
+    fun addLockoutResetCallbackDone() {
+        logBuffer.log(TAG, DEBUG, {}, { "addlockoutResetCallback done" })
+    }
+
     fun authRequested(uiEvent: FaceAuthUiEvent) {
         logBuffer.log(
             TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
index 0c07c05..89e4760 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
@@ -85,7 +85,10 @@
                     {
                         it.scene == Scenes.NotificationsShade || it.scene == Scenes.Shade
                     },
-                SYSUI_STATE_QUICK_SETTINGS_EXPANDED to { it.scene == Scenes.QuickSettings },
+                SYSUI_STATE_QUICK_SETTINGS_EXPANDED to
+                    {
+                        it.scene == Scenes.QuickSettingsShade || it.scene == Scenes.QuickSettings
+                    },
                 SYSUI_STATE_BOUNCER_SHOWING to { it.scene == Scenes.Bouncer },
                 SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
                     {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
index ba01776..f677ec1b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.shade.ui.viewmodel
+package com.android.systemui.notifications.ui.viewmodel
 
 import com.android.compose.animation.scene.Back
 import com.android.compose.animation.scene.SceneKey
@@ -23,6 +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.shade.ui.viewmodel.OverlayShadeViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
similarity index 89%
copy from packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt
copy to packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
index ba01776..d48d55d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.shade.ui.viewmodel
+package com.android.systemui.qs.ui.viewmodel
 
 import com.android.compose.animation.scene.Back
 import com.android.compose.animation.scene.SceneKey
@@ -23,6 +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.shade.ui.viewmodel.OverlayShadeViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
@@ -30,9 +31,9 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
-/** Models UI state and handles user input for the Notifications Shade scene. */
+/** Models UI state and handles user input for the Quick Settings Shade scene. */
 @SysUISingleton
-class NotificationsShadeSceneViewModel
+class QuickSettingsShadeSceneViewModel
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index aa8ecfc..28569d8 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -63,7 +63,8 @@
                 sceneKeys =
                     listOfNotNull(
                         Scenes.Gone,
-                        Scenes.QuickSettings,
+                        Scenes.QuickSettings.takeUnless { DualShade.isEnabled },
+                        Scenes.QuickSettingsShade.takeIf { DualShade.isEnabled },
                         Scenes.NotificationsShade.takeIf { DualShade.isEnabled },
                         Scenes.Shade.takeUnless { DualShade.isEnabled },
                     ),
@@ -73,7 +74,8 @@
                             Scenes.Gone to 0,
                             Scenes.NotificationsShade to 1.takeIf { DualShade.isEnabled },
                             Scenes.Shade to 1.takeUnless { DualShade.isEnabled },
-                            Scenes.QuickSettings to 2,
+                            Scenes.QuickSettingsShade to 2.takeIf { DualShade.isEnabled },
+                            Scenes.QuickSettings to 2.takeUnless { DualShade.isEnabled },
                         )
                         .filterValues { it != null }
                         .mapValues { checkNotNull(it.value) }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 551aa12..dbe0342 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -41,6 +41,7 @@
             LockscreenSceneModule::class,
             QuickSettingsSceneModule::class,
             ShadeSceneModule::class,
+            QuickSettingsShadeSceneModule::class,
             NotificationsShadeSceneModule::class,
         ],
 )
@@ -71,7 +72,8 @@
                         Scenes.Communal,
                         Scenes.Lockscreen,
                         Scenes.Bouncer,
-                        Scenes.QuickSettings,
+                        Scenes.QuickSettings.takeUnless { DualShade.isEnabled },
+                        Scenes.QuickSettingsShade.takeIf { DualShade.isEnabled },
                         Scenes.NotificationsShade.takeIf { DualShade.isEnabled },
                         Scenes.Shade.takeUnless { DualShade.isEnabled },
                     ),
@@ -83,7 +85,8 @@
                             Scenes.Communal to 1,
                             Scenes.NotificationsShade to 2.takeIf { DualShade.isEnabled },
                             Scenes.Shade to 2.takeUnless { DualShade.isEnabled },
-                            Scenes.QuickSettings to 3,
+                            Scenes.QuickSettingsShade to 3.takeIf { DualShade.isEnabled },
+                            Scenes.QuickSettings to 3.takeUnless { DualShade.isEnabled },
                             Scenes.Bouncer to 4,
                         )
                         .filterValues { it != null }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
index ace4491..6bcd923 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
@@ -127,6 +127,7 @@
                 Scenes.Lockscreen -> true
                 Scenes.NotificationsShade -> false
                 Scenes.QuickSettings -> false
+                Scenes.QuickSettingsShade -> false
                 Scenes.Shade -> false
                 else -> error("SceneKey \"$this\" doesn't have a mapping for canBeOccluded!")
             }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
index de3b87a..9c2b992 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
@@ -78,13 +78,16 @@
                         is ObservableTransitionState.Idle ->
                             state.currentScene == Scenes.Shade ||
                                 state.currentScene == Scenes.NotificationsShade ||
+                                state.currentScene == Scenes.QuickSettingsShade ||
                                 state.currentScene == Scenes.Lockscreen
                         is ObservableTransitionState.Transition ->
                             state.toScene == Scenes.Shade ||
                                 state.toScene == Scenes.NotificationsShade ||
+                                state.toScene == Scenes.QuickSettingsShade ||
                                 state.toScene == Scenes.Lockscreen ||
                                 state.fromScene == Scenes.Shade ||
                                 state.fromScene == Scenes.NotificationsShade ||
+                                state.fromScene == Scenes.QuickSettingsShade ||
                                 state.fromScene == Scenes.Lockscreen
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
index 08f1be9..6d139da 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
@@ -47,19 +47,45 @@
      * overlay UI.
      *
      * It's used only in the dual shade configuration, where there are two separate shades: one for
-     * notifications (this scene) and another for quick settings (where a separate scene is used).
+     * notifications (this scene) and another for [QuickSettingsShade].
      *
      * It's not used in the single/accordion configuration (swipe down once to reveal the shade,
-     * swipe down again the to expand quick settings) and for the "split" shade configuration (on
+     * swipe down again the to expand quick settings) or in the "split" shade configuration (on
      * large screens or unfolded foldables, where notifications and quick settings are shown
      * side-by-side in their own columns).
      */
     @JvmField val NotificationsShade = SceneKey("notifications_shade")
 
-    /** The quick settings scene shows the quick setting tiles. */
+    /**
+     * The quick settings scene shows the quick setting tiles.
+     *
+     * This scene is used for single/accordion configuration (swipe down once to reveal the shade,
+     * swipe down again the to expand quick settings).
+     *
+     * For the "split" shade configuration (on large screens or unfolded foldables, where
+     * notifications and quick settings are shown side-by-side in their own columns), the [Shade]
+     * scene is used].
+     *
+     * For the dual shade configuration, where there are two separate shades: one for notifications
+     * and one for quick settings, [NotificationsShade] and [QuickSettingsShade] scenes are used
+     * respectively.
+     */
     @JvmField val QuickSettings = SceneKey("quick_settings")
 
     /**
+     * The quick settings shade scene shows the quick setting tiles as an overlay UI.
+     *
+     * It's used only in the dual shade configuration, where there are two separate shades: one for
+     * quick settings (this scene) and another for [NotificationsShade].
+     *
+     * It's not used in the single/accordion configuration (swipe down once to reveal the shade,
+     * swipe down again the to expand quick settings) or in the "split" shade configuration (on
+     * large screens or unfolded foldables, where notifications and quick settings are shown
+     * side-by-side in their own columns).
+     */
+    @JvmField val QuickSettingsShade = SceneKey("quick_settings_shade")
+
+    /**
      * The shade is the scene that shows a scrollable list of notifications and the minimized
      * version of quick settings (AKA "quick quick settings" or "QQS").
      *
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 e4435cc..b0af7f9 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
@@ -60,6 +60,16 @@
                     )] = UserActionResult(Scenes.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)
+            }
+
             this[Swipe(direction = SwipeDirection.Down)] =
                 UserActionResult(
                     if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 7fbede47..09c80b0 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -127,6 +127,7 @@
                 Scenes.NotificationsShade -> Classifier.NOTIFICATION_DRAG_DOWN
                 Scenes.Shade -> Classifier.NOTIFICATION_DRAG_DOWN
                 Scenes.QuickSettings -> Classifier.QUICK_SETTINGS
+                Scenes.QuickSettingsShade -> Classifier.QUICK_SETTINGS
                 else -> null
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt
new file mode 100644
index 0000000..0bc280c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.screenshot.ui
+
+import android.animation.ValueAnimator
+import android.content.res.ColorStateList
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.drawable.Drawable
+import androidx.core.animation.doOnEnd
+import java.util.Objects
+
+/**  */
+class TransitioningIconDrawable : Drawable() {
+    // The drawable for the current icon of this view. During icon transitions, this is the one
+    // being animated out.
+    private var drawable: Drawable? = null
+
+    // The incoming new icon. Only populated during transition animations (when drawable is also
+    // non-null).
+    private var enteringDrawable: Drawable? = null
+    private var colorFilter: ColorFilter? = null
+    private var tint: ColorStateList? = null
+    private var alpha = 255
+
+    private var transitionAnimator =
+        ValueAnimator.ofFloat(0f, 1f).also { it.doOnEnd { onTransitionComplete() } }
+
+    /**
+     * Set the drawable to be displayed, potentially animating the transition from one icon to the
+     * next.
+     */
+    fun setIcon(incomingDrawable: Drawable?) {
+        if (Objects.equals(drawable, incomingDrawable) && !transitionAnimator.isRunning) {
+            return
+        }
+
+        incomingDrawable?.colorFilter = colorFilter
+        incomingDrawable?.setTintList(tint)
+
+        if (drawable == null) {
+            // No existing icon drawn, just show the new one without a transition
+            drawable = incomingDrawable
+            invalidateSelf()
+            return
+        }
+
+        if (enteringDrawable != null) {
+            // There's already an entrance animation happening, just update the entering icon, not
+            // maintaining a queue or anything.
+            enteringDrawable = incomingDrawable
+            return
+        }
+
+        // There was already an icon, need to animate between icons.
+        enteringDrawable = incomingDrawable
+        transitionAnimator.setCurrentFraction(0f)
+        transitionAnimator.start()
+        invalidateSelf()
+    }
+
+    override fun draw(canvas: Canvas) {
+        // Scale the old one down, scale the new one up.
+        drawable?.let {
+            val scale =
+                if (transitionAnimator.isRunning) {
+                    1f - transitionAnimator.animatedFraction
+                } else {
+                    1f
+                }
+            drawScaledDrawable(it, canvas, scale)
+        }
+        enteringDrawable?.let {
+            val scale = transitionAnimator.animatedFraction
+            drawScaledDrawable(it, canvas, scale)
+        }
+
+        if (transitionAnimator.isRunning) {
+            invalidateSelf()
+        }
+    }
+
+    private fun drawScaledDrawable(drawable: Drawable, canvas: Canvas, scale: Float) {
+        drawable.bounds = getBounds()
+        canvas.save()
+        canvas.scale(
+            scale,
+            scale,
+            (drawable.intrinsicWidth / 2).toFloat(),
+            (drawable.intrinsicHeight / 2).toFloat()
+        )
+        drawable.draw(canvas)
+        canvas.restore()
+    }
+
+    private fun onTransitionComplete() {
+        drawable = enteringDrawable
+        enteringDrawable = null
+        invalidateSelf()
+    }
+
+    override fun setTintList(tint: ColorStateList?) {
+        super.setTintList(tint)
+        drawable?.setTintList(tint)
+        enteringDrawable?.setTintList(tint)
+        this.tint = tint
+    }
+
+    override fun setAlpha(alpha: Int) {
+        this.alpha = alpha
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        this.colorFilter = colorFilter
+        drawable?.colorFilter = colorFilter
+        enteringDrawable?.colorFilter = colorFilter
+    }
+
+    override fun getOpacity(): Int = alpha
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
index 3c5a0ec..750bd53 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
@@ -21,6 +21,7 @@
 import android.widget.LinearLayout
 import android.widget.TextView
 import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.TransitioningIconDrawable
 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
 
 object ActionButtonViewBinder {
@@ -28,7 +29,13 @@
     fun bind(view: View, viewModel: ActionButtonViewModel) {
         val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon)
         val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text)
-        iconView.setImageDrawable(viewModel.appearance.icon)
+        if (iconView.drawable == null) {
+            iconView.setImageDrawable(TransitioningIconDrawable())
+        }
+        val drawable = iconView.drawable as? TransitioningIconDrawable
+        // Note we never re-bind a view to a different ActionButtonViewModel, different view
+        // models would remove/create separate views.
+        drawable?.setIcon(viewModel.appearance.icon)
         textView.text = viewModel.appearance.label
         setMargins(iconView, textView, viewModel.appearance.label?.isNotEmpty() ?: false)
         if (viewModel.onClicked != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 4a636d2..3eb4389 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -412,9 +412,9 @@
         }
 
         if (state.bouncerShowing) {
-            mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+            mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
         } else {
-            mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+            mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 5cc30bd..43e782c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -203,7 +203,11 @@
     }
 
     override fun expandToQs() {
-        sceneInteractor.changeScene(Scenes.QuickSettings, "ShadeController.animateExpandQs")
+        val shadeMode = shadeInteractor.shadeMode.value
+        sceneInteractor.changeScene(
+            if (shadeMode is ShadeMode.Dual) Scenes.QuickSettingsShade else Scenes.QuickSettings,
+            "ShadeController.animateExpandQs"
+        )
     }
 
     override fun setVisibilityListener(listener: ShadeVisibilityListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 96a50f7..70632d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -679,6 +679,7 @@
             Scenes.Shade, StatusBarState.SHADE_LOCKED,
             Scenes.NotificationsShade, StatusBarState.SHADE_LOCKED,
             Scenes.QuickSettings, StatusBarState.SHADE_LOCKED,
+            Scenes.QuickSettingsShade, StatusBarState.SHADE_LOCKED,
             Scenes.Gone, StatusBarState.SHADE
     );
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 3bd8735..d669369 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1185,6 +1185,11 @@
     }
 
     @Override
+    public void setCurrentGestureOverscrollConsumer(@Nullable Consumer<Boolean> consumer) {
+        mScrollViewFields.setCurrentGestureOverscrollConsumer(consumer);
+    }
+
+    @Override
     public void setStackHeightConsumer(@Nullable Consumer<Float> consumer) {
         mScrollViewFields.setStackHeightConsumer(consumer);
     }
@@ -3403,6 +3408,8 @@
             boolean isUpOrCancel = action == ACTION_UP || action == ACTION_CANCEL;
             if (mSendingTouchesToSceneFramework) {
                 mController.sendTouchToSceneFramework(ev);
+                mScrollViewFields.sendCurrentGestureOverscroll(
+                        getExpandedInThisMotion() && !isUpOrCancel);
             } else if (!isUpOrCancel) {
                 // if this is the first touch being sent to the scene framework,
                 // convert it into a synthetic DOWN event.
@@ -3410,6 +3417,7 @@
                 MotionEvent downEvent = MotionEvent.obtain(ev);
                 downEvent.setAction(MotionEvent.ACTION_DOWN);
                 mController.sendTouchToSceneFramework(downEvent);
+                mScrollViewFields.sendCurrentGestureOverscroll(getExpandedInThisMotion());
                 downEvent.recycle();
             }
 
@@ -3428,6 +3436,14 @@
         downEvent.recycle();
     }
 
+    // Only when scene container is enabled, mark that we are being dragged so that we start
+    // dispatching the rest of the gesture to scene container.
+    void startOverscrollAfterExpanding() {
+        SceneContainerFlag.isUnexpectedlyInLegacyMode();
+        getExpandHelper().finishExpanding();
+        setIsBeingDragged(true);
+    }
+
     @Override
     public boolean onGenericMotionEvent(MotionEvent event) {
         if (!isScrollingEnabled()
@@ -5545,6 +5561,11 @@
         return mExpandingNotification;
     }
 
+    @VisibleForTesting
+    void setExpandingNotification(boolean isExpanding) {
+        mExpandingNotification = isExpanding;
+    }
+
     boolean getDisallowScrollingInThisMotion() {
         return mDisallowScrollingInThisMotion;
     }
@@ -5557,6 +5578,11 @@
         return mExpandedInThisMotion;
     }
 
+    @VisibleForTesting
+    void setExpandedInThisMotion(boolean expandedInThisMotion) {
+        mExpandedInThisMotion = expandedInThisMotion;
+    }
+
     boolean getDisallowDismissInThisMotion() {
         return mDisallowDismissInThisMotion;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 5bb3f42..3011bc2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -206,6 +206,7 @@
     private final SeenNotificationsInteractor mSeenNotificationsInteractor;
     private final KeyguardTransitionRepository mKeyguardTransitionRepo;
     private NotificationStackScrollLayout mView;
+    private TouchHandler mTouchHandler;
     private NotificationSwipeHelper mSwipeHelper;
     @Nullable
     private Boolean mHistoryEnabled;
@@ -807,7 +808,8 @@
         mView.setStackStateLogger(mStackStateLogger);
         mView.setController(this);
         mView.setLogger(mLogger);
-        mView.setTouchHandler(new TouchHandler());
+        mTouchHandler = new TouchHandler();
+        mView.setTouchHandler(mTouchHandler);
         mView.setResetUserExpandedStatesRunnable(mNotificationsController::resetUserExpandedStates);
         mView.setActivityStarter(mActivityStarter);
         mView.setClearAllAnimationListener(this::onAnimationEnd);
@@ -1793,6 +1795,11 @@
         }
     }
 
+    @VisibleForTesting
+    TouchHandler getTouchHandler() {
+        return mTouchHandler;
+    }
+
     @Override
     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("mMaxAlphaFromView=" + mMaxAlphaFromView);
@@ -2043,7 +2050,14 @@
                 expandingNotification = mView.isExpandingNotification();
                 if (mView.getExpandedInThisMotion() && !expandingNotification && wasExpandingBefore
                         && !mView.getDisallowScrollingInThisMotion()) {
-                    mView.dispatchDownEventToScroller(ev);
+                    // We need to dispatch the overscroll differently when Scene Container is on,
+                    // since NSSL no longer controls its own scroll.
+                    if (SceneContainerFlag.isEnabled() && !isCancelOrUp) {
+                        mView.startOverscrollAfterExpanding();
+                        return true;
+                    } else {
+                        mView.dispatchDownEventToScroller(ev);
+                    }
                 }
             }
             boolean horizontalSwipeWantsIt = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
index edac5ed..a3827c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -51,6 +51,11 @@
      */
     var syntheticScrollConsumer: Consumer<Float>? = null
     /**
+     * When a gesture is consumed internally by NSSL but needs to be handled by other elements (such
+     * as the notif scrim) as overscroll, we can notify the placeholder through here.
+     */
+    var currentGestureOverscrollConsumer: Consumer<Boolean>? = null
+    /**
      * Any time the stack height is recalculated, it should be updated here to be used by the
      * placeholder
      */
@@ -64,6 +69,9 @@
     /** send the [syntheticScroll] to the [syntheticScrollConsumer], if present. */
     fun sendSyntheticScroll(syntheticScroll: Float) =
         syntheticScrollConsumer?.accept(syntheticScroll)
+    /** send [isCurrentGestureOverscroll] to the [currentGestureOverscrollConsumer], if present. */
+    fun sendCurrentGestureOverscroll(isCurrentGestureOverscroll: Boolean) =
+        currentGestureOverscrollConsumer?.accept(isCurrentGestureOverscroll)
     /** send the [stackHeight] to the [stackHeightConsumer], if present. */
     fun sendStackHeight(stackHeight: Float) = stackHeightConsumer?.accept(stackHeight)
     /** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
index 8a9da69..920c9c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
@@ -43,4 +43,10 @@
      * necessary to scroll up to keep expanding the notification.
      */
     val syntheticScroll = MutableStateFlow(0f)
+
+    /**
+     * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
+     * consumed part of the gesture.
+     */
+    val isCurrentGestureOverscroll = MutableStateFlow(false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index b8660ba..b94da38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -105,6 +105,13 @@
      */
     val syntheticScroll: Flow<Float> = viewHeightRepository.syntheticScroll.asStateFlow()
 
+    /**
+     * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
+     * consumed part of the gesture.
+     */
+    val isCurrentGestureOverscroll: Flow<Boolean> =
+        viewHeightRepository.isCurrentGestureOverscroll.asStateFlow()
+
     /** Sets the alpha to apply to the NSSL for the brightness mirror */
     fun setAlphaForBrightnessMirror(alpha: Float) {
         placeholderRepository.alphaForBrightnessMirror.value = alpha
@@ -146,6 +153,11 @@
         viewHeightRepository.syntheticScroll.value = delta
     }
 
+    /** Sets whether the current touch gesture is overscroll. */
+    fun setCurrentGestureOverscroll(isOverscroll: Boolean) {
+        viewHeightRepository.isCurrentGestureOverscroll.value = isOverscroll
+    }
+
     fun setConstrainedAvailableSpace(height: Int) {
         placeholderRepository.constrainedAvailableSpace.value = height
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index a56384d..2c88845 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -51,6 +51,8 @@
 
     /** Set a consumer for synthetic scroll events */
     fun setSyntheticScrollConsumer(consumer: Consumer<Float>?)
+    /** Set a consumer for current gesture overscroll events */
+    fun setCurrentGestureOverscrollConsumer(consumer: Consumer<Boolean>?)
     /** Set a consumer for stack height changed events */
     fun setStackHeightConsumer(consumer: Consumer<Float>?)
     /** Set a consumer for heads up height changed events */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 4476d87..26f7ad7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -89,10 +89,12 @@
 
         launchAndDispose {
             view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
+            view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer)
             view.setStackHeightConsumer(viewModel.stackHeightConsumer)
             view.setHeadsUpHeightConsumer(viewModel.headsUpHeightConsumer)
             DisposableHandle {
                 view.setSyntheticScrollConsumer(null)
+                view.setCurrentGestureOverscrollConsumer(null)
                 view.setStackHeightConsumer(null)
                 view.setHeadsUpHeightConsumer(null)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 8b1b93bf..b2184db 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
@@ -145,6 +145,12 @@
 
     /** Receives the amount (px) that the stack should scroll due to internal expansion. */
     val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll
+    /**
+     * Receives whether the current touch gesture is overscroll as it has already been consumed by
+     * the stack.
+     */
+    val currentGestureOverscrollConsumer: (Boolean) -> Unit =
+        stackAppearanceInteractor::setCurrentGestureOverscroll
     /** Receives the height of the contents of the notification stack. */
     val stackHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setStackHeight
     /** Receives the height of the heads up notification. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 486e305..11eaf54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -111,6 +111,13 @@
     val syntheticScroll: Flow<Float> =
         interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll")
 
+    /**
+     * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
+     * consumed part of the gesture.
+     */
+    val isCurrentGestureOverscroll: Flow<Boolean> =
+        interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll")
+
     /** Sets whether the notification stack is scrolled to the top. */
     fun setScrolledToTop(scrolledToTop: Boolean) {
         interactor.setScrolledToTop(scrolledToTop)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 37be1c6..a817b31 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.user.data.repository
 
 import android.annotation.SuppressLint
+import android.annotation.UserIdInt
 import android.content.Context
 import android.content.pm.UserInfo
 import android.os.UserHandle
@@ -107,6 +108,22 @@
     fun isSimpleUserSwitcher(): Boolean
 
     fun isUserSwitcherEnabled(): Boolean
+
+    /**
+     * Returns the user ID of the "main user" of the device. This user may have access to certain
+     * features which are limited to at most one user. There will never be more than one main user
+     * on a device.
+     *
+     * <p>Currently, on most form factors the first human user on the device will be the main user;
+     * in the future, the concept may be transferable, so a different user (or even no user at all)
+     * may be designated the main user instead. On other form factors there might not be a main
+     * user.
+     *
+     * <p> When the device doesn't have a main user, this will return {@code null}.
+     *
+     * @see [UserManager.getMainUser]
+     */
+    @UserIdInt suspend fun getMainUserId(): Int?
 }
 
 @SysUISingleton
@@ -239,6 +256,10 @@
         return _userSwitcherSettings.value.isUserSwitcherEnabled
     }
 
+    override suspend fun getMainUserId(): Int? {
+        return withContext(backgroundDispatcher) { manager.mainUser?.identifier }
+    }
+
     private suspend fun getSettings(): UserSwitcherSettingsModel {
         return withContext(backgroundDispatcher) {
             val isSimpleUserSwitcher =
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
index 38b381a..a5728d0 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
@@ -2,17 +2,27 @@
 
 import android.annotation.UserIdInt
 import android.content.pm.UserInfo
+import android.os.UserManager
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.Flags.refactorGetCurrentUser
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.user.data.repository.UserRepository
+import com.google.common.util.concurrent.ListenableFuture
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.guava.future
 
 /** Encapsulates business logic to interact the selected user */
 @SysUISingleton
-class SelectedUserInteractor @Inject constructor(private val repository: UserRepository) {
+class SelectedUserInteractor
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    private val repository: UserRepository
+) {
 
     /** Flow providing the ID of the currently selected user. */
     val selectedUser = repository.selectedUserInfo.map { it.id }.distinctUntilChanged()
@@ -38,4 +48,41 @@
             KeyguardUpdateMonitor.getCurrentUser()
         }
     }
+
+    /**
+     * Returns the user ID of the "main user" of the device. This user may have access to certain
+     * features which are limited to at most one user. There will never be more than one main user
+     * on a device.
+     *
+     * <p>Currently, on most form factors the first human user on the device will be the main user;
+     * in the future, the concept may be transferable, so a different user (or even no user at all)
+     * may be designated the main user instead. On other form factors there might not be a main
+     * user.
+     *
+     * <p> When the device doesn't have a main user, this will return {@code null}.
+     *
+     * @see [UserManager.getMainUser]
+     */
+    @UserIdInt
+    suspend fun getMainUserId(): Int? {
+        return repository.getMainUserId()
+    }
+
+    /**
+     * Returns a [ListenableFuture] for the user ID of the "main user" of the device. This user may
+     * have access to certain features which are limited to at most one user. There will never be
+     * more than one main user on a device.
+     *
+     * <p>Currently, on most form factors the first human user on the device will be the main user;
+     * in the future, the concept may be transferable, so a different user (or even no user at all)
+     * may be designated the main user instead. On other form factors there might not be a main
+     * user.
+     *
+     * <p> When the device doesn't have a main user, this will return {@code null}.
+     *
+     * @see [UserManager.getMainUser]
+     */
+    fun getMainUserIdAsync(): ListenableFuture<Int?> {
+        return applicationScope.future { getMainUserId() }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
index 19d9c3f..3eec3d9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
@@ -32,7 +32,6 @@
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
-import com.android.systemui.volume.panel.shared.model.filterData
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
@@ -69,14 +68,9 @@
                         communicationDevice?.toAudioOutputDevice()
                     }
                 } else {
-                    mediaOutputInteractor.defaultActiveMediaSession
-                        .filterData()
-                        .flatMapLatest {
-                            localMediaRepositoryFactory
-                                .create(it?.packageName)
-                                .currentConnectedDevice
-                        }
-                        .map { mediaDevice -> mediaDevice?.toAudioOutputDevice() }
+                    mediaOutputInteractor.currentConnectedDevice.map { mediaDevice ->
+                        mediaDevice?.toAudioOutputDevice()
+                    }
                 }
             }
             .map { it ?: AudioOutputDevice.Unknown }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
index 22c0530..199bc3b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
@@ -33,15 +33,15 @@
     private val mediaOutputDialogManager: MediaOutputDialogManager,
 ) {
 
-    fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable) {
+    fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable?) {
         if (sessionWithPlaybackState?.isPlaybackActive == true) {
             mediaOutputDialogManager.createAndShowWithController(
                 sessionWithPlaybackState.session.packageName,
                 false,
-                expandable.dialogController()
+                expandable?.dialogController()
             )
         } else {
-            mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController())
+            mediaOutputDialogManager.createAndShowForSystemRouting(expandable?.dialogController())
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
index b974f90..b00829e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
@@ -19,10 +19,12 @@
 import android.content.pm.PackageManager
 import android.media.VolumeProvider
 import android.media.session.MediaController
+import android.os.Handler
 import android.util.Log
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import com.android.settingslib.volume.data.repository.MediaControllerRepository
+import com.android.settingslib.volume.data.repository.stateChanges
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions
@@ -36,14 +38,15 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
 
@@ -58,21 +61,31 @@
     @VolumePanelScope private val coroutineScope: CoroutineScope,
     @Background private val backgroundCoroutineContext: CoroutineContext,
     mediaControllerRepository: MediaControllerRepository,
+    @Background private val backgroundHandler: Handler,
 ) {
 
     private val activeMediaControllers: Flow<MediaControllers> =
         mediaControllerRepository.activeSessions
+            .flatMapLatest { activeSessions ->
+                activeSessions
+                    .map { activeSession -> activeSession.stateChanges() }
+                    .merge()
+                    .map { activeSessions }
+                    .onStart { emit(activeSessions) }
+            }
             .map { getMediaControllers(it) }
-            .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
+            .stateIn(coroutineScope, SharingStarted.Eagerly, MediaControllers(null, null))
 
     /** [MediaDeviceSessions] that contains currently active sessions. */
     val activeMediaDeviceSessions: Flow<MediaDeviceSessions> =
-        activeMediaControllers.map {
-            MediaDeviceSessions(
-                local = it.local?.mediaDeviceSession(),
-                remote = it.remote?.mediaDeviceSession()
-            )
-        }
+        activeMediaControllers
+            .map {
+                MediaDeviceSessions(
+                    local = it.local?.mediaDeviceSession(),
+                    remote = it.remote?.mediaDeviceSession()
+                )
+            }
+            .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSessions(null, null))
 
     /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */
     val defaultActiveMediaSession: StateFlow<Result<MediaDeviceSession?>> =
@@ -89,13 +102,17 @@
             .flowOn(backgroundCoroutineContext)
             .stateIn(coroutineScope, SharingStarted.Eagerly, Result.Loading())
 
-    private val localMediaRepository: SharedFlow<LocalMediaRepository> =
+    private val localMediaRepository: Flow<LocalMediaRepository> =
         defaultActiveMediaSession
             .filterData()
             .map { it?.packageName }
             .distinctUntilChanged()
             .map { localMediaRepositoryFactory.create(it) }
-            .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
+            .stateIn(
+                coroutineScope,
+                SharingStarted.Eagerly,
+                localMediaRepositoryFactory.create(null)
+            )
 
     /** Currently connected [MediaDevice]. */
     val currentConnectedDevice: Flow<MediaDevice?> =
@@ -134,21 +151,33 @@
                     }
                     if (!remoteMediaSessions.contains(controller.packageName)) {
                         remoteMediaSessions.add(controller.packageName)
-                        if (remoteController == null) {
-                            remoteController = controller
-                        }
+                        remoteController = chooseController(remoteController, controller)
                     }
                 }
                 MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
                     if (controller.packageName in remoteMediaSessions) continue
-                    if (localController != null) continue
-                    localController = controller
+                    localController = chooseController(localController, controller)
                 }
             }
         }
         return MediaControllers(local = localController, remote = remoteController)
     }
 
+    private fun chooseController(
+        currentController: MediaController?,
+        newController: MediaController,
+    ): MediaController {
+        if (currentController == null) {
+            return newController
+        }
+        val isNewControllerActive = newController.playbackState?.isActive == true
+        val isCurrentControllerActive = currentController.playbackState?.isActive == true
+        if (isNewControllerActive && !isCurrentControllerActive) {
+            return newController
+        }
+        return currentController
+    }
+
     private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? {
         return MediaDeviceSession(
             packageName = packageName,
@@ -160,6 +189,14 @@
         )
     }
 
+    private fun MediaController?.stateChanges(): Flow<MediaController?> {
+        if (this == null) {
+            return flowOf(null)
+        }
+
+        return stateChanges(backgroundHandler).map { this }.onStart { emit(this@stateChanges) }
+    }
+
     private data class MediaControllers(
         val local: MediaController?,
         val remote: MediaController?,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
index 192e0ec..be3a529 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
@@ -143,7 +143,7 @@
                 null,
             )
 
-    fun onBarClick(expandable: Expandable) {
+    fun onBarClick(expandable: Expandable?) {
         uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_MEDIA_OUTPUT_CLICKED)
         val result = sessionWithPlaybackState.value
         actionsInteractor.onBarClick((result as? Result.Data)?.data, expandable)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
index f5b5261..bcaad01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
@@ -19,20 +19,24 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import android.os.fakeExecutorHandler
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
-import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
+import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.android.systemui.util.ThreadAssert
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -46,32 +50,31 @@
 class KeyguardBlueprintRepositoryTest : SysuiTestCase() {
     private lateinit var underTest: KeyguardBlueprintRepository
     @Mock lateinit var configurationRepository: ConfigurationRepository
+    @Mock lateinit var defaultLockscreenBlueprint: DefaultKeyguardBlueprint
     @Mock lateinit var threadAssert: ThreadAssert
-
     private val testScope = TestScope(StandardTestDispatcher())
     private val kosmos: Kosmos = testKosmos()
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        underTest = kosmos.keyguardBlueprintRepository
+        with(kosmos) {
+            whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT)
+            underTest =
+                KeyguardBlueprintRepository(
+                    setOf(defaultLockscreenBlueprint),
+                    fakeExecutorHandler,
+                    threadAssert,
+                )
+        }
     }
 
     @Test
     fun testApplyBlueprint_DefaultLayout() {
         testScope.runTest {
             val blueprint by collectLastValue(underTest.blueprint)
-            underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)
-            assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
-        }
-    }
-
-    @Test
-    fun testApplyBlueprint_SplitShadeLayout() {
-        testScope.runTest {
-            val blueprint by collectLastValue(underTest.blueprint)
-            underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)
-            assertThat(blueprint).isEqualTo(kosmos.splitShadeBlueprint)
+            underTest.applyBlueprint(defaultLockscreenBlueprint)
+            assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
         }
     }
 
@@ -80,22 +83,33 @@
         testScope.runTest {
             val blueprint by collectLastValue(underTest.blueprint)
             underTest.refreshBlueprint()
-            assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
+            assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
         }
     }
 
     @Test
-    fun testTransitionToDefaultLayout_validId() {
-        assertThat(underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)).isTrue()
-    }
-
-    @Test
-    fun testTransitionToSplitShadeLayout_validId() {
-        assertThat(underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)).isTrue()
+    fun testTransitionToLayout_validId() {
+        assertThat(underTest.applyBlueprint(DEFAULT)).isTrue()
     }
 
     @Test
     fun testTransitionToLayout_invalidId() {
         assertThat(underTest.applyBlueprint("abc")).isFalse()
     }
+
+    @Test
+    fun testTransitionToSameBlueprint_refreshesBlueprint() =
+        with(kosmos) {
+            testScope.runTest {
+                val transition by collectLastValue(underTest.refreshTransition)
+                fakeExecutor.runAllReady()
+                runCurrent()
+
+                underTest.applyBlueprint(defaultLockscreenBlueprint)
+                fakeExecutor.runAllReady()
+                runCurrent()
+
+                assertThat(transition).isNotNull()
+            }
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
index 9ccf212..f32e775 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
@@ -274,4 +274,27 @@
             runCurrent()
             assertThat(isAnimatingSurface).isFalse()
         }
+
+    @Test
+    fun notificationLaunchFalse_isAnimatingSurfaceFalse() =
+        testScope.runTest {
+            val isAnimatingSurface by collectLastValue(underTest.isAnimatingSurface)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                    transitionState = TransitionState.STARTED,
+                )
+            )
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.LOCKSCREEN,
+                    transitionState = TransitionState.FINISHED,
+                )
+            )
+            kosmos.notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(false)
+            runCurrent()
+            assertThat(isAnimatingSurface).isFalse()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
index 8a0613f..dbf6a29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
@@ -66,19 +66,25 @@
     fun testHelp() {
         command().execute(pw, listOf("help"))
         verify(pw, atLeastOnce()).println(anyString())
-        verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
+        verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
     }
 
     @Test
     fun testBlank() {
         command().execute(pw, listOf())
         verify(pw, atLeastOnce()).println(anyString())
-        verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
+        verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
     }
 
     @Test
     fun testValidArg() {
         command().execute(pw, listOf("fake"))
-        verify(keyguardBlueprintInteractor).transitionOrRefreshBlueprint("fake")
+        verify(keyguardBlueprintInteractor).transitionToBlueprint("fake")
+    }
+
+    @Test
+    fun testValidArg_Int() {
+        command().execute(pw, listOf("1"))
+        verify(keyguardBlueprintInteractor).transitionToBlueprint(1)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index a66a136..f262df1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -24,6 +24,7 @@
 
 import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
 
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -43,6 +44,7 @@
 import android.platform.test.annotations.EnableFlags;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewTreeObserver;
 
@@ -51,11 +53,14 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto;
+import com.android.systemui.ExpandHelper;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.DisableSceneContainer;
+import com.android.systemui.flags.EnableSceneContainer;
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -171,6 +176,7 @@
     @Mock private NotificationListViewBinder mViewBinder;
     @Mock
     private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController;
+    @Mock private ExpandHelper mExpandHelper;
 
     @Captor
     private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor;
@@ -895,6 +901,50 @@
         verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any());
     }
 
+    @Test
+    @EnableSceneContainer
+    public void onTouchEvent_stopExpandingNotification_sceneContainerEnabled() {
+        boolean touchHandled = stopExpandingNotification();
+
+        verify(mNotificationStackScrollLayout).startOverscrollAfterExpanding();
+        verify(mNotificationStackScrollLayout, never()).dispatchDownEventToScroller(any());
+        assertTrue(touchHandled);
+    }
+
+    @Test
+    @DisableSceneContainer
+    public void onTouchEvent_stopExpandingNotification_sceneContainerDisabled() {
+        stopExpandingNotification();
+
+        verify(mNotificationStackScrollLayout, never()).startOverscrollAfterExpanding();
+        verify(mNotificationStackScrollLayout).dispatchDownEventToScroller(any());
+    }
+
+    private boolean stopExpandingNotification() {
+        when(mNotificationStackScrollLayout.getExpandHelper()).thenReturn(mExpandHelper);
+        when(mNotificationStackScrollLayout.getIsExpanded()).thenReturn(true);
+        when(mNotificationStackScrollLayout.getExpandedInThisMotion()).thenReturn(true);
+        when(mNotificationStackScrollLayout.isExpandingNotification()).thenReturn(true);
+
+        when(mExpandHelper.onTouchEvent(any())).thenAnswer(i -> {
+            when(mNotificationStackScrollLayout.isExpandingNotification()).thenReturn(false);
+            return false;
+        });
+
+        initController(/* viewIsAttached= */ true);
+        NotificationStackScrollLayoutController.TouchHandler touchHandler =
+                mController.getTouchHandler();
+
+        return touchHandler.onTouchEvent(MotionEvent.obtain(
+                /* downTime= */ 0,
+                /* eventTime= */ 0,
+                MotionEvent.ACTION_DOWN,
+                0,
+                0,
+                /* metaState= */ 0
+        ));
+    }
+
     private LogMaker logMatcher(int category, int type) {
         return argThat(new LogMatcher(category, type));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 939d055..0c0a2a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -207,6 +207,7 @@
                 .thenReturn(mNotificationRoundnessManager);
         mStackScroller.setController(mStackScrollLayoutController);
         mStackScroller.setShelf(mNotificationShelf);
+        when(mStackScroller.getExpandHelper()).thenReturn(mExpandHelper);
 
         doNothing().when(mGroupExpansionManager).collapseGroups();
         doNothing().when(mExpandHelper).cancelImmediately();
@@ -1139,6 +1140,14 @@
         assertFalse(mStackScroller.mHeadsUpAnimatingAway);
     }
 
+    @Test
+    @EnableSceneContainer
+    public void finishExpanding_sceneContainerEnabled() {
+        mStackScroller.startOverscrollAfterExpanding();
+        verify(mStackScroller.getExpandHelper()).finishExpanding();
+        assertTrue(mStackScroller.getIsBeingDragged());
+    }
+
     private MotionEvent captureTouchSentToSceneFramework() {
         ArgumentCaptor<MotionEvent> captor = ArgumentCaptor.forClass(MotionEvent.class);
         verify(mStackScrollLayoutController).sendTouchToSceneFramework(captor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt
index 140e919..78d4f02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt
@@ -7,6 +7,7 @@
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -23,7 +24,7 @@
     @Before
     fun setUp() {
         userRepository.setUserInfos(USER_INFOS)
-        underTest = SelectedUserInteractor(userRepository)
+        underTest = SelectedUserInteractor(TestCoroutineScope(), userRepository)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt
index 7eef704..c7b06b6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt
@@ -58,7 +58,7 @@
         bouncerRepository: FakeKeyguardBouncerRepository = FakeKeyguardBouncerRepository(),
         keyguardUpdateMonitor: KeyguardUpdateMonitor = mock(KeyguardUpdateMonitor::class.java),
         powerRepository: FakePowerRepository = FakePowerRepository(),
-        userRepository: FakeUserRepository = FakeUserRepository(),
+        userRepository: FakeUserRepository = FakeUserRepository()
     ): WithDependencies {
         val primaryBouncerInteractor =
             PrimaryBouncerInteractor(
@@ -95,7 +95,11 @@
             PowerInteractorFactory.create(
                 repository = powerRepository,
             )
-        val selectedUserInteractor = SelectedUserInteractor(repository = userRepository)
+        val selectedUserInteractor =
+            SelectedUserInteractor(
+                applicationScope = testScope.backgroundScope,
+                repository = userRepository
+            )
         return WithDependencies(
             trustRepository = trustRepository,
             keyguardRepository = keyguardRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
index 5e71227..872eba06 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel
 
 val Kosmos.notificationsShadeSceneViewModel: NotificationsShadeSceneViewModel by
     Kosmos.Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
new file mode 100644
index 0000000..8c5ff1d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel
+
+val Kosmos.quickSettingsShadeSceneViewModel: QuickSettingsShadeSceneViewModel by
+    Kosmos.Fixture {
+        QuickSettingsShadeSceneViewModel(
+            applicationScope = applicationCoroutineScope,
+            overlayShadeViewModel = overlayShadeViewModel,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 3e9ae4d..1f2ecb7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -37,7 +37,7 @@
 class FakeUserRepository @Inject constructor() : UserRepository {
     companion object {
         // User id to represent a non system (human) user id. We presume this is the main user.
-        private const val MAIN_USER_ID = 10
+        const val MAIN_USER_ID = 10
 
         private const val DEFAULT_SELECTED_USER = 0
         private val DEFAULT_SELECTED_USER_INFO =
@@ -84,6 +84,10 @@
 
     override var isRefreshUsersPaused: Boolean = false
 
+    override suspend fun getMainUserId(): Int? {
+        return MAIN_USER_ID
+    }
+
     var refreshUsersCallCount: Int = 0
         private set
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt
index 89672f1..9dddfcd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.user.domain.interactor
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.user.data.repository.userRepository
 
-val Kosmos.selectedUserInteractor by Kosmos.Fixture { SelectedUserInteractor(userRepository) }
+val Kosmos.selectedUserInteractor by
+    Kosmos.Fixture { SelectedUserInteractor(applicationCoroutineScope, userRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
index 5db1724..546a797 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
@@ -19,8 +19,10 @@
 import android.content.packageManager
 import android.content.pm.ApplicationInfo
 import android.media.AudioAttributes
+import android.media.VolumeProvider
 import android.media.session.MediaController
 import android.media.session.MediaSession
+import android.media.session.PlaybackState
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -28,6 +30,18 @@
 import com.android.systemui.util.mockito.whenever
 
 private const val LOCAL_PACKAGE = "local.test.pkg"
+var Kosmos.localPlaybackInfo by
+    Kosmos.Fixture {
+        MediaController.PlaybackInfo(
+            MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+            VolumeProvider.VOLUME_CONTROL_ABSOLUTE,
+            10,
+            3,
+            AudioAttributes.Builder().build(),
+            "",
+        )
+    }
+var Kosmos.localPlaybackStateBuilder by Kosmos.Fixture { PlaybackState.Builder() }
 var Kosmos.localMediaController: MediaController by
     Kosmos.Fixture {
         val appInfo: ApplicationInfo = mock {
@@ -39,22 +53,25 @@
         val localSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
         mock {
             whenever(packageName).thenReturn(LOCAL_PACKAGE)
-            whenever(playbackInfo)
-                .thenReturn(
-                    MediaController.PlaybackInfo(
-                        MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
-                        0,
-                        0,
-                        0,
-                        AudioAttributes.Builder().build(),
-                        "",
-                    )
-                )
+            whenever(playbackInfo).thenReturn(localPlaybackInfo)
+            whenever(playbackState).thenReturn(localPlaybackStateBuilder.build())
             whenever(sessionToken).thenReturn(localSessionToken)
         }
     }
 
 private const val REMOTE_PACKAGE = "remote.test.pkg"
+var Kosmos.remotePlaybackInfo by
+    Kosmos.Fixture {
+        MediaController.PlaybackInfo(
+            MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+            VolumeProvider.VOLUME_CONTROL_ABSOLUTE,
+            10,
+            7,
+            AudioAttributes.Builder().build(),
+            "",
+        )
+    }
+var Kosmos.remotePlaybackStateBuilder by Kosmos.Fixture { PlaybackState.Builder() }
 var Kosmos.remoteMediaController: MediaController by
     Kosmos.Fixture {
         val appInfo: ApplicationInfo = mock {
@@ -66,17 +83,8 @@
         val remoteSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
         mock {
             whenever(packageName).thenReturn(REMOTE_PACKAGE)
-            whenever(playbackInfo)
-                .thenReturn(
-                    MediaController.PlaybackInfo(
-                        MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
-                        0,
-                        0,
-                        0,
-                        AudioAttributes.Builder().build(),
-                        "",
-                    )
-                )
+            whenever(playbackInfo).thenReturn(remotePlaybackInfo)
+            whenever(playbackState).thenReturn(remotePlaybackStateBuilder.build())
             whenever(sessionToken).thenReturn(remoteSessionToken)
         }
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index fa3a19b..d743558 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -30,13 +30,12 @@
 import com.android.systemui.volume.data.repository.FakeLocalMediaRepository
 import com.android.systemui.volume.data.repository.FakeMediaControllerRepository
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.FakeLocalMediaRepositoryFactory
-import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
 
 val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() }
-val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by
+val Kosmos.localMediaRepositoryFactory by
     Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } }
 
 val Kosmos.mediaOutputActionsInteractor by
@@ -55,6 +54,7 @@
             testScope.backgroundScope,
             testScope.testScheduler,
             mediaControllerRepository,
+            Handler(TestableLooper.get(testCase).looper),
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt
index 1b3480c..9c902cf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt
@@ -18,9 +18,15 @@
 
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 
-class FakeLocalMediaRepositoryFactory(
-    val provider: (packageName: String?) -> LocalMediaRepository
-) : LocalMediaRepositoryFactory {
+class FakeLocalMediaRepositoryFactory(private val defaultProvider: () -> LocalMediaRepository) :
+    LocalMediaRepositoryFactory {
 
-    override fun create(packageName: String?): LocalMediaRepository = provider(packageName)
+    private val repositories = mutableMapOf<String, LocalMediaRepository>()
+
+    fun setLocalMediaRepository(packageName: String, localMediaRepository: LocalMediaRepository) {
+        repositories[packageName] = localMediaRepository
+    }
+
+    override fun create(packageName: String?): LocalMediaRepository =
+        repositories[packageName] ?: defaultProvider()
 }
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 9b83ede..9520621 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -128,6 +128,7 @@
         "aoc",
         "app_widgets",
         "arc_next",
+        "art_mainline",
         "avic",
         "biometrics",
         "biometrics_framework",
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 3c702b4..b1976cd 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -7155,6 +7155,7 @@
         synchronized (mUsersLock) {
             pw.println("  Boot user: " + mBootUser);
         }
+        pw.println("Can add private profile: "+ canAddPrivateProfile(currentUserId));
 
         pw.println();
         pw.println("Number of listeners for");
diff --git a/services/core/java/com/android/server/power/batterysaver/flags.aconfig b/services/core/java/com/android/server/power/batterysaver/flags.aconfig
index fa29dc1..059c4a4 100644
--- a/services/core/java/com/android/server/power/batterysaver/flags.aconfig
+++ b/services/core/java/com/android/server/power/batterysaver/flags.aconfig
@@ -3,7 +3,7 @@
 
 flag {
   name: "update_auto_turn_on_notification_string_and_action"
-  namespace: "battery_saver"
+  namespace: "power_optimization"
   description: "Improve the string and hightligh settings item for battery saver auto-turn-on notification"
   bug: "336960905"
   metadata {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index d20b3b2..f8eb789 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -3646,7 +3646,8 @@
             }
             // System wallpaper does not support multiple displays, attach this display to
             // the fallback wallpaper.
-            if (mFallbackWallpaper != null) {
+            if (mFallbackWallpaper != null && mFallbackWallpaper
+                        .connection != null) {
                 final DisplayConnector connector = mFallbackWallpaper
                         .connection.getDisplayConnectorOrCreate(displayId);
                 if (connector == null) return;
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index c5683f3..fe4522a 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -99,7 +99,6 @@
 import android.window.SizeConfigurationBuckets;
 import android.window.TransitionInfo;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.AssistUtils;
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.protolog.common.ProtoLog;
@@ -109,9 +108,6 @@
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.uri.GrantUri;
 import com.android.server.uri.NeededUriGrants;
-import com.android.server.utils.quota.Categorizer;
-import com.android.server.utils.quota.Category;
-import com.android.server.utils.quota.CountQuotaTracker;
 import com.android.server.vr.VrManagerInternal;
 
 /**
@@ -127,13 +123,6 @@
     private final ActivityTaskSupervisor mTaskSupervisor;
     private final Context mContext;
 
-    // Prevent malicious app abusing the Activity#setPictureInPictureParams API
-    @VisibleForTesting CountQuotaTracker mSetPipAspectRatioQuotaTracker;
-    // Limit to 60 times / minute
-    private static final int SET_PIP_ASPECT_RATIO_LIMIT = 60;
-    // The timeWindowMs here can not be smaller than QuotaTracker#MIN_WINDOW_SIZE_MS
-    private static final long SET_PIP_ASPECT_RATIO_TIME_WINDOW_MS = 60_000;
-
     /** Wrapper around VoiceInteractionServiceManager. */
     private AssistUtils mAssistUtils;
 
@@ -1046,25 +1035,6 @@
                     + ": Current activity does not support picture-in-picture.");
         }
 
-        // Rate limit how frequent an app can request aspect ratio change via
-        // Activity#setPictureInPictureParams
-        final int userId = UserHandle.getCallingUserId();
-        if (mSetPipAspectRatioQuotaTracker == null) {
-            mSetPipAspectRatioQuotaTracker = new CountQuotaTracker(mContext,
-                    Categorizer.SINGLE_CATEGORIZER);
-            mSetPipAspectRatioQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY,
-                    SET_PIP_ASPECT_RATIO_LIMIT, SET_PIP_ASPECT_RATIO_TIME_WINDOW_MS);
-        }
-        if (r.pictureInPictureArgs.hasSetAspectRatio()
-                && params.hasSetAspectRatio()
-                && !r.pictureInPictureArgs.getAspectRatio().equals(
-                params.getAspectRatio())
-                && !mSetPipAspectRatioQuotaTracker.noteEvent(
-                userId, r.packageName, "setPipAspectRatio")) {
-            throw new IllegalStateException(caller
-                    + ": Too many PiP aspect ratio change requests from " + r.packageName);
-        }
-
         final float minAspectRatio = mContext.getResources().getFloat(
                 com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
         final float maxAspectRatio = mContext.getResources().getFloat(
diff --git a/services/core/java/com/android/server/wm/InputConfigAdapter.java b/services/core/java/com/android/server/wm/InputConfigAdapter.java
index ef1b02d..119fafd 100644
--- a/services/core/java/com/android/server/wm/InputConfigAdapter.java
+++ b/services/core/java/com/android/server/wm/InputConfigAdapter.java
@@ -58,8 +58,8 @@
                     LayoutParams.INPUT_FEATURE_SPY,
                     InputConfig.SPY, false /* inverted */),
             new FlagMapping(
-                    LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING,
-                    InputConfig.SENSITIVE_FOR_TRACING, false /* inverted */));
+                    LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
+                    InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */));
 
     @InputConfigFlags
     private static final int INPUT_FEATURE_TO_CONFIG_MASK =
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 56e5d76..5f13672 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3176,6 +3176,11 @@
                 mTaskId, snapshot);
     }
 
+    void onSnapshotInvalidated() {
+        mAtmService.getTaskChangeNotificationController().notifyTaskSnapshotInvalidated(mTaskId);
+    }
+
+
     TaskDescription getTaskDescription() {
         return mTaskDescription;
     }
diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
index 9324e29..21e7a8d 100644
--- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
+++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
@@ -61,6 +61,7 @@
     private static final int NOTIFY_ACTIVITY_ROTATED_MSG = 26;
     private static final int NOTIFY_TASK_MOVED_TO_BACK_LISTENERS_MSG = 27;
     private static final int NOTIFY_LOCK_TASK_MODE_CHANGED_MSG = 28;
+    private static final int NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG = 29;
 
     // Delay in notifying task stack change listeners (in millis)
     private static final int NOTIFY_TASK_STACK_CHANGE_LISTENERS_DELAY = 100;
@@ -150,6 +151,9 @@
     private final TaskStackConsumer mNotifyTaskSnapshotChanged = (l, m) -> {
         l.onTaskSnapshotChanged(m.arg1, (TaskSnapshot) m.obj);
     };
+    private final TaskStackConsumer mNotifyTaskSnapshotInvalidated = (l, m) -> {
+        l.onTaskSnapshotInvalidated(m.arg1);
+    };
 
     private final TaskStackConsumer mNotifyTaskDisplayChanged = (l, m) -> {
         l.onTaskDisplayChanged(m.arg1, m.arg2);
@@ -271,6 +275,9 @@
                 case NOTIFY_LOCK_TASK_MODE_CHANGED_MSG:
                     forAllRemoteListeners(mNotifyLockTaskModeChanged, msg);
                     break;
+                case NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG:
+                    forAllRemoteListeners(mNotifyTaskSnapshotInvalidated, msg);
+                    break;
             }
             if (msg.obj instanceof SomeArgs) {
                 ((SomeArgs) msg.obj).recycle();
@@ -485,6 +492,16 @@
     }
 
     /**
+     * Notify listeners that the snapshot of a task is invalidated.
+     */
+    void notifyTaskSnapshotInvalidated(int taskId) {
+        final Message msg = mHandler.obtainMessage(NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG,
+                taskId, 0 /* unused */);
+        forAllLocalListeners(mNotifyTaskSnapshotInvalidated, msg);
+        msg.sendToTarget();
+    }
+
+    /**
      * Notify listeners that an activity received a back press when there are no other activities
      * in the back stack.
      */
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index d0a50d5..dbe3d36 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -44,6 +44,7 @@
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.permission.flags.Flags.sensitiveContentImprovements;
 import static android.permission.flags.Flags.sensitiveContentMetricsBugfix;
+import static android.permission.flags.Flags.sensitiveContentRecentsScreenshotBugfix;
 import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
 import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW;
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
@@ -68,7 +69,7 @@
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
 import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
 import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
@@ -8868,6 +8869,14 @@
                         mRoot.forAllWindows((w) -> {
                             if (w.isVisible()) {
                                 WindowManagerService.this.showToastIfBlockingScreenCapture(w);
+                            } else if (sensitiveContentRecentsScreenshotBugfix()
+                                    && shouldInvalidateSnapshot(w)) {
+                                final Task task = w.getTask();
+                                // preventing from showing up in starting window.
+                                mTaskSnapshotController.removeAndDeleteSnapshot(
+                                        task.mTaskId, task.mUserId);
+                                // Refresh TaskThumbnailCache
+                                task.onSnapshotInvalidated();
                             }
                         }, /* traverseTopToBottom= */ true);
                     }
@@ -8875,6 +8884,12 @@
             }
         }
 
+        private boolean shouldInvalidateSnapshot(WindowState w) {
+            return w.getTask() != null
+                    && mSensitiveContentPackages.shouldBlockScreenCaptureForApp(
+                    w.getOwningPackage(), w.getOwningUid(), w.getWindowToken());
+        }
+
         @Override
         public void removeBlockScreenCaptureForApps(ArraySet<PackageInfo> packageInfos) {
             synchronized (mGlobalLock) {
@@ -9296,11 +9311,11 @@
             }
         }
 
-        // You can only use INPUT_FEATURE_SENSITIVE_FOR_TRACING on a trusted overlay.
-        if ((inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) != 0 && !isTrustedOverlay) {
-            Slog.w(TAG, "Removing INPUT_FEATURE_SENSITIVE_FOR_TRACING from '" + windowName
+        // You can only use INPUT_FEATURE_SENSITIVE_FOR_PRIVACY on a trusted overlay.
+        if ((inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) != 0 && !isTrustedOverlay) {
+            Slog.w(TAG, "Removing INPUT_FEATURE_SENSITIVE_FOR_PRIVACY from '" + windowName
                     + "' because it isn't a trusted overlay");
-            return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+            return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
         }
         return inputFeatures;
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java
index 950ec77..502607b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java
@@ -19,7 +19,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.admin.BooleanPolicyValue;
-import android.app.admin.PolicyKey;
 import android.util.Log;
 
 import com.android.modules.utils.TypedXmlPullParser;
@@ -37,8 +36,7 @@
     private static final String TAG = "BooleanPolicySerializer";
 
     @Override
-    void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, @NonNull Boolean value)
-            throws IOException {
+    void saveToXml(TypedXmlSerializer serializer, @NonNull Boolean value) throws IOException {
         Objects.requireNonNull(value);
         serializer.attributeBoolean(/* namespace= */ null, ATTR_VALUE, value);
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java
index d24afabe..a65c7e1 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java
@@ -18,8 +18,6 @@
 
 import android.annotation.NonNull;
 import android.app.admin.BundlePolicyValue;
-import android.app.admin.PackagePolicyKey;
-import android.app.admin.PolicyKey;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.util.Log;
@@ -53,14 +51,8 @@
     private static final String ATTR_TYPE_BUNDLE_ARRAY = "BA";
 
     @Override
-    void saveToXml(@NonNull PolicyKey policyKey, TypedXmlSerializer serializer,
-            @NonNull Bundle value) throws IOException {
+    void saveToXml(TypedXmlSerializer serializer, @NonNull Bundle value) throws IOException {
         Objects.requireNonNull(value);
-        Objects.requireNonNull(policyKey);
-        if (!(policyKey instanceof PackagePolicyKey)) {
-            throw new IllegalArgumentException("policyKey is not of type "
-                    + "PackagePolicyKey");
-        }
         writeBundle(value, serializer);
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java
index 6303a1a..01f56e0 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java
@@ -19,7 +19,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.admin.ComponentNamePolicyValue;
-import android.app.admin.PolicyKey;
 import android.content.ComponentName;
 import android.util.Log;
 
@@ -37,8 +36,7 @@
     private static final String ATTR_CLASS_NAME = "class-name";
 
     @Override
-    void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
-            @NonNull ComponentName value) throws IOException {
+    void saveToXml(TypedXmlSerializer serializer, @NonNull ComponentName value) throws IOException {
         Objects.requireNonNull(value);
         serializer.attribute(
                 /* namespace= */ null, ATTR_PACKAGE_NAME, value.getPackageName());
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index f553a5a..dd173af 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1985,11 +1985,6 @@
             CryptoTestHelper.runAndLogSelfTest();
         }
 
-        public String[] getPersonalAppsForSuspension(@UserIdInt int userId) {
-            return PersonalAppsSuspensionHelper.forUser(mContext, userId)
-                    .getPersonalAppsForSuspension();
-        }
-
         public long systemCurrentTimeMillis() {
             return System.currentTimeMillis();
         }
@@ -21610,9 +21605,12 @@
                                 == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
             }
 
-            if (Flags.headlessSingleUserFixes() && mInjector.userManagerIsHeadlessSystemUserMode()
-                    && isSingleUserMode && !mInjector.isChangeEnabled(
-                    PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(), caller.getUserId())) {
+            if (Flags.headlessSingleMinTargetSdk()
+                    && mInjector.userManagerIsHeadlessSystemUserMode()
+                    && isSingleUserMode
+                    && !mInjector.isChangeEnabled(
+                            PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(),
+                    caller.getUserId())) {
                 throw new IllegalStateException("Device admin is not targeting Android V.");
             }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java
index 45a2d2a..ebbf22c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java
@@ -19,7 +19,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.admin.IntegerPolicyValue;
-import android.app.admin.PolicyKey;
 import android.util.Log;
 
 import com.android.modules.utils.TypedXmlPullParser;
@@ -37,8 +36,7 @@
     private static final String ATTR_VALUE = "value";
 
     @Override
-    void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
-            @NonNull Integer value) throws IOException {
+    void saveToXml(TypedXmlSerializer serializer, @NonNull Integer value) throws IOException {
         Objects.requireNonNull(value);
         serializer.attributeInt(/* namespace= */ null, ATTR_VALUE, value);
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java
index 20bd2d7..13412d0 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.app.admin.LockTaskPolicy;
-import android.app.admin.PolicyKey;
 import android.util.Log;
 
 import com.android.modules.utils.TypedXmlPullParser;
@@ -39,8 +38,8 @@
     private static final String ATTR_FLAGS = "flags";
 
     @Override
-    void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
-            @NonNull LockTaskPolicy value) throws IOException {
+    void saveToXml(TypedXmlSerializer serializer, @NonNull LockTaskPolicy value)
+            throws IOException {
         Objects.requireNonNull(value);
         serializer.attribute(
                 /* namespace= */ null,
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java
index 522c4b5..c363e66 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java
@@ -19,7 +19,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.admin.LongPolicyValue;
-import android.app.admin.PolicyKey;
 import android.util.Log;
 
 import com.android.modules.utils.TypedXmlPullParser;
@@ -37,8 +36,7 @@
     private static final String ATTR_VALUE = "value";
 
     @Override
-    void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
-            @NonNull Long value) throws IOException {
+    void saveToXml(TypedXmlSerializer serializer, @NonNull Long value) throws IOException {
         Objects.requireNonNull(value);
         serializer.attributeLong(/* namespace= */ null, ATTR_VALUE, value);
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
index 8cb511e..7483b43 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
@@ -37,7 +37,6 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
-import android.util.Log;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IAccessibilityManager;
 import android.view.inputmethod.InputMethodInfo;
@@ -107,10 +106,6 @@
         for (final String pkg : unsuspendablePackages) {
             result.remove(pkg);
         }
-
-        if (Log.isLoggable(LOG_TAG, Log.INFO)) {
-            Slogf.i(LOG_TAG, "Packages subject to suspension: %s", String.join(",", result));
-        }
         return result.toArray(new String[0]);
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 7a9fa0f..9a73d5e 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -684,7 +684,7 @@
 
     void savePolicyValueToXml(TypedXmlSerializer serializer, V value)
             throws IOException {
-        mPolicySerializer.saveToXml(mPolicyKey, serializer, value);
+        mPolicySerializer.saveToXml(serializer, value);
     }
 
     @Nullable
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index eeb4976..4bf3ff4 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -375,6 +375,7 @@
     private static void suspendPersonalAppsInPackageManager(Context context, int userId) {
         final String[] appsToSuspend = PersonalAppsSuspensionHelper.forUser(context, userId)
                 .getPersonalAppsForSuspension();
+        Slogf.i(LOG_TAG, "Suspending personal apps: %s", String.join(",", appsToSuspend));
         final String[] failedApps = LocalServices.getService(PackageManagerInternal.class)
                 .setPackagesSuspendedByAdmin(userId, appsToSuspend, true);
         if (!ArrayUtils.isEmpty(failedApps)) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java
index 5af2fa2..e83b031 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java
@@ -17,7 +17,6 @@
 package com.android.server.devicepolicy;
 
 import android.annotation.NonNull;
-import android.app.admin.PolicyKey;
 import android.app.admin.PolicyValue;
 
 import com.android.modules.utils.TypedXmlPullParser;
@@ -26,7 +25,6 @@
 import java.io.IOException;
 
 abstract class PolicySerializer<V> {
-    abstract void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, @NonNull V value)
-            throws IOException;
+    abstract void saveToXml(TypedXmlSerializer serializer, @NonNull V value) throws IOException;
     abstract PolicyValue<V> readFromXml(TypedXmlPullParser parser);
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java
index 0265453..a9d65ac 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.admin.PolicyKey;
 import android.app.admin.PolicyValue;
 import android.app.admin.StringSetPolicyValue;
 import android.util.Log;
@@ -35,8 +34,7 @@
     private static final String ATTR_VALUES = "strings";
     private static final String ATTR_VALUES_SEPARATOR = ";";
     @Override
-    void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
-            @NonNull Set<String> value) throws IOException {
+    void saveToXml(TypedXmlSerializer serializer, @NonNull Set<String> value) throws IOException {
         Objects.requireNonNull(value);
         serializer.attribute(
                 /* namespace= */ null, ATTR_VALUES, String.join(ATTR_VALUES_SEPARATOR, value));
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 855c658..b4cc343 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -441,11 +441,6 @@
         @Override
         public void runCryptoSelfTest() {}
 
-        @Override
-        public String[] getPersonalAppsForSuspension(int userId) {
-            return new String[]{};
-        }
-
         public void setSystemCurrentTimeMillis(long value) {
             mCurrentTimeMillis = value;
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index a78fc10..7e6301f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -21,13 +21,15 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_IMPROVEMENTS;
+import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_RECENTS_SCREENSHOT_BUGFIX;
 import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.FLAG_OWN_FOCUS;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
 import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
@@ -1020,6 +1022,35 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(
+            {FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION, FLAG_SENSITIVE_CONTENT_IMPROVEMENTS,
+                    FLAG_SENSITIVE_CONTENT_RECENTS_SCREENSHOT_BUGFIX})
+    public void addBlockScreenCaptureForApps_appNotInForeground_invalidateSnapshot() {
+        spyOn(mWm.mTaskSnapshotController);
+
+        // createAppWindow uses package name of "test" and uid of "0"
+        String testPackage = "test";
+        int ownerId1 = 0;
+
+        final Task task = createTask(mDisplayContent);
+        final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "appWindow");
+        mWm.mWindowMap.put(win.mClient.asBinder(), win);
+        final ActivityRecord activity = win.mActivityRecord;
+        activity.setVisibleRequested(false);
+        activity.setVisible(false);
+        win.setHasSurface(false);
+
+        PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1);
+        ArraySet<PackageInfo> blockedPackages = new ArraySet();
+        blockedPackages.add(blockedPackage);
+
+        WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+        wmInternal.addBlockScreenCaptureForApps(blockedPackages);
+
+        verify(mWm.mTaskSnapshotController).removeAndDeleteSnapshot(anyInt(), eq(ownerId1));
+    }
+
+    @Test
     public void clearBlockedApps_clearsCache() {
         String testPackage = "test";
         int ownerId1 = 20;
@@ -1192,20 +1223,20 @@
         final InputChannel inputChannel = new InputChannel();
         mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY, surfaceControl,
                 window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */,
-                INPUT_FEATURE_SENSITIVE_FOR_TRACING, TYPE_APPLICATION, null /* windowToken */,
+                INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, TYPE_APPLICATION, null /* windowToken */,
                 inputTransferToken,
                 "TestInputChannel", inputChannel);
         verify(mTransaction).setInputWindowInfo(
                 eq(surfaceControl),
-                argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_TRACING) == 0));
+                argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_PRIVACY) == 0));
 
         mWm.updateInputChannel(inputChannel.getToken(), DEFAULT_DISPLAY, surfaceControl,
                 FLAG_NOT_FOCUSABLE, PRIVATE_FLAG_TRUSTED_OVERLAY,
-                INPUT_FEATURE_SENSITIVE_FOR_TRACING,
+                INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
                 null /* region */);
         verify(mTransaction).setInputWindowInfo(
                 eq(surfaceControl),
-                argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_TRACING) != 0));
+                argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_PRIVACY) != 0));
     }
 
     @RequiresFlagsDisabled(Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER)
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index fb854c5..43b424f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -1235,12 +1235,6 @@
         assertNotNull(o.mInfo);
         assertNotNull(o.mInfo.pictureInPictureParams);
 
-        // Bypass the quota check, which causes NPE in current test setup.
-        if (mWm.mAtmService.mActivityClientController.mSetPipAspectRatioQuotaTracker != null) {
-            mWm.mAtmService.mActivityClientController.mSetPipAspectRatioQuotaTracker
-                    .setEnabled(false);
-        }
-
         final PictureInPictureParams p2 = new PictureInPictureParams.Builder()
                 .setAspectRatio(new Rational(3, 4)).build();
         mWm.mAtmService.mActivityClientController.setPictureInPictureParams(record.token, p2);