Merge "Optimize DisplayRepository to reduce lock contention" into main
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 14ae3f5..d03dd16 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -46,6 +46,7 @@
     field public static final String REMAP_MODIFIER_KEYS = "android.permission.REMAP_MODIFIER_KEYS";
     field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS";
     field public static final String REQUEST_UNIQUE_ID_ATTESTATION = "android.permission.REQUEST_UNIQUE_ID_ATTESTATION";
+    field public static final String RESERVED_FOR_TESTING_SIGNATURE = "android.permission.RESERVED_FOR_TESTING_SIGNATURE";
     field public static final String RESET_APP_ERRORS = "android.permission.RESET_APP_ERRORS";
     field public static final String REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL = "android.permission.REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL";
     field public static final String SET_AND_VERIFY_LOCKSCREEN_CREDENTIALS = "android.permission.SET_AND_VERIFY_LOCKSCREEN_CREDENTIALS";
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index 6cc71e5..b4a3abc 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -1973,6 +1973,8 @@
 
 UnflaggedApi: android.Manifest.permission#MANAGE_REMOTE_AUTH:
     New API must be flagged with @FlaggedApi: field android.Manifest.permission.MANAGE_REMOTE_AUTH
+UnflaggedApi: android.Manifest.permission#RESERVED_FOR_TESTING_SIGNATURE:
+    New API must be flagged with @FlaggedApi: field android.Manifest.permission.RESERVED_FOR_TESTING_SIGNATURE
 UnflaggedApi: android.Manifest.permission#START_ACTIVITIES_FROM_SDK_SANDBOX:
     New API must be flagged with @FlaggedApi: field android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX
 UnflaggedApi: android.Manifest.permission#USE_REMOTE_AUTH:
diff --git a/core/java/android/app/assist/AssistContent.java b/core/java/android/app/assist/AssistContent.java
index e5316bc0..a488689 100644
--- a/core/java/android/app/assist/AssistContent.java
+++ b/core/java/android/app/assist/AssistContent.java
@@ -34,12 +34,22 @@
     }
 
     /**
+     * Create an AssistContent with extras initialized.
+     *
      * @hide
+     */
+    public AssistContent(@android.annotation.NonNull Bundle extras) {
+        mExtras = extras;
+    }
+
+    /**
      * Called by {@link android.app.ActivityThread} to set the default Intent based on
      * {@link android.app.Activity#getIntent Activity.getIntent}.
      *
      * <p>Automatically populates {@link #mUri} if that Intent is an {@link Intent#ACTION_VIEW}
      * of a web (http or https scheme) URI.</p>
+     *
+     * @hide
      */
     public void setDefaultIntent(Intent intent) {
         mIntent = intent;
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 3d7b714..8519722 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -548,6 +548,20 @@
         }
     }
 
+    /**
+     * Request to power a display ON or OFF.
+     * @hide
+     */
+    @RequiresPermission("android.permission.MANAGE_DISPLAYS")
+    public boolean requestDisplayPower(int displayId, boolean on) {
+        try {
+            return mDm.requestDisplayPower(displayId, on);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Error trying to request display power " + on, ex);
+            return false;
+        }
+    }
+
     public void startWifiDisplayScan() {
         synchronized (mLock) {
             if (mWifiDisplayScanNestCount++ == 0) {
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 70efc6f..b7c02b0 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -236,6 +236,10 @@
     @EnforcePermission("MANAGE_DISPLAYS")
     void disableConnectedDisplay(int displayId);
 
+    // Request to power display ON or OFF.
+    @EnforcePermission("MANAGE_DISPLAYS")
+    boolean requestDisplayPower(int displayId, boolean on);
+
     // Restricts display modes to specified modeIds.
     @EnforcePermission("RESTRICT_DISPLAY_MODES")
     void requestDisplayModes(in IBinder token, int displayId, in @nullable int[] modeIds);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 05345d8..63f0b9e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6101,6 +6101,15 @@
         public static final String POINTER_SPEED = "pointer_speed";
 
         /**
+         * Pointer scale setting.
+         *
+         * <p>This float value represents the scale by which the size of the pointer increases.
+         * @hide
+         */
+        @Readable
+        public static final String POINTER_SCALE = "pointer_scale";
+
+        /**
          * Touchpad pointer speed setting.
          * This is an integer value in a range between -7 and +7, so there are 15 possible values.
          *   -7 = slowest
@@ -6358,6 +6367,7 @@
             PRIVATE_SETTINGS.add(SIP_ASK_ME_EACH_TIME);
             PRIVATE_SETTINGS.add(POINTER_SPEED);
             PRIVATE_SETTINGS.add(POINTER_FILL_STYLE);
+            PRIVATE_SETTINGS.add(POINTER_SCALE);
             PRIVATE_SETTINGS.add(LOCK_TO_APP_ENABLED);
             PRIVATE_SETTINGS.add(EGG_MODE);
             PRIVATE_SETTINGS.add(SHOW_BATTERY_PERCENT);
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index e1965ef..405fe26 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -29,6 +29,7 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.app.Service;
+import android.app.assist.AssistContent;
 import android.content.ComponentName;
 import android.content.ContentCaptureOptions;
 import android.content.Intent;
@@ -133,6 +134,16 @@
      */
     public static final String SERVICE_META_DATA = "android.content_capture";
 
+
+    /**
+     * Extras key to flag that the passed in {@link AssistContent} is sent only during Activity
+     * start.
+     *
+     * @hide
+     */
+    public static final String ASSIST_CONTENT_ACTIVITY_START_KEY = "activity_start_assist_content";
+
+
     private final LocalDataShareAdapterResourceManager mDataShareAdapterResourceManager =
             new LocalDataShareAdapterResourceManager();
 
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index 7c2577f..c302126 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -193,6 +193,9 @@
     /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_END =
             POINTER_ICON_VECTOR_STYLE_FILL_BLUE;
 
+    /** @hide */ public static final float DEFAULT_POINTER_SCALE = 1f;
+    /** @hide */ public static final float LARGE_POINTER_SCALE = 2.5f;
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private final int mType;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -253,7 +256,7 @@
      * @hide
      */
     public static @NonNull PointerIcon getLoadedSystemIcon(@NonNull Context context, int type,
-            boolean useLargeIcons) {
+            boolean useLargeIcons, float pointerScale) {
         if (type == TYPE_NOT_SPECIFIED) {
             throw new IllegalStateException("Cannot load icon for type TYPE_NOT_SPECIFIED");
         }
@@ -268,13 +271,18 @@
         }
 
         final int defStyle;
-        // TODO(b/305193969): Use scaled vectors when large icons are requested.
-        if (useLargeIcons) {
-            defStyle = com.android.internal.R.style.LargePointer;
-        } else if (android.view.flags.Flags.enableVectorCursors()) {
+        if (android.view.flags.Flags.enableVectorCursorA11ySettings()) {
             defStyle = com.android.internal.R.style.VectorPointer;
         } else {
-            defStyle = com.android.internal.R.style.Pointer;
+            // TODO(b/346358375): Remove useLargeIcons and the legacy pointer styles when
+            //  enableVectorCursorA11ySetting is rolled out.
+            if (useLargeIcons) {
+                defStyle = com.android.internal.R.style.LargePointer;
+            } else if (android.view.flags.Flags.enableVectorCursors()) {
+                defStyle = com.android.internal.R.style.VectorPointer;
+            } else {
+                defStyle = com.android.internal.R.style.Pointer;
+            }
         }
         TypedArray a = context.obtainStyledAttributes(null,
                 com.android.internal.R.styleable.Pointer,
@@ -286,11 +294,11 @@
             Log.w(TAG, "Missing theme resources for pointer icon type " + type);
             return type == TYPE_DEFAULT
                     ? getSystemIcon(TYPE_NULL)
-                    : getLoadedSystemIcon(context, TYPE_DEFAULT, useLargeIcons);
+                    : getLoadedSystemIcon(context, TYPE_DEFAULT, useLargeIcons, pointerScale);
         }
 
         final PointerIcon icon = new PointerIcon(type);
-        icon.loadResource(context.getResources(), resourceId, context.getTheme());
+        icon.loadResource(context.getResources(), resourceId, context.getTheme(), pointerScale);
         return icon;
     }
 
@@ -353,7 +361,7 @@
         }
 
         PointerIcon icon = new PointerIcon(TYPE_CUSTOM);
-        icon.loadResource(resources, resourceId, null);
+        icon.loadResource(resources, resourceId, null, DEFAULT_POINTER_SCALE);
         return icon;
     }
 
@@ -460,12 +468,13 @@
     }
 
     private BitmapDrawable getBitmapDrawableFromVectorDrawable(Resources resources,
-            VectorDrawable vectorDrawable) {
+            VectorDrawable vectorDrawable, float pointerScale) {
         // Ensure we pass the display metrics into the Bitmap constructor so that it is initialized
         // with the correct density.
         Bitmap bitmap = Bitmap.createBitmap(resources.getDisplayMetrics(),
-                vectorDrawable.getIntrinsicWidth(),
-                vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888, true /* hasAlpha */);
+                (int) (vectorDrawable.getIntrinsicWidth() * pointerScale),
+                (int) (vectorDrawable.getIntrinsicHeight() * pointerScale),
+                Bitmap.Config.ARGB_8888, true /* hasAlpha */);
         Canvas canvas = new Canvas(bitmap);
         vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
         vectorDrawable.draw(canvas);
@@ -473,7 +482,7 @@
     }
 
     private void loadResource(@NonNull Resources resources, @XmlRes int resourceId,
-            @Nullable Resources.Theme theme) {
+            @Nullable Resources.Theme theme, float pointerScale) {
         final XmlResourceParser parser = resources.getXml(resourceId);
         final int bitmapRes;
         final float hotSpotX;
@@ -484,8 +493,10 @@
             final TypedArray a = resources.obtainAttributes(
                     parser, com.android.internal.R.styleable.PointerIcon);
             bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0);
-            hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0);
-            hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0);
+            hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0)
+                    * pointerScale;
+            hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0)
+                    * pointerScale;
             a.recycle();
         } catch (Exception ex) {
             throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex);
@@ -534,7 +545,7 @@
                     }
                     if (isVectorAnimation) {
                         drawableFrame = getBitmapDrawableFromVectorDrawable(resources,
-                                (VectorDrawable) drawableFrame);
+                                (VectorDrawable) drawableFrame, pointerScale);
                     }
                     mBitmapFrames[i - 1] = getBitmapFromDrawable((BitmapDrawable) drawableFrame);
                 }
@@ -542,7 +553,8 @@
         }
         if (drawable instanceof VectorDrawable) {
             mDrawNativeDropShadow = true;
-            drawable = getBitmapDrawableFromVectorDrawable(resources, (VectorDrawable) drawable);
+            drawable = getBitmapDrawableFromVectorDrawable(resources, (VectorDrawable) drawable,
+                    pointerScale);
         }
         if (!(drawable instanceof BitmapDrawable)) {
             throw new IllegalArgumentException("<pointer-icon> bitmap attribute must "
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index bcef37f..d74867c 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -366,6 +366,14 @@
             "enable_content_protection_receiver";
 
     /**
+     * Whether AssistContent snapshot should be sent on activity start.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_ENABLE_ACTIVITY_START_ASSIST_CONTENT =
+            "enable_activity_start_assist_content";
+
+    /**
      * Sets the size of the in-memory ring buffer for the content protection flow.
      *
      * @hide
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 2194c89..40d760e 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -537,8 +537,13 @@
         }
         mContentParent.requestApplyInsets();
         final Callback cb = getCallback();
-        if (cb != null && !isDestroyed()) {
-            cb.onContentChanged();
+        if (!isDestroyed()) {
+            if (cb != null) {
+                cb.onContentChanged();
+            }
+            if (mDecorContentParent != null) {
+                mDecorContentParent.notifyContentChanged();
+            }
         }
         mContentParentExplicitlySet = true;
     }
@@ -568,8 +573,13 @@
         }
         mContentParent.requestApplyInsets();
         final Callback cb = getCallback();
-        if (cb != null && !isDestroyed()) {
-            cb.onContentChanged();
+        if (!isDestroyed()) {
+            if (cb != null) {
+                cb.onContentChanged();
+            }
+            if (mDecorContentParent != null) {
+                mDecorContentParent.notifyContentChanged();
+            }
         }
         mContentParentExplicitlySet = true;
     }
@@ -586,8 +596,13 @@
         mContentParent.addView(view, params);
         mContentParent.requestApplyInsets();
         final Callback cb = getCallback();
-        if (cb != null && !isDestroyed()) {
-            cb.onContentChanged();
+        if (!isDestroyed()) {
+            if (cb != null) {
+                cb.onContentChanged();
+            }
+            if (mDecorContentParent != null) {
+                mDecorContentParent.notifyContentChanged();
+            }
         }
     }
 
diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
index 6832825..ff57fd4 100644
--- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
+++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
@@ -898,6 +898,13 @@
         mDecorToolbar.dismissPopupMenus();
     }
 
+    @Override
+    public void notifyContentChanged() {
+        mLastBaseContentInsets.setEmpty();
+        mLastBaseInnerInsets = WindowInsets.CONSUMED;
+        mLastInnerInsets = WindowInsets.CONSUMED;
+    }
+
     public static class LayoutParams extends MarginLayoutParams {
         public LayoutParams(Context c, AttributeSet attrs) {
             super(c, attrs);
diff --git a/core/java/com/android/internal/widget/DecorContentParent.java b/core/java/com/android/internal/widget/DecorContentParent.java
index ac524f9..8d6cfd1 100644
--- a/core/java/com/android/internal/widget/DecorContentParent.java
+++ b/core/java/com/android/internal/widget/DecorContentParent.java
@@ -22,6 +22,7 @@
 import android.util.SparseArray;
 import android.view.Menu;
 import android.view.Window;
+
 import com.android.internal.view.menu.MenuPresenter;
 
 /**
@@ -49,4 +50,5 @@
     void saveToolbarHierarchyState(SparseArray<Parcelable> toolbarStates);
     void restoreToolbarHierarchyState(SparseArray<Parcelable> toolbarStates);
     void dismissPopups();
+    void notifyContentChanged();
 }
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index 1233069..6a0ec1d 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -126,6 +126,7 @@
         option (android.msg_privacy).dest = DEST_EXPLICIT;
 
         optional SettingProto pointer_fill_style = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto pointer_scale = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
     }
     optional Pointer pointer = 37;
     optional SettingProto pointer_speed = 18 [ (android.privacy).dest = DEST_AUTOMATIC ];
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 09ffdf3..c6db371 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3249,16 +3249,20 @@
     <permission android:name="android.permission.INTERACT_ACROSS_PROFILES"
         android:protectionLevel="signature|appop" />
 
-    <!-- Allows applications to access profiles with ACCESS_HIDDEN_PROFILES user property
-     <p>Protection level: normal
-     @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") -->
+    <!-- Allows applications to access profiles with
+        {@code android.content.pm.UserProperties#PROFILE_API_VISIBILITY_HIDDEN} user property, e.g.
+        {@link android.os.UserManager#USER_TYPE_PROFILE_PRIVATE}.
+        <p>Protection level: normal
+        @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") -->
     <permission android:name="android.permission.ACCESS_HIDDEN_PROFILES"
         android:label="@string/permlab_accessHiddenProfile"
         android:description="@string/permdesc_accessHiddenProfile"
         android:protectionLevel="normal" />
 
-    <!-- @SystemApi @hide Allows privileged applications to get details about hidden profile
-        users.
+    <!-- @SystemApi @hide Allows privileged applications to get details about profiles with
+        {@code android.content.pm.UserProperties#PROFILE_API_VISIBILITY_HIDDEN} user property, e.g.
+        {@link android.os.UserManager#USER_TYPE_PROFILE_PRIVATE}. Removes extra requirements such
+        as having {@link android.app.role.RoleManager#ROLE_HOME} role for LauncherApps APIs.
         @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") -->
     <permission
         android:name="android.permission.ACCESS_HIDDEN_PROFILES_FULL"
@@ -8194,6 +8198,17 @@
     <permission android:name="android.permission.SETUP_FSVERITY"
                 android:protectionLevel="signature|privileged"/>
 
+    <!--
+        @TestApi
+        Signature permission reserved for testing. This should never be used to
+        gate any actual functionality.
+        <p>
+        Protection level: signature
+        @hide
+    -->
+    <permission android:name="android.permission.RESERVED_FOR_TESTING_SIGNATURE"
+                android:protectionLevel="signature"/>
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 16c77d0..ecf4720 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -24,6 +24,7 @@
 import android.app.compat.CompatChanges;
 import android.content.Context;
 import android.hardware.devicestate.DeviceStateManager;
+import android.os.SystemProperties;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -50,6 +51,11 @@
     private static final String TAG = "WindowExtensionsImpl";
 
     /**
+     * The value of the system property that indicates no override is set.
+     */
+    private static final int NO_LEVEL_OVERRIDE = -1;
+
+    /**
      * The min version of the WM Extensions that must be supported in the current platform version.
      */
     @VisibleForTesting
@@ -66,14 +72,30 @@
 
     WindowExtensionsImpl() {
         mIsActivityEmbeddingEnabled = isActivityEmbeddingEnabled();
-        Log.i(TAG, "Initializing Window Extensions, vendor API level=" + mVersion
-                + ", activity embedding enabled=" + mIsActivityEmbeddingEnabled);
+
+        Log.i(TAG, generateLogMessage());
+    }
+
+    private String generateLogMessage() {
+        final StringBuilder logBuilder = new StringBuilder("Initializing Window Extensions, "
+                + "vendor API level=" + mVersion);
+        final int levelOverride = getLevelOverride();
+        if (levelOverride != NO_LEVEL_OVERRIDE) {
+            logBuilder.append(", override to ").append(levelOverride);
+        }
+        logBuilder.append(", activity embedding enabled=").append(mIsActivityEmbeddingEnabled);
+        return logBuilder.toString();
     }
 
     // TODO(b/241126279) Introduce constants to better version functionality
     @Override
     public int getVendorApiLevel() {
-        return mVersion;
+        final int levelOverride = getLevelOverride();
+        return (levelOverride != NO_LEVEL_OVERRIDE) ? levelOverride : mVersion;
+    }
+
+    private int getLevelOverride() {
+        return SystemProperties.getInt("persist.wm.debug.ext_version_override", NO_LEVEL_OVERRIDE);
     }
 
     @NonNull
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index a7da07d..972dce5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -209,7 +209,7 @@
 
             @Override
             public void onDismissBubble(Bubble bubble) {
-                mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED);
+                mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE);
             }
         });
         mHandleView.setOnClickListener(view -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 57e95d6..f4ac5f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -538,8 +538,10 @@
 
                 @Override
                 public void onAnimationStart(Animator animation) {
+                    ValueAnimator valueAnimator = (ValueAnimator) animation;
+                    float value = (float) valueAnimator.getAnimatedValue();
                     SurfaceControl.Transaction t = mTransactionPool.acquire();
-                    t.setPosition(mImeSourceControl.getLeash(), x, startY);
+                    t.setPosition(mImeSourceControl.getLeash(), x, value);
                     if (DEBUG) {
                         Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:"
                                 + imeTop(hiddenY) + "->" + imeTop(shownY)
@@ -549,7 +551,7 @@
                             imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t);
                     mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0;
                     final float alpha = (mAnimateAlpha || isFloating)
-                            ? (startY - hiddenY) / (shownY - hiddenY)
+                            ? (value - hiddenY) / (shownY - hiddenY)
                             : 1.f;
                     t.setAlpha(mImeSourceControl.getLeash(), alpha);
                     if (mAnimationDirection == DIRECTION_SHOW) {
@@ -560,7 +562,7 @@
                     if (DEBUG_IME_VISIBILITY) {
                         EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START,
                                 mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE,
-                                mDisplayId, mAnimationDirection, alpha, startY , endY,
+                                mDisplayId, mAnimationDirection, alpha, value, endY,
                                 Objects.toString(mImeSourceControl.getLeash()),
                                 Objects.toString(mImeSourceControl.getInsetsHint()),
                                 Objects.toString(mImeSourceControl.getSurfacePosition()),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 991fbaf..609e5af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -87,6 +87,7 @@
 import com.android.wm.shell.recents.RecentTasks;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.recents.TaskStackTransitionObserver;
 import com.android.wm.shell.shared.DesktopModeStatus;
 import com.android.wm.shell.shared.ShellTransitions;
 import com.android.wm.shell.shared.annotations.ShellAnimationThread;
@@ -619,12 +620,13 @@
             TaskStackListenerImpl taskStackListener,
             ActivityTaskManager activityTaskManager,
             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+            TaskStackTransitionObserver taskStackTransitionObserver,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
         return Optional.ofNullable(
                 RecentTasksController.create(context, shellInit, shellController,
                         shellCommandHandler, taskStackListener, activityTaskManager,
-                        desktopModeTaskRepository, mainExecutor));
+                        desktopModeTaskRepository, taskStackTransitionObserver, mainExecutor));
     }
 
     @BindsOptionalOf
@@ -924,6 +926,19 @@
     }
 
     //
+    // Task Stack
+    //
+
+    @WMSingleton
+    @Provides
+    static TaskStackTransitionObserver provideTaskStackTransitionObserver(
+            Lazy<Transitions> transitions,
+            ShellInit shellInit
+    ) {
+        return new TaskStackTransitionObserver(transitions, shellInit);
+    }
+
+    //
     // Misc
     //
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 12bbd51..87bd840 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -121,9 +121,9 @@
  */
 @Module(
         includes = {
-            WMShellBaseModule.class,
-            PipModule.class,
-            ShellBackAnimationModule.class,
+                WMShellBaseModule.class,
+                PipModule.class,
+                ShellBackAnimationModule.class,
         })
 public abstract class WMShellModule {
 
@@ -664,7 +664,8 @@
     @Provides
     static Object provideIndependentShellComponentsToCreate(
             DragAndDropController dragAndDropController,
-            Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) {
+            Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional
+    ) {
         return new Object();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
index 62d195e..245829e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
@@ -42,4 +42,7 @@
      * Called when a running task changes.
      */
     void onRunningTaskChanged(in RunningTaskInfo taskInfo);
-}
+
+    /** A task has moved to front. */
+    oneway void onTaskMovedToFront(in RunningTaskInfo taskInfo);
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS
new file mode 100644
index 0000000..452644b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS
@@ -0,0 +1,6 @@
+# WM shell sub-module task stack owners
+uysalorhan@google.com
+samcackett@google.com
+alexchau@google.com
+silvajordan@google.com
+uwaisashraf@google.com
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 9d16246..03c8cf8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -20,6 +20,7 @@
 import static android.content.pm.PackageManager.FEATURE_PC;
 
 import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
+import static com.android.window.flags.Flags.enableTaskStackObserverInShell;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
 
 import android.app.ActivityManager;
@@ -57,6 +58,7 @@
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.SplitBounds;
 
@@ -73,7 +75,8 @@
  * Manages the recent task list from the system, caching it as necessary.
  */
 public class RecentTasksController implements TaskStackListenerCallback,
-        RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener {
+        RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener,
+        TaskStackTransitionObserver.TaskStackTransitionObserverListener {
     private static final String TAG = RecentTasksController.class.getSimpleName();
 
     private final Context mContext;
@@ -84,6 +87,7 @@
     private final TaskStackListenerImpl mTaskStackListener;
     private final RecentTasksImpl mImpl = new RecentTasksImpl();
     private final ActivityTaskManager mActivityTaskManager;
+    private final TaskStackTransitionObserver mTaskStackTransitionObserver;
     private RecentsTransitionHandler mTransitionHandler = null;
     private IRecentTasksListener mListener;
     private final boolean mPcFeatureEnabled;
@@ -112,13 +116,15 @@
             TaskStackListenerImpl taskStackListener,
             ActivityTaskManager activityTaskManager,
             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+            TaskStackTransitionObserver taskStackTransitionObserver,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
         if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
             return null;
         }
         return new RecentTasksController(context, shellInit, shellController, shellCommandHandler,
-                taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor);
+                taskStackListener, activityTaskManager, desktopModeTaskRepository,
+                taskStackTransitionObserver, mainExecutor);
     }
 
     RecentTasksController(Context context,
@@ -128,6 +134,7 @@
             TaskStackListenerImpl taskStackListener,
             ActivityTaskManager activityTaskManager,
             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+            TaskStackTransitionObserver taskStackTransitionObserver,
             ShellExecutor mainExecutor) {
         mContext = context;
         mShellController = shellController;
@@ -136,6 +143,7 @@
         mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
         mTaskStackListener = taskStackListener;
         mDesktopModeTaskRepository = desktopModeTaskRepository;
+        mTaskStackTransitionObserver = taskStackTransitionObserver;
         mMainExecutor = mainExecutor;
         shellInit.addInitCallback(this::onInit, this);
     }
@@ -154,6 +162,10 @@
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mTaskStackListener.addListener(this);
         mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this,
+                    mMainExecutor);
+        }
     }
 
     void setTransitionHandler(RecentsTransitionHandler handler) {
@@ -267,6 +279,12 @@
         notifyRecentTasksChanged();
     }
 
+    @Override
+    public void onTaskMovedToFrontThroughTransition(
+            ActivityManager.RunningTaskInfo runningTaskInfo) {
+        notifyTaskMovedToFront(runningTaskInfo);
+    }
+
     @VisibleForTesting
     void notifyRecentTasksChanged() {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed");
@@ -328,6 +346,19 @@
         }
     }
 
+    private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+        if (mListener == null
+                || !enableTaskStackObserverInShell()
+                || taskInfo.realActivity == null) {
+            return;
+        }
+        try {
+            mListener.onTaskMovedToFront(taskInfo);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed call onTaskMovedToFront", e);
+        }
+    }
+
     private boolean shouldEnableRunningTasksForDesktopMode() {
         return mPcFeatureEnabled
                 || (DesktopModeStatus.canEnterDesktopMode(mContext)
@@ -464,6 +495,7 @@
         }
         return null;
     }
+
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
         pw.println(prefix + TAG);
@@ -547,6 +579,11 @@
             public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onRunningTaskChanged(taskInfo));
             }
+
+            @Override
+            public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+                mListener.call(l -> l.onTaskMovedToFront(taskInfo));
+            }
         };
 
         public IRecentTasksImpl(RecentTasksController controller) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
new file mode 100644
index 0000000..7c5f10a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.recents
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.os.IBinder
+import android.util.ArrayMap
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import com.android.window.flags.Flags.enableTaskStackObserverInShell
+import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import dagger.Lazy
+import java.util.concurrent.Executor
+
+/**
+ * A [Transitions.TransitionObserver] that observes shell transitions and sends updates to listeners
+ * about task stack changes.
+ *
+ * TODO(346588978) Move split/pip signals here as well so that launcher don't need to handle it
+ */
+class TaskStackTransitionObserver(
+    private val transitions: Lazy<Transitions>,
+    shellInit: ShellInit
+) : Transitions.TransitionObserver {
+
+    private val transitionToTransitionChanges: MutableMap<IBinder, TransitionChanges> =
+        mutableMapOf()
+    private val taskStackTransitionObserverListeners =
+        ArrayMap<TaskStackTransitionObserverListener, Executor>()
+
+    init {
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            shellInit.addInitCallback(::onInit, this)
+        }
+    }
+
+    fun onInit() {
+        transitions.get().registerObserver(this)
+    }
+
+    override fun onTransitionReady(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction
+    ) {
+        if (enableTaskStackObserverInShell()) {
+            val taskInfoList = mutableListOf<RunningTaskInfo>()
+            val transitionTypeList = mutableListOf<Int>()
+
+            for (change in info.changes) {
+                if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
+                    continue
+                }
+
+                val taskInfo = change.taskInfo
+                if (taskInfo == null || taskInfo.taskId == -1) {
+                    continue
+                }
+
+                if (change.mode == WindowManager.TRANSIT_OPEN) {
+                    change.taskInfo?.let { taskInfoList.add(it) }
+                    transitionTypeList.add(change.mode)
+                }
+            }
+            transitionToTransitionChanges.put(
+                transition,
+                TransitionChanges(taskInfoList, transitionTypeList)
+            )
+        }
+    }
+
+    override fun onTransitionStarting(transition: IBinder) {}
+
+    override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
+
+    override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
+        val taskInfoList =
+            transitionToTransitionChanges.getOrDefault(transition, TransitionChanges()).taskInfoList
+        val typeList =
+            transitionToTransitionChanges
+                .getOrDefault(transition, TransitionChanges())
+                .transitionTypeList
+        transitionToTransitionChanges.remove(transition)
+
+        for ((index, taskInfo) in taskInfoList.withIndex()) {
+            if (
+                TransitionUtil.isOpeningType(typeList[index]) &&
+                    taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
+            ) {
+                notifyTaskStackTransitionObserverListeners(taskInfo)
+            }
+        }
+    }
+
+    fun addTaskStackTransitionObserverListener(
+        taskStackTransitionObserverListener: TaskStackTransitionObserverListener,
+        executor: Executor
+    ) {
+        taskStackTransitionObserverListeners[taskStackTransitionObserverListener] = executor
+    }
+
+    fun removeTaskStackTransitionObserverListener(
+        taskStackTransitionObserverListener: TaskStackTransitionObserverListener
+    ) {
+        taskStackTransitionObserverListeners.remove(taskStackTransitionObserverListener)
+    }
+
+    private fun notifyTaskStackTransitionObserverListeners(taskInfo: RunningTaskInfo) {
+        taskStackTransitionObserverListeners.forEach { (listener, executor) ->
+            executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) }
+        }
+    }
+
+    /** Listener to use to get updates regarding task stack from this observer */
+    interface TaskStackTransitionObserverListener {
+        /** Called when a task is moved to front. */
+        fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {}
+    }
+
+    private data class TransitionChanges(
+        val taskInfoList: MutableList<RunningTaskInfo> = ArrayList(),
+        val transitionTypeList: MutableList<Int> = ArrayList()
+    )
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
index f813b0d..0fe7a16b 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
@@ -65,9 +65,11 @@
 
 android_test {
     name: "WMShellFlickerTestsSplitScreenGroup2",
+    defaults: ["WMShellFlickerTestsDefault"],
     manifest: "AndroidManifest.xml",
     package_name: "com.android.wm.shell.flicker.splitscreen",
     instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen",
+    test_config_template: "AndroidTestTemplate.xml",
     srcs: [
         ":WMShellFlickerTestsSplitScreenBase-src",
         ":WMShellFlickerTestsSplitScreenGroup2-src",
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt
new file mode 100644
index 0000000..dad5db9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.Presubmit
+import android.tools.Rotation
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.traces.component.ComponentNameMatcher
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitscreen.benchmark.MultipleShowImeRequestsInSplitScreenBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switch between two split pairs.
+ *
+ * To run this test: `atest WMShellFlickerTestsSplitScreenGroup2:MultipleShowImeRequestsInSplitScreen`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class MultipleShowImeRequestsInSplitScreen(override val flicker: LegacyFlickerTest) :
+        MultipleShowImeRequestsInSplitScreenBenchmark(flicker), ICommonAssertions {
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            defaultSetup(this)
+            defaultTeardown(this)
+            thisTransition(this)
+        }
+
+    @Presubmit
+    @Test
+    fun imeLayerAlwaysVisible() =
+            flicker.assertLayers {
+                this.isVisible(ComponentNameMatcher.IME)
+            }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(Rotation.ROTATION_0)
+        )
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
index 9045364..d349988 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
@@ -47,7 +47,7 @@
  * To run this test: `atest WMShellFlickerTestsSplitScreen:UnlockKeyguardToSplitScreen`
  */
 @RequiresDevice
-@Postsubmit
+@FlakyTest(bugId = 293578017)
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
@@ -61,7 +61,6 @@
         }
 
     @Test
-    @FlakyTest(bugId = 293578017)
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt
new file mode 100644
index 0000000..2492531
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.splitscreen.benchmark
+
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+abstract class MultipleShowImeRequestsInSplitScreenBenchmark(
+        override val flicker: LegacyFlickerTest
+) : SplitScreenBase(flicker) {
+    override val primaryApp = ImeAppHelper(instrumentation)
+    override val defaultTeardown: FlickerBuilder.() -> Unit
+        get() = {
+            teardown {
+                primaryApp.closeIME(wmHelper)
+                super.defaultTeardown
+            }
+        }
+
+    protected val thisTransition: FlickerBuilder.() -> Unit
+        get() = {
+            setup {
+                SplitScreenUtils.enterSplit(
+                        wmHelper,
+                        tapl,
+                        device,
+                        primaryApp,
+                        secondaryApp,
+                        flicker.scenario.startRotation
+                )
+                // initially open the IME
+                primaryApp.openIME(wmHelper)
+            }
+            transitions {
+                for (i in 1..OPEN_IME_COUNT) {
+                    primaryApp.openIME(wmHelper)
+                }
+            }
+        }
+
+    companion object {
+        const val OPEN_IME_COUNT = 30
+
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt
index 4b10603..51074f6 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt
@@ -25,7 +25,7 @@
 
 abstract class SplitScreenBase(flicker: LegacyFlickerTest) : BaseBenchmarkTest(flicker) {
     protected val context: Context = instrumentation.context
-    protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+    protected open val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
     protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
 
     protected open val defaultSetup: FlickerBuilder.() -> Unit = {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index 5f6132a..0d3cd10 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -40,14 +40,14 @@
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
 import com.android.wm.shell.shared.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.TransitionInfoBuilder
@@ -78,409 +78,397 @@
 @RunWith(AndroidTestingRunner::class)
 class DesktopModeLoggerTransitionObserverTest {
 
-    @JvmField
-    @Rule
-    val extendedMockitoRule =
-        ExtendedMockitoRule.Builder(this)
-            .mockStatic(DesktopModeEventLogger::class.java)
-            .mockStatic(DesktopModeStatus::class.java)
-            .build()!!
+  @JvmField
+  @Rule
+  val extendedMockitoRule =
+      ExtendedMockitoRule.Builder(this)
+          .mockStatic(DesktopModeEventLogger::class.java)
+          .mockStatic(DesktopModeStatus::class.java)
+          .build()!!
 
-    @Mock lateinit var testExecutor: ShellExecutor
-    @Mock private lateinit var mockShellInit: ShellInit
-    @Mock private lateinit var transitions: Transitions
-    @Mock private lateinit var context: Context
+  @Mock lateinit var testExecutor: ShellExecutor
+  @Mock private lateinit var mockShellInit: ShellInit
+  @Mock private lateinit var transitions: Transitions
+  @Mock private lateinit var context: Context
 
-    private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
-    private lateinit var shellInit: ShellInit
-    private lateinit var desktopModeEventLogger: DesktopModeEventLogger
+  private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
+  private lateinit var shellInit: ShellInit
+  private lateinit var desktopModeEventLogger: DesktopModeEventLogger
 
-    @Before
-    fun setup() {
-        doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
-        shellInit = Mockito.spy(ShellInit(testExecutor))
-        desktopModeEventLogger = mock(DesktopModeEventLogger::class.java)
+  @Before
+  fun setup() {
+    doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
+    shellInit = Mockito.spy(ShellInit(testExecutor))
+    desktopModeEventLogger = mock(DesktopModeEventLogger::class.java)
 
-        transitionObserver =
-            DesktopModeLoggerTransitionObserver(
-                context,
-                mockShellInit,
-                transitions,
-                desktopModeEventLogger
-            )
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
-            verify(mockShellInit)
-                .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
-            initRunnableCaptor.value.run()
-        } else {
-            transitionObserver.onInit()
-        }
+    transitionObserver =
+        DesktopModeLoggerTransitionObserver(
+            context, mockShellInit, transitions, desktopModeEventLogger)
+    if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+      val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+      verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
+      initRunnableCaptor.value.run()
+    } else {
+      transitionObserver.onInit()
+    }
+  }
+
+  @Test
+  fun testRegistersObserverAtInit() {
+    verify(transitions).registerObserver(same(transitionObserver))
+  }
+
+  @Test
+  fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() {
+    val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+    val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+    verify(desktopModeEventLogger, never()).logTaskAdded(any(), any())
+  }
+
+  @Test
+  fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() {
+    val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+    callOnTransitionReady(transitionInfo)
+    val sessionId = transitionObserver.getLoggerSessionId()
+
+    assertThat(sessionId).isNotNull()
+    verify(desktopModeEventLogger, times(1))
+        .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FREEFORM_INTENT))
+    verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    verifyZeroInteractions(desktopModeEventLogger)
+  }
+
+  @Test
+  fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() {
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    // task change is finalised when drag ends
+    val transitionInfo =
+        TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0)
+            .addChange(change)
+            .build()
+
+    callOnTransitionReady(transitionInfo)
+    val sessionId = transitionObserver.getLoggerSessionId()
+
+    assertThat(sessionId).isNotNull()
+    verify(desktopModeEventLogger, times(1))
+        .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_DRAG))
+    verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    verifyZeroInteractions(desktopModeEventLogger)
+  }
+
+  @Test
+  fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() {
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    val transitionInfo =
+        TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0)
+            .addChange(change)
+            .build()
+
+    callOnTransitionReady(transitionInfo)
+    val sessionId = transitionObserver.getLoggerSessionId()
+
+    assertThat(sessionId).isNotNull()
+    verify(desktopModeEventLogger, times(1))
+        .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_MENU_BUTTON))
+    verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    verifyZeroInteractions(desktopModeEventLogger)
+  }
+
+  @Test
+  fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonUnknown() {
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    val transitionInfo =
+        TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
+            .addChange(change)
+            .build()
+
+    callOnTransitionReady(transitionInfo)
+    val sessionId = transitionObserver.getLoggerSessionId()
+
+    assertThat(sessionId).isNotNull()
+    verify(desktopModeEventLogger, times(1))
+        .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW))
+    verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    verifyZeroInteractions(desktopModeEventLogger)
+  }
+
+  @Test
+  fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() {
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    val transitionInfo =
+        TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0)
+            .addChange(change)
+            .build()
+
+    callOnTransitionReady(transitionInfo)
+    val sessionId = transitionObserver.getLoggerSessionId()
+
+    assertThat(sessionId).isNotNull()
+    verify(desktopModeEventLogger, times(1))
+        .logSessionEnter(eq(sessionId!!), eq(EnterReason.KEYBOARD_SHORTCUT_ENTER))
+    verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    verifyZeroInteractions(desktopModeEventLogger)
+  }
+
+  @Test
+  fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() {
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    val transitionInfo =
+        TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build()
+
+    callOnTransitionReady(transitionInfo)
+    val sessionId = transitionObserver.getLoggerSessionId()
+
+    assertThat(sessionId).isNotNull()
+    verify(desktopModeEventLogger, times(1))
+        .logSessionEnter(eq(sessionId!!), eq(EnterReason.UNKNOWN_ENTER))
+    verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    verifyZeroInteractions(desktopModeEventLogger)
+  }
+
+  @Test
+  fun transitWake_logTaskAddedAndEnterReasonScreenOn() {
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build()
+
+    callOnTransitionReady(transitionInfo)
+    val sessionId = transitionObserver.getLoggerSessionId()
+
+    assertThat(sessionId).isNotNull()
+    verify(desktopModeEventLogger, times(1))
+        .logSessionEnter(eq(sessionId!!), eq(EnterReason.SCREEN_ON))
+    verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    verifyZeroInteractions(desktopModeEventLogger)
+  }
+
+  @Test
+  fun transitSleep_logTaskAddedAndExitReasonScreenOff_sessionIdNull() {
+    val sessionId = 1
+    // add a freeform task
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+    verify(desktopModeEventLogger, times(1))
+        .logSessionExit(eq(sessionId), eq(ExitReason.SCREEN_OFF))
+    verifyZeroInteractions(desktopModeEventLogger)
+    assertThat(transitionObserver.getLoggerSessionId()).isNull()
+  }
+
+  @Test
+  fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit_sessionIdNull() {
+    val sessionId = 1
+    // add a freeform task
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    // window mode changing from FREEFORM to FULLSCREEN
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+    val transitionInfo =
+        TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build()
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+    verify(desktopModeEventLogger, times(1))
+        .logSessionExit(eq(sessionId), eq(ExitReason.DRAG_TO_EXIT))
+    verifyZeroInteractions(desktopModeEventLogger)
+    assertThat(transitionObserver.getLoggerSessionId()).isNull()
+  }
+
+  @Test
+  fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton_sessionIdNull() {
+    val sessionId = 1
+    // add a freeform task
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    // window mode changing from FREEFORM to FULLSCREEN
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+    val transitionInfo =
+        TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON)
+            .addChange(change)
+            .build()
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+    verify(desktopModeEventLogger, times(1))
+        .logSessionExit(eq(sessionId), eq(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT))
+    verifyZeroInteractions(desktopModeEventLogger)
+    assertThat(transitionObserver.getLoggerSessionId()).isNull()
+  }
+
+  @Test
+  fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard_sessionIdNull() {
+    val sessionId = 1
+    // add a freeform task
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    // window mode changing from FREEFORM to FULLSCREEN
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+    val transitionInfo =
+        TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT).addChange(change).build()
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+    verify(desktopModeEventLogger, times(1))
+        .logSessionExit(eq(sessionId), eq(ExitReason.KEYBOARD_SHORTCUT_EXIT))
+    verifyZeroInteractions(desktopModeEventLogger)
+    assertThat(transitionObserver.getLoggerSessionId()).isNull()
+  }
+
+  @Test
+  fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown_sessionIdNull() {
+    val sessionId = 1
+    // add a freeform task
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    // window mode changing from FREEFORM to FULLSCREEN
+    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+    val transitionInfo =
+        TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build()
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+    verify(desktopModeEventLogger, times(1))
+        .logSessionExit(eq(sessionId), eq(ExitReason.UNKNOWN_EXIT))
+    verifyZeroInteractions(desktopModeEventLogger)
+    assertThat(transitionObserver.getLoggerSessionId()).isNull()
+  }
+
+  @Test
+  fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview_sessionIdNull() {
+    val sessionId = 1
+    // add a freeform task
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    // recents transition
+    val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    val transitionInfo =
+        TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build()
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+    verify(desktopModeEventLogger, times(1))
+        .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+    verifyZeroInteractions(desktopModeEventLogger)
+    assertThat(transitionObserver.getLoggerSessionId()).isNull()
+  }
+
+  @Test
+  fun transitClose_logTaskRemovedAndExitReasonTaskFinished_sessionIdNull() {
+    val sessionId = 1
+    // add a freeform task
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    // task closing
+    val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+    val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+    verify(desktopModeEventLogger, times(1))
+        .logSessionExit(eq(sessionId), eq(ExitReason.TASK_FINISHED))
+    verifyZeroInteractions(desktopModeEventLogger)
+    assertThat(transitionObserver.getLoggerSessionId()).isNull()
+  }
+
+  @Test
+  fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
+    val sessionId = 1
+    // add a freeform task to an existing session
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    // recents transition sent freeform window to back
+    val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    val transitionInfo1 =
+        TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build()
+    callOnTransitionReady(transitionInfo1)
+    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+    verify(desktopModeEventLogger, times(1))
+        .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+    assertThat(transitionObserver.getLoggerSessionId()).isNull()
+
+    val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
+    callOnTransitionReady(transitionInfo2)
+
+    verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any())
+    verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any())
+  }
+
+  @Test
+  fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
+    val sessionId = 1
+    // add an existing freeform task
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    // new freeform task added
+    val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+    val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+    verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+  }
+
+  @Test
+  fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
+    val sessionId = 1
+    // add two existing freeform tasks
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+    transitionObserver.setLoggerSessionId(sessionId)
+
+    // new freeform task added
+    val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+    val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
+    callOnTransitionReady(transitionInfo)
+
+    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+    verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
+  }
+
+  /** Simulate calling the onTransitionReady() method */
+  private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+    val transition = mock(IBinder::class.java)
+    val startT = mock(SurfaceControl.Transaction::class.java)
+    val finishT = mock(SurfaceControl.Transaction::class.java)
+
+    transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
+  }
+
+  companion object {
+    fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo {
+      val taskInfo = ActivityManager.RunningTaskInfo()
+      taskInfo.taskId = taskId
+      taskInfo.configuration.windowConfiguration.windowingMode = windowMode
+
+      return taskInfo
     }
 
-    @Test
-    fun testRegistersObserverAtInit() {
-        verify(transitions).registerObserver(same(transitionObserver))
+    fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
+      val change =
+          Change(
+              WindowContainerToken(mock(IWindowContainerToken::class.java)),
+              mock(SurfaceControl::class.java))
+      change.mode = mode
+      change.taskInfo = taskInfo
+      return change
     }
-
-    @Test
-    fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() {
-        val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
-        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
-        verify(desktopModeEventLogger, never()).logTaskAdded(any(), any())
-    }
-
-    @Test
-    fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() {
-        val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-
-        callOnTransitionReady(transitionInfo)
-        val sessionId = transitionObserver.getLoggerSessionId()
-
-        assertThat(sessionId).isNotNull()
-        verify(desktopModeEventLogger, times(1))
-            .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FREEFORM_INTENT))
-        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
-        verifyZeroInteractions(desktopModeEventLogger)
-    }
-
-    @Test
-    fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() {
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        // task change is finalised when drag ends
-        val transitionInfo =
-            TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0)
-                .addChange(change)
-                .build()
-
-        callOnTransitionReady(transitionInfo)
-        val sessionId = transitionObserver.getLoggerSessionId()
-
-        assertThat(sessionId).isNotNull()
-        verify(desktopModeEventLogger, times(1))
-            .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_DRAG))
-        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
-        verifyZeroInteractions(desktopModeEventLogger)
-    }
-
-    @Test
-    fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() {
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        val transitionInfo =
-            TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0)
-                .addChange(change)
-                .build()
-
-        callOnTransitionReady(transitionInfo)
-        val sessionId = transitionObserver.getLoggerSessionId()
-
-        assertThat(sessionId).isNotNull()
-        verify(desktopModeEventLogger, times(1))
-            .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_MENU_BUTTON))
-        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
-        verifyZeroInteractions(desktopModeEventLogger)
-    }
-
-    @Test
-    fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonUnknown() {
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        val transitionInfo =
-            TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
-                .addChange(change)
-                .build()
-
-        callOnTransitionReady(transitionInfo)
-        val sessionId = transitionObserver.getLoggerSessionId()
-
-        assertThat(sessionId).isNotNull()
-        verify(desktopModeEventLogger, times(1))
-            .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW))
-        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
-        verifyZeroInteractions(desktopModeEventLogger)
-    }
-
-    @Test
-    fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() {
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        val transitionInfo =
-            TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0)
-                .addChange(change)
-                .build()
-
-        callOnTransitionReady(transitionInfo)
-        val sessionId = transitionObserver.getLoggerSessionId()
-
-        assertThat(sessionId).isNotNull()
-        verify(desktopModeEventLogger, times(1))
-            .logSessionEnter(eq(sessionId!!), eq(EnterReason.KEYBOARD_SHORTCUT_ENTER))
-        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
-        verifyZeroInteractions(desktopModeEventLogger)
-    }
-
-    @Test
-    fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() {
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        val transitionInfo =
-            TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build()
-
-        callOnTransitionReady(transitionInfo)
-        val sessionId = transitionObserver.getLoggerSessionId()
-
-        assertThat(sessionId).isNotNull()
-        verify(desktopModeEventLogger, times(1))
-            .logSessionEnter(eq(sessionId!!), eq(EnterReason.UNKNOWN_ENTER))
-        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
-        verifyZeroInteractions(desktopModeEventLogger)
-    }
-
-    @Test
-    fun transitWake_logTaskAddedAndEnterReasonScreenOn() {
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build()
-
-        callOnTransitionReady(transitionInfo)
-        val sessionId = transitionObserver.getLoggerSessionId()
-
-        assertThat(sessionId).isNotNull()
-        verify(desktopModeEventLogger, times(1))
-            .logSessionEnter(eq(sessionId!!), eq(EnterReason.SCREEN_ON))
-        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
-        verifyZeroInteractions(desktopModeEventLogger)
-    }
-
-    @Test
-    fun transitSleep_logTaskAddedAndExitReasonScreenOff_sessionIdNull() {
-        val sessionId = 1
-        // add a freeform task
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
-        verify(desktopModeEventLogger, times(1))
-            .logSessionExit(eq(sessionId), eq(ExitReason.SCREEN_OFF))
-        verifyZeroInteractions(desktopModeEventLogger)
-        assertThat(transitionObserver.getLoggerSessionId()).isNull()
-    }
-
-    @Test
-    fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit_sessionIdNull() {
-        val sessionId = 1
-        // add a freeform task
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        // window mode changing from FREEFORM to FULLSCREEN
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
-        val transitionInfo =
-            TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build()
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
-        verify(desktopModeEventLogger, times(1))
-            .logSessionExit(eq(sessionId), eq(ExitReason.DRAG_TO_EXIT))
-        verifyZeroInteractions(desktopModeEventLogger)
-        assertThat(transitionObserver.getLoggerSessionId()).isNull()
-    }
-
-    @Test
-    fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton_sessionIdNull() {
-        val sessionId = 1
-        // add a freeform task
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        // window mode changing from FREEFORM to FULLSCREEN
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
-        val transitionInfo =
-            TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON)
-                .addChange(change)
-                .build()
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
-        verify(desktopModeEventLogger, times(1))
-            .logSessionExit(eq(sessionId), eq(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT))
-        verifyZeroInteractions(desktopModeEventLogger)
-        assertThat(transitionObserver.getLoggerSessionId()).isNull()
-    }
-
-    @Test
-    fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard_sessionIdNull() {
-        val sessionId = 1
-        // add a freeform task
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        // window mode changing from FREEFORM to FULLSCREEN
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
-        val transitionInfo =
-            TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT)
-                .addChange(change)
-                .build()
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
-        verify(desktopModeEventLogger, times(1))
-            .logSessionExit(eq(sessionId), eq(ExitReason.KEYBOARD_SHORTCUT_EXIT))
-        verifyZeroInteractions(desktopModeEventLogger)
-        assertThat(transitionObserver.getLoggerSessionId()).isNull()
-    }
-
-    @Test
-    fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown_sessionIdNull() {
-        val sessionId = 1
-        // add a freeform task
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        // window mode changing from FREEFORM to FULLSCREEN
-        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
-        val transitionInfo =
-            TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build()
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
-        verify(desktopModeEventLogger, times(1))
-            .logSessionExit(eq(sessionId), eq(ExitReason.UNKNOWN_EXIT))
-        verifyZeroInteractions(desktopModeEventLogger)
-        assertThat(transitionObserver.getLoggerSessionId()).isNull()
-    }
-
-    @Test
-    fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview_sessionIdNull() {
-        val sessionId = 1
-        // add a freeform task
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        // recents transition
-        val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        val transitionInfo =
-            TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
-                .addChange(change)
-                .build()
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
-        verify(desktopModeEventLogger, times(1))
-            .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
-        verifyZeroInteractions(desktopModeEventLogger)
-        assertThat(transitionObserver.getLoggerSessionId()).isNull()
-    }
-
-    @Test
-    fun transitClose_logTaskRemovedAndExitReasonTaskFinished_sessionIdNull() {
-        val sessionId = 1
-        // add a freeform task
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        // task closing
-        val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
-        val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
-        verify(desktopModeEventLogger, times(1))
-            .logSessionExit(eq(sessionId), eq(ExitReason.TASK_FINISHED))
-        verifyZeroInteractions(desktopModeEventLogger)
-        assertThat(transitionObserver.getLoggerSessionId()).isNull()
-    }
-
-    @Test
-    fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
-        val sessionId = 1
-        // add a freeform task to an existing session
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        // recents transition sent freeform window to back
-        val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        val transitionInfo1 =
-            TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
-                .addChange(change)
-                .build()
-        callOnTransitionReady(transitionInfo1)
-        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
-        verify(desktopModeEventLogger, times(1))
-            .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
-        assertThat(transitionObserver.getLoggerSessionId()).isNull()
-
-        val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
-        callOnTransitionReady(transitionInfo2)
-
-        verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any())
-        verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any())
-    }
-
-    @Test
-    fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
-        val sessionId = 1
-        // add an existing freeform task
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        // new freeform task added
-        val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
-        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
-        verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
-    }
-
-    @Test
-    fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
-        val sessionId = 1
-        // add two existing freeform tasks
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
-        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM))
-        transitionObserver.setLoggerSessionId(sessionId)
-
-        // new freeform task added
-        val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
-        val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
-        callOnTransitionReady(transitionInfo)
-
-        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
-        verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
-    }
-
-    /** Simulate calling the onTransitionReady() method */
-    private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
-        val transition = mock(IBinder::class.java)
-        val startT = mock(SurfaceControl.Transaction::class.java)
-        val finishT = mock(SurfaceControl.Transaction::class.java)
-
-        transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
-    }
-
-    companion object {
-        fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo {
-            val taskInfo = ActivityManager.RunningTaskInfo()
-            taskInfo.taskId = taskId
-            taskInfo.configuration.windowConfiguration.windowingMode = windowMode
-
-            return taskInfo
-        }
-
-        fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
-            val change =
-                Change(
-                    WindowContainerToken(mock(IWindowContainerToken::class.java)),
-                    mock(SurfaceControl::class.java)
-                )
-            change.mode = mode
-            change.taskInfo = taskInfo
-            return change
-        }
-    }
+  }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 56c4cea..e291c0e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -113,6 +113,8 @@
     private DisplayInsetsController mDisplayInsetsController;
     @Mock
     private IRecentTasksListener mRecentTasksListener;
+    @Mock
+    private TaskStackTransitionObserver mTaskStackTransitionObserver;
 
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -139,7 +141,8 @@
                 mDisplayInsetsController, mMainExecutor));
         mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit,
                 mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
-                Optional.of(mDesktopModeTaskRepository), mMainExecutor);
+                Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver,
+                mMainExecutor);
         mRecentTasksController = spy(mRecentTasksControllerReal);
         mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
                 null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
@@ -557,6 +560,30 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    public void onTaskMovedToFront_TaskStackObserverEnabled_triggersOnTaskMovedToFront()
+            throws Exception {
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+        ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+        mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo);
+
+        verify(mRecentTasksListener).onTaskMovedToFront(taskInfo);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    public void onTaskMovedToFront_TaskStackObserverEnabled_doesNotTriggersOnTaskMovedToFront()
+            throws Exception {
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+        ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+        mRecentTasksControllerReal.onTaskMovedToFront(taskInfo);
+
+        verify(mRecentTasksListener, never()).onTaskMovedToFront(any());
+    }
+
+    @Test
     public void getNullSplitBoundsNonSplitTask() {
         SplitBounds sb = mRecentTasksController.getSplitBoundsForTaskId(3);
         assertNull("splitBounds should be null for non-split task", sb);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
new file mode 100644
index 0000000..f959970
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.recents
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.os.IBinder
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.IWindowContainerToken
+import android.window.TransitionInfo
+import android.window.WindowContainerToken
+import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.TransitionInfoBuilder
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+
+/**
+ * Test class for {@link TaskStackTransitionObserver}
+ *
+ * Usage: atest WMShellUnitTests:TaskStackTransitionObserverTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TaskStackTransitionObserverTest {
+
+    @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
+    @Mock private lateinit var shellInit: ShellInit
+    @Mock lateinit var testExecutor: ShellExecutor
+    @Mock private lateinit var transitionsLazy: Lazy<Transitions>
+    @Mock private lateinit var transitions: Transitions
+    @Mock private lateinit var mockTransitionBinder: IBinder
+
+    private lateinit var transitionObserver: TaskStackTransitionObserver
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        shellInit = Mockito.spy(ShellInit(testExecutor))
+        whenever(transitionsLazy.get()).thenReturn(transitions)
+        transitionObserver = TaskStackTransitionObserver(transitionsLazy, shellInit)
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+            verify(shellInit)
+                .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
+            initRunnableCaptor.value.run()
+        } else {
+            transitionObserver.onInit()
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    fun testRegistersObserverAtInit() {
+        verify(transitions).registerObserver(same(transitionObserver))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    fun taskCreated_freeformWindow_listenerNotified() {
+        val listener = TestListener()
+        val executor = TestShellExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+        val change =
+            createChange(
+                WindowManager.TRANSIT_OPEN,
+                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+            )
+        val transitionInfo =
+            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+        callOnTransitionFinished()
+        executor.flushAll()
+
+        assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId)
+        assertThat(listener.taskInfoToBeNotified.windowingMode)
+            .isEqualTo(change.taskInfo?.windowingMode)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    fun taskCreated_fullscreenWindow_listenerNotNotified() {
+        val listener = TestListener()
+        val executor = TestShellExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+        val change =
+            createChange(
+                WindowManager.TRANSIT_OPEN,
+                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+            )
+        val transitionInfo =
+            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+        callOnTransitionFinished()
+        executor.flushAll()
+
+        assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(0)
+        assertThat(listener.taskInfoToBeNotified.windowingMode)
+            .isEqualTo(WindowConfiguration.WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    fun taskCreated_freeformWindowOnTopOfFreeform_listenerNotified() {
+        val listener = TestListener()
+        val executor = TestShellExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+        val freeformOpenChange =
+            createChange(
+                WindowManager.TRANSIT_OPEN,
+                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+            )
+        val freeformReorderChange =
+            createChange(
+                WindowManager.TRANSIT_TO_BACK,
+                createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+            )
+        val transitionInfo =
+            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0)
+                .addChange(freeformOpenChange)
+                .addChange(freeformReorderChange)
+                .build()
+
+        callOnTransitionReady(transitionInfo)
+        callOnTransitionFinished()
+        executor.flushAll()
+
+        assertThat(listener.taskInfoToBeNotified.taskId)
+            .isEqualTo(freeformOpenChange.taskInfo?.taskId)
+        assertThat(listener.taskInfoToBeNotified.windowingMode)
+            .isEqualTo(freeformOpenChange.taskInfo?.windowingMode)
+    }
+
+    class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener {
+        var taskInfoToBeNotified = ActivityManager.RunningTaskInfo()
+
+        override fun onTaskMovedToFrontThroughTransition(
+            taskInfo: ActivityManager.RunningTaskInfo
+        ) {
+            taskInfoToBeNotified = taskInfo
+        }
+    }
+
+    /** Simulate calling the onTransitionReady() method */
+    private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+        val startT = Mockito.mock(SurfaceControl.Transaction::class.java)
+        val finishT = Mockito.mock(SurfaceControl.Transaction::class.java)
+
+        transitionObserver.onTransitionReady(mockTransitionBinder, transitionInfo, startT, finishT)
+    }
+
+    /** Simulate calling the onTransitionFinished() method */
+    private fun callOnTransitionFinished() {
+        transitionObserver.onTransitionFinished(mockTransitionBinder, false)
+    }
+
+    companion object {
+        fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo {
+            val taskInfo = ActivityManager.RunningTaskInfo()
+            taskInfo.taskId = taskId
+            taskInfo.configuration.windowConfiguration.windowingMode = windowingMode
+
+            return taskInfo
+        }
+
+        fun createChange(
+            mode: Int,
+            taskInfo: ActivityManager.RunningTaskInfo
+        ): TransitionInfo.Change {
+            val change =
+                TransitionInfo.Change(
+                    WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)),
+                    Mockito.mock(SurfaceControl::class.java)
+                )
+            change.mode = mode
+            change.taskInfo = taskInfo
+            return change
+        }
+    }
+}
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 055ccbc..3375e18c 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -57,7 +57,9 @@
 
   @FlaggedApi("android.nfc.nfc_oem_extension") public final class NfcOemExtension {
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void clearPreference();
+    method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
+    method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void synchronizeScreenState();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void unregisterCallback(@NonNull android.nfc.NfcOemExtension.Callback);
   }
 
diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl
index 7150b54..fd77820 100644
--- a/nfc/java/android/nfc/INfcAdapter.aidl
+++ b/nfc/java/android/nfc/INfcAdapter.aidl
@@ -110,4 +110,6 @@
     void registerOemExtensionCallback(INfcOemExtensionCallback callbacks);
     void unregisterOemExtensionCallback(INfcOemExtensionCallback callbacks);
     void clearPreference();
+    void setScreenState();
+    void checkFirmware();
 }
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index 1eff58c..f6138a6 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -141,6 +141,34 @@
         }
     }
 
+    /**
+     * Get the screen state from system and set it to current screen state.
+     */
+    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+    public void synchronizeScreenState() {
+        try {
+            NfcAdapter.sService.setScreenState();
+        } catch (RemoteException e) {
+            mAdapter.attemptDeadServiceRecovery(e);
+        }
+    }
+
+    /**
+     * Check if the firmware needs updating.
+     *
+     * <p>If an update is needed, a firmware will be triggered when NFC is disabled.
+     */
+    @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+    @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+    public void maybeTriggerFirmwareUpdate() {
+        try {
+            NfcAdapter.sService.checkFirmware();
+        } catch (RemoteException e) {
+            mAdapter.attemptDeadServiceRecovery(e);
+        }
+    }
+
     private final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub {
         @Override
         public void onTagConnected(boolean connected, Tag tag) throws RemoteException {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index ed964a9..b3e48b2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -209,44 +209,34 @@
                 CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice);
                 if (mainDevice != null) {
                     if (mainDevice.isConnected()) {
-                        // When main device exists and in connected state, receiving sub device
-                        // connection. To refresh main device UI
+                        // Sub/member device is connected and main device is connected
+                        // To refresh main device UI
                         mainDevice.refresh();
                     } else {
-                        // When both Hearing Aid devices are disconnected, receiving sub device
-                        // connection. To switch content and dispatch to notify UI change
-                        mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
-                        mainDevice.switchSubDeviceContent();
-                        mainDevice.refresh();
-                        // It is necessary to do remove and add for updating the mapping on
-                        // preference and device
-                        mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
+                        // Sub/member device is connected and main device is disconnected
+                        // To switch content and dispatch to notify UI change
+                        switchDeviceContent(mainDevice, cachedDevice);
                     }
                     return true;
                 }
                 break;
             case BluetoothProfile.STATE_DISCONNECTED:
-                mainDevice = findMainDevice(cachedDevice);
                 if (cachedDevice.getUnpairing()) {
                     return true;
                 }
+                mainDevice = findMainDevice(cachedDevice);
                 if (mainDevice != null) {
-                    // When main device exists, receiving sub device disconnection
+                    // Sub/member device is disconnected and main device exists
                     // To update main device UI
                     mainDevice.refresh();
                     return true;
                 }
-                CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
-                if (subDevice != null && subDevice.isConnected()) {
-                    // Main device is disconnected and sub device is connected
-                    // To copy data from sub device to main device
-                    mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
-                    cachedDevice.switchSubDeviceContent();
-                    cachedDevice.refresh();
-                    // It is necessary to do remove and add for updating the mapping on
-                    // preference and device
-                    mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice);
-
+                CachedBluetoothDevice connectedSecondaryDevice = getConnectedSecondaryDevice(
+                        cachedDevice);
+                if (connectedSecondaryDevice != null) {
+                    // Main device is disconnected and sub/member device is connected
+                    // To switch content and dispatch to notify UI change
+                    switchDeviceContent(cachedDevice, connectedSecondaryDevice);
                     return true;
                 }
                 break;
@@ -254,6 +244,29 @@
         return false;
     }
 
+    private void switchDeviceContent(CachedBluetoothDevice mainDevice,
+            CachedBluetoothDevice secondaryDevice) {
+        mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
+        if (mainDevice.getSubDevice() != null
+                && mainDevice.getSubDevice().equals(secondaryDevice)) {
+            mainDevice.switchSubDeviceContent();
+        } else {
+            mainDevice.switchMemberDeviceContent(secondaryDevice);
+        }
+        mainDevice.refresh();
+        // It is necessary to do remove and add for updating the mapping on
+        // preference and device
+        mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
+    }
+
+    private CachedBluetoothDevice getConnectedSecondaryDevice(CachedBluetoothDevice cachedDevice) {
+        if (cachedDevice.getSubDevice() != null && cachedDevice.getSubDevice().isConnected()) {
+            return cachedDevice.getSubDevice();
+        }
+        return cachedDevice.getMemberDevice().stream().filter(
+                CachedBluetoothDevice::isConnected).findAny().orElse(null);
+    }
+
     void onActiveDeviceChanged(CachedBluetoothDevice device) {
         if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
             if (device.isActiveDevice(BluetoothProfile.HEARING_AID) || device.isActiveDevice(
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 4188d2e..bf927a1 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
@@ -681,6 +681,53 @@
         verify(mCachedDevice1).refresh();
     }
 
+
+    /**
+     * Test onProfileConnectionStateChangedIfProcessed.
+     * When main device is disconnected, to verify switch() result for member device connected
+     * event
+     */
+    @Test
+    public void onProfileConnectionStateChanged_connect_member_mainDisconnected_switch() {
+        when(mCachedDevice1.isConnected()).thenReturn(false);
+        when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+        when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_1);
+        mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+        mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+        assertThat(mCachedDevice1.mDevice).isEqualTo(mDevice1);
+        assertThat(mCachedDevice2.mDevice).isEqualTo(mDevice2);
+        assertThat(mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(
+                mCachedDevice2, BluetoothProfile.STATE_CONNECTED)).isTrue();
+
+        assertThat(mCachedDevice1.mDevice).isEqualTo(mDevice2);
+        assertThat(mCachedDevice2.mDevice).isEqualTo(mDevice1);
+        verify(mCachedDevice1).refresh();
+    }
+
+    /**
+     * Test onProfileConnectionStateChangedIfProcessed.
+     * When member device is connected, to verify switch() result for main device disconnected
+     * event
+     */
+    @Test
+    public void onProfileConnectionStateChanged_disconnect_main_subDeviceConnected_switch() {
+        when(mCachedDevice2.isConnected()).thenReturn(true);
+        when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+        when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_1);
+        mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+        mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+        assertThat(mCachedDevice1.mDevice).isEqualTo(mDevice1);
+        assertThat(mCachedDevice2.mDevice).isEqualTo(mDevice2);
+        assertThat(mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(
+                mCachedDevice1, BluetoothProfile.STATE_DISCONNECTED)).isTrue();
+
+        assertThat(mCachedDevice1.mDevice).isEqualTo(mDevice2);
+        assertThat(mCachedDevice2.mDevice).isEqualTo(mDevice1);
+        verify(mCachedDevice1).refresh();
+    }
+
     @Test
     public void onActiveDeviceChanged_connected_callSetStrategies() {
         when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn(
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 5245456..00fb7a1 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -80,6 +80,7 @@
                 Settings.System.SIP_RECEIVE_CALLS,
                 Settings.System.POINTER_SPEED,
                 Settings.System.POINTER_FILL_STYLE,
+                Settings.System.POINTER_SCALE,
                 Settings.System.VIBRATE_ON,
                 Settings.System.VIBRATE_WHEN_RINGING,
                 Settings.System.RINGTONE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 2c3be4c..4235bc4 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -26,6 +26,8 @@
 import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_INTEGER_VALIDATOR;
 import static android.provider.settings.validators.SettingsValidators.URI_VALIDATOR;
 import static android.provider.settings.validators.SettingsValidators.VIBRATION_INTENSITY_VALIDATOR;
+import static android.view.PointerIcon.DEFAULT_POINTER_SCALE;
+import static android.view.PointerIcon.LARGE_POINTER_SCALE;
 import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BEGIN;
 import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_END;
 
@@ -211,6 +213,8 @@
         VALIDATORS.put(System.POINTER_FILL_STYLE,
                 new InclusiveIntegerRangeValidator(POINTER_ICON_VECTOR_STYLE_FILL_BEGIN,
                         POINTER_ICON_VECTOR_STYLE_FILL_END));
+        VALIDATORS.put(System.POINTER_SCALE,
+                new InclusiveFloatRangeValidator(DEFAULT_POINTER_SCALE, LARGE_POINTER_SCALE));
         VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
         VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 625b8e4..384cb7e 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2915,6 +2915,9 @@
         dumpSetting(s, p,
                 Settings.System.POINTER_FILL_STYLE,
                 SystemSettingsProto.Pointer.POINTER_FILL_STYLE);
+        dumpSetting(s, p,
+                Settings.System.POINTER_SCALE,
+                SystemSettingsProto.Pointer.POINTER_SCALE);
         p.end(pointerToken);
         dumpSetting(s, p,
                 Settings.System.POINTER_SPEED,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index a1f8f1b..c63389b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -252,7 +252,10 @@
     Box(
         Modifier.matchParentSize()
             .background(colors.primary)
-            .animatedRadialGradientBackground(colors.primary, colors.primaryContainer)
+            .animatedRadialGradientBackground(
+                toColor = colors.primary,
+                fromColor = colors.primaryContainer.copy(alpha = 0.6f)
+            )
     )
     BackgroundTopScrim()
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 10c4030..68395b4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -59,6 +59,13 @@
         goneToShadeTransition(durationScale = 0.9)
     }
     from(Scenes.Gone, to = Scenes.QuickSettings) { goneToQuickSettingsTransition() }
+    from(
+        Scenes.Gone,
+        to = Scenes.QuickSettings,
+        key = SlightlyFasterShadeCollapse,
+    ) {
+        goneToQuickSettingsTransition(durationScale = 0.9)
+    }
     from(Scenes.Gone, to = Scenes.QuickSettingsShade) { goneToQuickSettingsShadeTransition() }
     from(Scenes.Lockscreen, to = Scenes.Bouncer) { lockscreenToBouncerTransition() }
     from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index ac3e015..b5a10ca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -19,7 +19,10 @@
 
 import android.view.ContextThemeWrapper
 import android.view.ViewGroup
+import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsHoveredAsState
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -32,7 +35,9 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.derivedStateOf
@@ -40,6 +45,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.graphicsLayer
@@ -58,6 +64,7 @@
 import com.android.compose.animation.scene.TransitionState
 import com.android.compose.animation.scene.ValueKey
 import com.android.compose.animation.scene.animateElementFloatAsState
+import com.android.compose.modifiers.thenIf
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.settingslib.Utils
 import com.android.systemui.battery.BatteryMeterView
@@ -69,6 +76,7 @@
 import com.android.systemui.privacy.OngoingPrivacyChip
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.onScrimDim
 import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.CollapsedHeight
 import com.android.systemui.shade.ui.composable.ShadeHeader.Values.ClockScale
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
@@ -79,7 +87,6 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
 import com.android.systemui.statusbar.policy.Clock
-import kotlin.math.max
 
 object ShadeHeader {
     object Elements {
@@ -103,6 +110,8 @@
     object Colors {
         val ColorScheme.shadeHeaderText: Color
             get() = Color.White
+        val ColorScheme.onScrimDim: Color
+            get() = Color.DarkGray
     }
 
     object TestTags {
@@ -130,7 +139,7 @@
     val horizontalPadding =
         max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
 
-    val useExpandedFormat by
+    val useExpandedTextFormat by
         remember(cutoutLocation) {
             derivedStateOf {
                 cutoutLocation != CutoutLocation.CENTER ||
@@ -138,6 +147,10 @@
             }
         }
 
+    val isLargeScreenLayout =
+            LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Medium ||
+                    LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Expanded
+
     val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
 
     // This layout assumes it is globally positioned at (0, 0) and is the
@@ -182,22 +195,22 @@
                                 Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
                                     .padding(horizontal = horizontalPadding)
                         ) {
+                            if (isLargeScreenLayout) {
+                                ShadeCarrierGroup(
+                                        viewModel = viewModel,
+                                        modifier = Modifier.align(Alignment.CenterVertically),
+                                )
+                            }
                             SystemIconContainer(
+                                viewModel = viewModel,
+                                isClickable = isLargeScreenLayout,
                                 modifier = Modifier.align(Alignment.CenterVertically)
                             ) {
-                                when (LocalWindowSizeClass.current.widthSizeClass) {
-                                    WindowWidthSizeClass.Medium,
-                                    WindowWidthSizeClass.Expanded ->
-                                        ShadeCarrierGroup(
-                                            viewModel = viewModel,
-                                            modifier = Modifier.align(Alignment.CenterVertically),
-                                        )
-                                }
                                 StatusIcons(
                                     viewModel = viewModel,
                                     createTintedIconManager = createTintedIconManager,
                                     statusBarIconController = statusBarIconController,
-                                    useExpandedFormat = useExpandedFormat,
+                                    useExpandedFormat = useExpandedTextFormat,
                                     modifier =
                                         Modifier.align(Alignment.CenterVertically)
                                             .padding(end = 6.dp)
@@ -206,7 +219,7 @@
                                 BatteryIcon(
                                     createBatteryMeterViewController =
                                         createBatteryMeterViewController,
-                                    useExpandedFormat = useExpandedFormat,
+                                    useExpandedFormat = useExpandedTextFormat,
                                     modifier = Modifier.align(Alignment.CenterVertically),
                                 )
                             }
@@ -322,7 +335,7 @@
                     modifier = Modifier.widthIn(max = 90.dp).align(Alignment.CenterVertically),
                 )
                 Spacer(modifier = Modifier.weight(1f))
-                SystemIconContainer {
+                SystemIconContainer(viewModel = viewModel, isClickable = false) {
                     StatusIcons(
                         viewModel = viewModel,
                         createTintedIconManager = createTintedIconManager,
@@ -531,12 +544,30 @@
 
 @Composable
 private fun SystemIconContainer(
+    viewModel: ShadeHeaderViewModel,
+    isClickable: Boolean,
     modifier: Modifier = Modifier,
     content: @Composable RowScope.() -> Unit
 ) {
-    // TODO(b/298524053): add hover state for this container
+    val interactionSource = remember { MutableInteractionSource() }
+    val isHovered by interactionSource.collectIsHoveredAsState()
+
+    val hoverModifier = Modifier
+            .clip(RoundedCornerShape(CollapsedHeight / 4))
+            .background(MaterialTheme.colorScheme.onScrimDim)
+
     Row(
-        modifier = modifier.height(CollapsedHeight),
+        modifier = modifier
+                .height(CollapsedHeight)
+                .padding(vertical = CollapsedHeight / 4)
+                .thenIf(isClickable) {
+                    Modifier.clickable(
+                            interactionSource = interactionSource,
+                            indication = null,
+                            onClick = { viewModel.onSystemIconContainerClicked() },
+                    )
+                }
+                .thenIf(isHovered) { hoverModifier },
         content = content,
     )
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 7fd3a176..114dcf4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -381,18 +381,17 @@
                 // relayout/redraw for nothing.
                 fromValue
             } else {
-                // In the case of bouncing, if the value remains constant during the overscroll, we
-                // should use the value of the scene we are bouncing around.
-                if (!canOverflow && transition is TransitionState.HasOverscrollProperties) {
-                    val bouncingScene = transition.bouncingScene
-                    if (bouncingScene != null) {
-                        return sharedValue[bouncingScene]
-                    }
-                }
-
+                val overscrollSpec = transition.currentOverscrollSpec
                 val progress =
-                    if (canOverflow) transition.progress
-                    else transition.progress.fastCoerceIn(0f, 1f)
+                    when {
+                        overscrollSpec == null -> {
+                            if (canOverflow) transition.progress
+                            else transition.progress.fastCoerceIn(0f, 1f)
+                        }
+                        overscrollSpec.scene == transition.toScene -> 1f
+                        else -> 0f
+                    }
+
                 sharedValue.type.lerp(fromValue, toValue, progress)
             }
         } else fromValue ?: toValue
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 980982a..5611c6e 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
@@ -39,6 +39,8 @@
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.node.DrawModifierNode
 import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.TraversableNode
+import androidx.compose.ui.node.traverseDescendants
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntSize
@@ -165,7 +167,7 @@
     private var currentTransitions: List<TransitionState.Transition>,
     private var scene: Scene,
     private var key: ElementKey,
-) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode {
+) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
     private var _element: Element? = null
     private val element: Element
         get() = _element!!
@@ -174,6 +176,8 @@
     private val sceneState: Element.SceneState
         get() = _sceneState!!
 
+    override val traverseKey: Any = ElementTraverseKey
+
     override fun onAttach() {
         super.onAttach()
         updateElementAndSceneValues()
@@ -289,18 +293,15 @@
         val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != scene.key
         val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null
         if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) {
-            sceneState.lastOffset = Offset.Unspecified
-            sceneState.lastScale = Scale.Unspecified
-            sceneState.lastAlpha = Element.AlphaUnspecified
+            recursivelyClearPlacementValues()
+            sceneState.lastSize = Element.SizeUnspecified
 
             val placeable = measurable.measure(constraints)
-            sceneState.lastSize = placeable.size()
-
             return layout(placeable.width, placeable.height) { /* Do not place */ }
         }
 
         val placeable =
-            measure(layoutImpl, scene, element, transition, sceneState, measurable, constraints)
+            measure(layoutImpl, element, transition, sceneState, measurable, constraints)
         sceneState.lastSize = placeable.size()
         return layout(placeable.width, placeable.height) { place(transition, placeable) }
     }
@@ -315,13 +316,10 @@
             // scene when idle.
             val coords =
                 coordinates ?: error("Element ${element.key} does not have any coordinates")
-            val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
 
             // No need to place the element in this scene if we don't want to draw it anyways.
             if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) {
-                sceneState.lastOffset = Offset.Unspecified
-                sceneState.lastScale = Scale.Unspecified
-                sceneState.lastAlpha = Element.AlphaUnspecified
+                recursivelyClearPlacementValues()
                 return
             }
 
@@ -329,12 +327,11 @@
             val targetOffset =
                 computeValue(
                     layoutImpl,
-                    scene,
+                    sceneState,
                     element,
                     transition,
                     sceneValue = { it.targetOffset },
                     transformation = { it.offset },
-                    idleValue = targetOffsetInScene,
                     currentValue = { currentOffset },
                     isSpecified = { it != Offset.Unspecified },
                     ::lerp,
@@ -395,18 +392,37 @@
                         return@placeWithLayer
                     }
 
-                    alpha = elementAlpha(layoutImpl, scene, element, transition, sceneState)
+                    alpha = elementAlpha(layoutImpl, element, transition, sceneState)
                     compositingStrategy = CompositingStrategy.ModulateAlpha
                 }
             }
         }
     }
 
+    /**
+     * Recursively clear the last placement values on this node and all descendants ElementNodes.
+     * This should be called when this node is not placed anymore, so that we correctly clear values
+     * for the descendants for which approachMeasure() won't be called.
+     */
+    private fun recursivelyClearPlacementValues() {
+        fun Element.SceneState.clearLastPlacementValues() {
+            lastOffset = Offset.Unspecified
+            lastScale = Scale.Unspecified
+            lastAlpha = Element.AlphaUnspecified
+        }
+
+        sceneState.clearLastPlacementValues()
+        traverseDescendants(ElementTraverseKey) { node ->
+            (node as ElementNode).sceneState.clearLastPlacementValues()
+            TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal
+        }
+    }
+
     override fun ContentDrawScope.draw() {
         element.wasDrawnInAnyScene = true
 
         val transition = elementTransition(layoutImpl, element, currentTransitions)
-        val drawScale = getDrawScale(layoutImpl, scene, element, transition, sceneState)
+        val drawScale = getDrawScale(layoutImpl, element, transition, sceneState)
         if (drawScale == Scale.Default) {
             drawContent()
         } else {
@@ -421,6 +437,8 @@
     }
 
     companion object {
+        private val ElementTraverseKey = Any()
+
         private fun maybePruneMaps(
             layoutImpl: SceneTransitionLayoutImpl,
             element: Element,
@@ -494,22 +512,23 @@
     // Remove the interruption values to all scenes but the scene(s) where the element will be
     // placed, to make sure that interruption deltas are computed only right after this interruption
     // is prepared.
-    fun maybeCleanPlacementValuesBeforeInterruption(sceneState: Element.SceneState) {
+    fun cleanInterruptionValues(sceneState: Element.SceneState) {
+        sceneState.sizeInterruptionDelta = IntSize.Zero
+        sceneState.offsetInterruptionDelta = Offset.Zero
+        sceneState.alphaInterruptionDelta = 0f
+        sceneState.scaleInterruptionDelta = Scale.Zero
+
         if (!shouldPlaceElement(layoutImpl, sceneState.scene, element, transition)) {
             sceneState.offsetBeforeInterruption = Offset.Unspecified
             sceneState.alphaBeforeInterruption = Element.AlphaUnspecified
             sceneState.scaleBeforeInterruption = Scale.Unspecified
-
-            sceneState.offsetInterruptionDelta = Offset.Zero
-            sceneState.alphaInterruptionDelta = 0f
-            sceneState.scaleInterruptionDelta = Scale.Zero
         }
     }
 
-    previousFromState?.let { maybeCleanPlacementValuesBeforeInterruption(it) }
-    previousToState?.let { maybeCleanPlacementValuesBeforeInterruption(it) }
-    fromState?.let { maybeCleanPlacementValuesBeforeInterruption(it) }
-    toState?.let { maybeCleanPlacementValuesBeforeInterruption(it) }
+    previousFromState?.let { cleanInterruptionValues(it) }
+    previousToState?.let { cleanInterruptionValues(it) }
+    fromState?.let { cleanInterruptionValues(it) }
+    toState?.let { cleanInterruptionValues(it) }
 }
 
 /**
@@ -780,7 +799,6 @@
  */
 private fun elementAlpha(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
     element: Element,
     transition: TransitionState.Transition?,
     sceneState: Element.SceneState,
@@ -788,12 +806,11 @@
     val alpha =
         computeValue(
                 layoutImpl,
-                scene,
+                sceneState,
                 element,
                 transition,
                 sceneValue = { 1f },
                 transformation = { it.alpha },
-                idleValue = 1f,
                 currentValue = { 1f },
                 isSpecified = { true },
                 ::lerp,
@@ -841,9 +858,8 @@
     )
 }
 
-private fun ApproachMeasureScope.measure(
+private fun measure(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
     element: Element,
     transition: TransitionState.Transition?,
     sceneState: Element.SceneState,
@@ -858,12 +874,11 @@
     val targetSize =
         computeValue(
             layoutImpl,
-            scene,
+            sceneState,
             element,
             transition,
             sceneValue = { it.targetSize },
             transformation = { it.size },
-            idleValue = lookaheadSize,
             currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
             isSpecified = { it != Element.SizeUnspecified },
             ::lerp,
@@ -909,7 +924,6 @@
 
 private fun ContentDrawScope.getDrawScale(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
     element: Element,
     transition: TransitionState.Transition?,
     sceneState: Element.SceneState,
@@ -917,12 +931,11 @@
     val scale =
         computeValue(
             layoutImpl,
-            scene,
+            sceneState,
             element,
             transition,
             sceneValue = { Scale.Default },
             transformation = { it.drawScale },
-            idleValue = Scale.Default,
             currentValue = { Scale.Default },
             isSpecified = { true },
             ::lerp,
@@ -989,11 +1002,12 @@
  * Measurable.
  *
  * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
- * @param scene the scene containing [element].
+ * @param currentSceneState the scene state of the scene for which we are computing the value. Note
+ *   that during interruptions, this could be the state of a scene that is neither
+ *   [transition.toScene] nor [transition.fromScene].
  * @param element the element being animated.
  * @param sceneValue the value being animated.
  * @param transformation the transformation associated to the value being animated.
- * @param idleValue the value when idle, i.e. when there is no transition happening.
  * @param currentValue the value that would be used if it is not transformed. Note that this is
  *   different than [idleValue] even if the value is not transformed directly because it could be
  *   impacted by the transformations on other elements, like a parent that is being translated or
@@ -1003,12 +1017,11 @@
  */
 private inline fun <T> computeValue(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
+    currentSceneState: Element.SceneState,
     element: Element,
     transition: TransitionState.Transition?,
     sceneValue: (Element.SceneState) -> T,
     transformation: (ElementTransformations) -> PropertyTransformation<T>?,
-    idleValue: T,
     currentValue: () -> T,
     isSpecified: (T) -> Boolean,
     lerp: (T, T, Float) -> T,
@@ -1030,19 +1043,22 @@
     if (fromState == null && toState == null) {
         // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
         // run anymore.
-        return idleValue
+        return sceneValue(currentSceneState)
     }
 
+    val currentScene = currentSceneState.scene
     if (transition is TransitionState.HasOverscrollProperties) {
         val overscroll = transition.currentOverscrollSpec
-        if (overscroll?.scene == scene.key) {
-            val elementSpec = overscroll.transformationSpec.transformations(element.key, scene.key)
+        if (overscroll?.scene == currentScene) {
+            val elementSpec =
+                overscroll.transformationSpec.transformations(element.key, currentScene)
             val propertySpec = transformation(elementSpec) ?: return currentValue()
-            val overscrollState = checkNotNull(if (scene.key == toScene) toState else fromState)
+            val overscrollState = checkNotNull(if (currentScene == toScene) toState else fromState)
+            val idleValue = sceneValue(overscrollState)
             val targetValue =
                 propertySpec.transform(
                     layoutImpl,
-                    scene,
+                    currentScene,
                     element,
                     overscrollState,
                     transition,
@@ -1086,24 +1102,30 @@
         return if (start == end) start else lerp(start, end, transition.progress)
     }
 
-    val transformation =
-        transformation(transition.transformationSpec.transformations(element.key, scene.key))
-            // If there is no transformation explicitly associated to this element value, let's use
-            // the value given by the system (like the current position and size given by the layout
-            // pass).
-            ?: return currentValue()
-
     // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
     // end (for leaving elements) of the transition.
     val sceneState =
         checkNotNull(
             when {
-                isSharedElement && scene.key == fromScene -> fromState
+                isSharedElement && currentScene == fromScene -> fromState
                 isSharedElement -> toState
                 else -> fromState ?: toState
             }
         )
 
+    // The scene for which we compute the transformation. Note that this is not necessarily
+    // [currentScene] because [currentScene] could be a different scene than the transition
+    // fromScene or toScene during interruptions.
+    val scene = sceneState.scene
+
+    val transformation =
+        transformation(transition.transformationSpec.transformations(element.key, scene))
+            // If there is no transformation explicitly associated to this element value, let's use
+            // the value given by the system (like the current position and size given by the layout
+            // pass).
+            ?: return currentValue()
+
+    val idleValue = sceneValue(sceneState)
     val targetValue =
         transformation.transform(
             layoutImpl,
@@ -1125,7 +1147,7 @@
     val rangeProgress = transformation.range?.progress(progress) ?: progress
 
     // Interpolate between the value at rest and the value before entering/after leaving.
-    val isEntering = scene.key == toScene
+    val isEntering = scene == toScene
     return if (isEntering) {
         lerp(targetValue, idleValue, rangeProgress)
     } else {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index f32720c..7ea8cbd 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -293,7 +293,15 @@
                 width = fromSize.width
                 height = fromSize.height
             } else {
-                val size = lerp(fromSize, toSize, transition.progress)
+                val overscrollSpec = transition.currentOverscrollSpec
+                val progress =
+                    when {
+                        overscrollSpec == null -> transition.progress
+                        overscrollSpec.scene == transition.toScene -> 1f
+                        else -> 0f
+                    }
+
+                val size = lerp(fromSize, toSize, progress)
                 width = size.width.coerceAtLeast(0)
                 height = size.height.coerceAtLeast(0)
             }
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 6a178c8..a8df6f4 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
@@ -768,7 +768,7 @@
 /** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */
 internal class MutableSceneTransitionLayoutStateImpl(
     initialScene: SceneKey,
-    override var transitions: SceneTransitions,
+    override var transitions: SceneTransitions = transitions {},
     private val canChangeScene: (SceneKey) -> Boolean = { true },
     stateLinks: List<StateLink> = emptyList(),
     enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index b54afae..73ee451 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -20,7 +20,6 @@
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
@@ -34,7 +33,7 @@
 ) : PropertyTransformation<IntSize> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: Scene,
+        scene: SceneKey,
         element: Element,
         sceneState: Element.SceneState,
         transition: TransitionState.Transition,
@@ -60,7 +59,7 @@
         // This simple implementation assumes that the size of [element] is the same as the size of
         // the [anchor] in [scene], so simply transform to the size of the anchor in the other
         // scene.
-        return if (scene.key == transition.fromScene) {
+        return if (scene == transition.fromScene) {
             anchorSizeIn(transition.toScene)
         } else {
             anchorSizeIn(transition.fromScene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index 2bab4f8..70dca4c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -21,7 +21,6 @@
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
@@ -33,7 +32,7 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: Scene,
+        scene: SceneKey,
         element: Element,
         sceneState: Element.SceneState,
         transition: TransitionState.Transition,
@@ -61,7 +60,7 @@
             anchorOffsetIn(transition.toScene) ?: throwException(transition.toScene)
         val offset = anchorToOffset - anchorFromOffset
 
-        return if (scene.key == transition.toScene) {
+        return if (scene == transition.toScene) {
             Offset(
                 value.x - offset.x,
                 value.y - offset.y,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index 6704a3b..98c2dd3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -20,7 +20,7 @@
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.Scale
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -37,7 +37,7 @@
 
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: Scene,
+        scene: SceneKey,
         element: Element,
         sceneState: Element.SceneState,
         transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 191a8fb..aa8dc38 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -20,7 +20,7 @@
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -32,13 +32,13 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: Scene,
+        scene: SceneKey,
         element: Element,
         sceneState: Element.SceneState,
         transition: TransitionState.Transition,
         value: Offset
     ): Offset {
-        val sceneSize = scene.targetSize
+        val sceneSize = layoutImpl.scene(scene).targetSize
         val elementSize = sceneState.targetSize
         if (elementSize == Element.SizeUnspecified) {
             return value
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index 41f626e..ada814e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -18,7 +18,7 @@
 
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -28,7 +28,7 @@
 ) : PropertyTransformation<Float> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: Scene,
+        scene: SceneKey,
         element: Element,
         sceneState: Element.SceneState,
         transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index f5207dc..dca8f85 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -19,7 +19,7 @@
 import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 import kotlin.math.roundToInt
@@ -35,7 +35,7 @@
 ) : PropertyTransformation<IntSize> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: Scene,
+        scene: SceneKey,
         element: Element,
         sceneState: Element.SceneState,
         transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 603f7ba..7be9ce1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -21,7 +21,7 @@
 import androidx.compose.ui.util.fastCoerceIn
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -61,7 +61,7 @@
     // to these internal classes.
     fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: Scene,
+        scene: SceneKey,
         element: Element,
         sceneState: Element.SceneState,
         transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index 849c9d7..f066511 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -22,7 +22,7 @@
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.OverscrollScope
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -33,7 +33,7 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: Scene,
+        scene: SceneKey,
         element: Element,
         sceneState: Element.SceneState,
         transition: TransitionState.Transition,
@@ -55,7 +55,7 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: Scene,
+        scene: SceneKey,
         element: Element,
         sceneState: Element.SceneState,
         transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
index 6e8b208..a7889e2 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
@@ -18,10 +18,13 @@
 
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -443,4 +446,56 @@
         assertThat(lastValues[bar]?.get(SceneC)).isWithin(0.001f).of(7f)
         assertThat(lastValues[bar]?.get(SceneD)).isWithin(0.001f).of(7f)
     }
+
+    @Test
+    fun animatedValueDoesNotOverscrollWhenOverscrollIsSpecified() {
+        val state =
+            rule.runOnUiThread {
+                MutableSceneTransitionLayoutStateImpl(
+                    SceneA,
+                    transitions { overscroll(SceneB, Orientation.Horizontal) }
+                )
+            }
+
+        val key = ValueKey("foo")
+        val lastValues = mutableMapOf<SceneKey, Float>()
+
+        @Composable
+        fun SceneScope.animateFloat(value: Float, key: ValueKey) {
+            val animatedValue = animateSceneFloatAsState(value, key)
+            LaunchedEffect(animatedValue) {
+                snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it }
+            }
+        }
+
+        rule.setContent {
+            SceneTransitionLayout(state) {
+                scene(SceneA) { animateFloat(0f, key) }
+                scene(SceneB) { animateFloat(100f, key) }
+            }
+        }
+
+        // Overscroll on A at -100%: value should be interpolated given that there is no overscroll
+        // defined for scene A.
+        var progress by mutableStateOf(-1f)
+        rule.runOnIdle {
+            state.startTransition(transition(from = SceneA, to = SceneB, progress = { progress }))
+        }
+        rule.waitForIdle()
+        assertThat(lastValues[SceneA]).isWithin(0.001f).of(-100f)
+        assertThat(lastValues[SceneB]).isWithin(0.001f).of(-100f)
+
+        // Middle of the transition.
+        progress = 0.5f
+        rule.waitForIdle()
+        assertThat(lastValues[SceneA]).isWithin(0.001f).of(50f)
+        assertThat(lastValues[SceneB]).isWithin(0.001f).of(50f)
+
+        // Overscroll on B at 200%: value should not be interpolated given that there is an
+        // overscroll defined for scene B.
+        progress = 2f
+        rule.waitForIdle()
+        assertThat(lastValues[SceneA]).isWithin(0.001f).of(100f)
+        assertThat(lastValues[SceneB]).isWithin(0.001f).of(100f)
+    }
 }
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 41cacb4..a18da73 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
@@ -47,10 +47,12 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.approachLayout
 import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.hasText
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -1719,4 +1721,220 @@
         rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsNotDisplayed()
         rule.onNode(isElement(TestElements.Foo, SceneC)).assertPositionInRootIsEqualTo(40.dp, 40.dp)
     }
+
+    @Test
+    fun lastPlacementValuesAreClearedOnNestedElements() {
+        val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+
+        @Composable
+        fun SceneScope.NestedFooBar() {
+            Box(Modifier.element(TestElements.Foo)) {
+                Box(Modifier.element(TestElements.Bar).size(10.dp))
+            }
+        }
+
+        lateinit var layoutImpl: SceneTransitionLayoutImpl
+        rule.setContent {
+            SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) {
+                scene(SceneA) { NestedFooBar() }
+                scene(SceneB) { NestedFooBar() }
+            }
+        }
+
+        // Idle on A: composed and placed only in B.
+        rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsDisplayed()
+        rule.onNode(isElement(TestElements.Bar, SceneA)).assertIsDisplayed()
+        rule.onNode(isElement(TestElements.Foo, SceneB)).assertDoesNotExist()
+        rule.onNode(isElement(TestElements.Bar, SceneB)).assertDoesNotExist()
+
+        assertThat(layoutImpl.elements).containsKey(TestElements.Foo)
+        assertThat(layoutImpl.elements).containsKey(TestElements.Bar)
+        val foo = layoutImpl.elements.getValue(TestElements.Foo)
+        val bar = layoutImpl.elements.getValue(TestElements.Bar)
+
+        assertThat(foo.sceneStates).containsKey(SceneA)
+        assertThat(bar.sceneStates).containsKey(SceneA)
+        assertThat(foo.sceneStates).doesNotContainKey(SceneB)
+        assertThat(bar.sceneStates).doesNotContainKey(SceneB)
+
+        val fooInA = foo.sceneStates.getValue(SceneA)
+        val barInA = bar.sceneStates.getValue(SceneA)
+        assertThat(fooInA.lastOffset).isNotEqualTo(Offset.Unspecified)
+        assertThat(fooInA.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
+        assertThat(fooInA.lastScale).isNotEqualTo(Scale.Unspecified)
+
+        assertThat(barInA.lastOffset).isNotEqualTo(Offset.Unspecified)
+        assertThat(barInA.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
+        assertThat(barInA.lastScale).isNotEqualTo(Scale.Unspecified)
+
+        // A => B: composed in both and placed only in B.
+        rule.runOnUiThread { state.startTransition(transition(from = SceneA, to = SceneB)) }
+        rule.onNode(isElement(TestElements.Foo, SceneA)).assertExists().assertIsNotDisplayed()
+        rule.onNode(isElement(TestElements.Bar, SceneA)).assertExists().assertIsNotDisplayed()
+        rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsDisplayed()
+        rule.onNode(isElement(TestElements.Bar, SceneB)).assertIsDisplayed()
+
+        assertThat(foo.sceneStates).containsKey(SceneB)
+        assertThat(bar.sceneStates).containsKey(SceneB)
+
+        val fooInB = foo.sceneStates.getValue(SceneB)
+        val barInB = bar.sceneStates.getValue(SceneB)
+        assertThat(fooInA.lastOffset).isEqualTo(Offset.Unspecified)
+        assertThat(fooInA.lastAlpha).isEqualTo(Element.AlphaUnspecified)
+        assertThat(fooInA.lastScale).isEqualTo(Scale.Unspecified)
+        assertThat(fooInB.lastOffset).isNotEqualTo(Offset.Unspecified)
+        assertThat(fooInB.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
+        assertThat(fooInB.lastScale).isNotEqualTo(Scale.Unspecified)
+
+        assertThat(barInA.lastOffset).isEqualTo(Offset.Unspecified)
+        assertThat(barInA.lastAlpha).isEqualTo(Element.AlphaUnspecified)
+        assertThat(barInA.lastScale).isEqualTo(Scale.Unspecified)
+        assertThat(barInB.lastOffset).isNotEqualTo(Offset.Unspecified)
+        assertThat(barInB.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
+        assertThat(barInB.lastScale).isNotEqualTo(Scale.Unspecified)
+    }
+
+    @Test
+    fun currentTransitionSceneIsUsedToComputeElementValues() = runTest {
+        val state =
+            rule.runOnIdle {
+                MutableSceneTransitionLayoutStateImpl(
+                    SceneA,
+                    transitions {
+                        from(SceneB, to = SceneC) {
+                            scaleSize(TestElements.Foo, width = 2f, height = 3f)
+                        }
+                    }
+                )
+            }
+
+        @Composable
+        fun SceneScope.Foo() {
+            Box(Modifier.testTag("fooParentIn${sceneKey.debugName}")) {
+                Box(Modifier.element(TestElements.Foo).size(20.dp))
+            }
+        }
+
+        rule.setContent {
+            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+                scene(SceneA) { Foo() }
+                scene(SceneB) {}
+                scene(SceneC) { Foo() }
+            }
+        }
+
+        // We have 2 transitions:
+        //  - A => B at 100%
+        //  - B => C at 0%
+        // So Foo should have a size of (40dp, 60dp) in both A and C given that it is scaling its
+        // size in B => C.
+        rule.runOnUiThread {
+            state.startTransition(
+                transition(from = SceneA, to = SceneB, progress = { 1f }, onFinish = neverFinish())
+            )
+            state.startTransition(transition(from = SceneB, to = SceneC, progress = { 0f }))
+        }
+
+        rule.onNode(hasTestTag("fooParentInSceneA")).assertSizeIsEqualTo(40.dp, 60.dp)
+        rule.onNode(hasTestTag("fooParentInSceneC")).assertSizeIsEqualTo(40.dp, 60.dp)
+    }
+
+    @Test
+    fun interruptionDeltasAreProperlyCleaned() = runTest {
+        val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+
+        @Composable
+        fun SceneScope.Foo(offset: Dp) {
+            Box(Modifier.fillMaxSize()) {
+                Box(Modifier.offset(offset, offset).element(TestElements.Foo).size(20.dp))
+            }
+        }
+
+        rule.setContent {
+            SceneTransitionLayout(state, Modifier.size(200.dp)) {
+                scene(SceneA) { Foo(offset = 0.dp) }
+                scene(SceneB) { Foo(offset = 20.dp) }
+                scene(SceneC) { Foo(offset = 40.dp) }
+            }
+        }
+
+        // Start A => B at 50%.
+        val aToB =
+            transition(from = SceneA, to = SceneB, progress = { 0.5f }, onFinish = neverFinish())
+        rule.runOnUiThread { state.startTransition(aToB) }
+        rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(10.dp, 10.dp)
+
+        // Start B => C at 0%. This will compute an interruption delta of (-10dp, -10dp) so that the
+        // position of Foo is unchanged and converges to (20dp, 20dp).
+        var interruptionProgress by mutableStateOf(1f)
+        val bToC =
+            transition(
+                from = SceneB,
+                to = SceneC,
+                progress = { 0f },
+                interruptionProgress = { interruptionProgress },
+                onFinish = neverFinish(),
+            )
+        rule.runOnUiThread { state.startTransition(bToC) }
+        rule.onNode(isElement(TestElements.Foo, SceneC)).assertPositionInRootIsEqualTo(10.dp, 10.dp)
+
+        // Finish the interruption and leave the transition progress at 0f. We should be at the same
+        // state as in B.
+        interruptionProgress = 0f
+        rule.onNode(isElement(TestElements.Foo, SceneC)).assertPositionInRootIsEqualTo(20.dp, 20.dp)
+
+        // Finish both transitions but directly start a new one B => A with interruption progress
+        // 100%. We should be at (20dp, 20dp), unless the interruption deltas have not been
+        // correctly cleaned.
+        rule.runOnUiThread {
+            state.finishTransition(aToB, idleScene = SceneB)
+            state.finishTransition(bToC, idleScene = SceneB)
+            state.startTransition(
+                transition(
+                    from = SceneB,
+                    to = SceneA,
+                    progress = { 0f },
+                    interruptionProgress = { 1f },
+                )
+            )
+        }
+        rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(20.dp, 20.dp)
+    }
+
+    @Test
+    fun lastSizeIsUnspecifiedWhenOverscrollingOtherScene() = runTest {
+        val state =
+            rule.runOnIdle {
+                MutableSceneTransitionLayoutStateImpl(
+                    SceneA,
+                    transitions { overscroll(SceneA, Orientation.Horizontal) }
+                )
+            }
+
+        @Composable
+        fun SceneScope.Foo() {
+            Box(Modifier.element(TestElements.Foo).size(10.dp))
+        }
+
+        lateinit var layoutImpl: SceneTransitionLayoutImpl
+        rule.setContent {
+            SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) {
+                scene(SceneA) { Foo() }
+                scene(SceneB) { Foo() }
+            }
+        }
+
+        // Overscroll A => B on A.
+        rule.runOnUiThread {
+            state.startTransition(
+                transition(from = SceneA, to = SceneB, progress = { -1f }, onFinish = neverFinish())
+            )
+        }
+        rule.waitForIdle()
+
+        assertThat(
+                layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB).lastSize
+            )
+            .isEqualTo(Element.SizeUnspecified)
+    }
 }
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 08532bd..a8dd572 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
@@ -21,6 +21,7 @@
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.offset
@@ -333,6 +334,42 @@
     }
 
     @Test
+    fun layoutSizeDoesNotOverscrollWhenOverscrollIsSpecified() {
+        val state =
+            rule.runOnUiThread {
+                MutableSceneTransitionLayoutStateImpl(
+                    SceneA,
+                    transitions { overscroll(SceneB, Orientation.Horizontal) }
+                )
+            }
+
+        val layoutTag = "layout"
+        rule.setContent {
+            SceneTransitionLayout(state, Modifier.testTag(layoutTag)) {
+                scene(SceneA) { Box(Modifier.size(50.dp)) }
+                scene(SceneB) { Box(Modifier.size(70.dp)) }
+            }
+        }
+
+        // Overscroll on A at -100%: size should be interpolated given that there is no overscroll
+        // defined for scene A.
+        var progress by mutableStateOf(-1f)
+        rule.runOnIdle {
+            state.startTransition(transition(from = SceneA, to = SceneB, progress = { progress }))
+        }
+        rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(30.dp)
+
+        // Middle of the transition.
+        progress = 0.5f
+        rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(60.dp)
+
+        // Overscroll on B at 200%: size should not be interpolated given that there is an
+        // overscroll defined for scene B.
+        progress = 2f
+        rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(70.dp)
+    }
+
+    @Test
     fun multipleTransitionsWillComposeMultipleScenes() {
         val duration = 10 * 16L
 
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
index e743c78..6d063a0 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
@@ -17,7 +17,7 @@
 package com.android.compose.animation.scene
 
 import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.hasParent
+import androidx.compose.ui.test.hasAnyAncestor
 import androidx.compose.ui.test.hasTestTag
 
 /** A [SemanticsMatcher] that matches [element], optionally restricted to scene [scene]. */
@@ -25,6 +25,6 @@
     return if (scene == null) {
         hasTestTag(element.testTag)
     } else {
-        hasTestTag(element.testTag) and hasParent(hasTestTag(scene.testTag))
+        hasTestTag(element.testTag) and hasAnyAncestor(hasTestTag(scene.testTag))
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
index cfc6b33..a12b6f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
@@ -32,8 +32,11 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -76,6 +79,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun transitionToGone_keyguardOccluded_biometricAuthenticated() =
         testScope.runTest {
             transitionRepository.sendTransitionSteps(
@@ -96,6 +100,25 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun transitionToGone_keyguardOccludedThenAltBouncer_authed_wmStateRefactor() =
+        testScope.runTest {
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.OCCLUDED,
+                to = KeyguardState.ALTERNATE_BOUNCER,
+                testScope
+            )
+            reset(transitionRepository)
+
+            // Authentication results in calling startDismissKeyguardTransition.
+            kosmos.keyguardTransitionInteractor.startDismissKeyguardTransition()
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(from = KeyguardState.ALTERNATE_BOUNCER, to = KeyguardState.GONE)
+        }
+
+    @Test
     fun noTransition_keyguardNotOccluded_biometricAuthenticated() =
         testScope.runTest {
             transitionRepository.sendTransitionSteps(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
index 6c5001a..6eb9862 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -53,6 +53,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 import com.android.systemui.testKosmos
 import junit.framework.Assert.assertEquals
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -299,6 +300,7 @@
     fun testTransitionToOccluded_onWake() =
         testScope.runTest {
             kosmos.fakeKeyguardRepository.setKeyguardOccluded(true)
+            kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(true)
             powerInteractor.setAwakeForTest()
             advanceTimeBy(100) // account for debouncing
 
@@ -312,6 +314,7 @@
         testScope.runTest {
             kosmos.fakeKeyguardRepository.setKeyguardShowing(false)
             kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
+            kosmos.keyguardTransitionInteractor.startDismissKeyguardTransition()
             powerInteractor.setAwakeForTest()
             advanceTimeBy(100) // account for debouncing
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index addbdb6..7906a82 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeCommandQueue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -191,6 +192,7 @@
     fun dismissAlpha() =
         testScope.runTest {
             val dismissAlpha by collectLastValue(underTest.dismissAlpha)
+            assertThat(dismissAlpha).isEqualTo(1f)
 
             keyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.AOD,
@@ -202,9 +204,9 @@
             // User begins to swipe up
             shadeRepository.setLegacyShadeExpansion(0.99f)
 
-            // When not dismissable, no alpha value (null) should emit
+            // When not dismissable, the last alpha value should still be present
             repository.setKeyguardDismissible(false)
-            assertThat(dismissAlpha).isNull()
+            assertThat(dismissAlpha).isEqualTo(1f)
 
             repository.setKeyguardDismissible(true)
             shadeRepository.setLegacyShadeExpansion(0.98f)
@@ -212,9 +214,11 @@
         }
 
     @Test
-    fun dismissAlpha_whenShadeIsExpandedEmitsNull() =
+    fun dismissAlpha_whenShadeResetsEmitsOne() =
         testScope.runTest {
-            val dismissAlpha by collectLastValue(underTest.dismissAlpha)
+            val dismissAlpha by collectValues(underTest.dismissAlpha)
+            assertThat(dismissAlpha[0]).isEqualTo(1f)
+            assertThat(dismissAlpha.size).isEqualTo(1)
 
             keyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.AOD,
@@ -222,14 +226,50 @@
                 testScope,
             )
 
-            repository.setStatusBarState(StatusBarState.SHADE_LOCKED)
-            shadeRepository.setQsExpansion(1f)
+            // User begins to swipe up
+            repository.setStatusBarState(StatusBarState.KEYGUARD)
+            repository.setKeyguardDismissible(true)
+            shadeRepository.setLegacyShadeExpansion(0.98f)
 
-            repository.setKeyguardDismissible(false)
-            assertThat(dismissAlpha).isNull()
+            assertThat(dismissAlpha[1]).isGreaterThan(0.5f)
+            assertThat(dismissAlpha[1]).isLessThan(1f)
+            assertThat(dismissAlpha.size).isEqualTo(2)
+
+            // Now reset the shade
+            shadeRepository.setLegacyShadeExpansion(1f)
+            assertThat(dismissAlpha[2]).isEqualTo(1f)
+            assertThat(dismissAlpha.size).isEqualTo(3)
+        }
+
+    @Test
+    fun dismissAlpha_doesNotEmitWhileTransitioning() =
+        testScope.runTest {
+            val dismissAlpha by collectLastValue(underTest.dismissAlpha)
+            assertThat(dismissAlpha).isEqualTo(1f)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    TransitionStep(
+                        from = KeyguardState.AOD,
+                        to = KeyguardState.GONE,
+                        value = 0f,
+                        transitionState = TransitionState.STARTED,
+                    ),
+                    TransitionStep(
+                        from = KeyguardState.AOD,
+                        to = KeyguardState.GONE,
+                        value = 0.1f,
+                        transitionState = TransitionState.RUNNING,
+                    ),
+                ),
+                testScope,
+            )
 
             repository.setKeyguardDismissible(true)
-            assertThat(dismissAlpha).isNull()
+            shadeRepository.setLegacyShadeExpansion(0.98f)
+
+            // Should still be one
+            assertThat(dismissAlpha).isEqualTo(1f)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
index 73e6506..bc0512a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
@@ -152,7 +152,6 @@
             underTest.setRecommendation(mediaRecommendation.copy(isActive = false))
 
             assertThat(smartspaceMediaData).isNotEqualTo(mediaRecommendation)
-            assertThat(smartspaceMediaData?.isActive).isFalse()
             assertThat(underTest.isRecommendationActive()).isFalse()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index ac66e66..e40c8ee 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+@file:OptIn(ExperimentalCoroutinesApi::class)
 
 package com.android.systemui.scene.domain.startable
 
@@ -395,6 +395,7 @@
                 )
             assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
             underTest.start()
+            runCurrent()
 
             kosmos.fakePowerRepository.updateWakefulness(
                 rawState = WakefulnessState.STARTING_TO_SLEEP,
@@ -1285,6 +1286,42 @@
         }
 
     @Test
+    fun switchToGone_whenSurfaceBehindLockscreenVisibleMidTransition() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val transitionStateFlow =
+                prepareState(
+                    authenticationMethod = AuthenticationMethodModel.None,
+                )
+            underTest.start()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            // Swipe to Gone, more than halfway
+            transitionStateFlow.value =
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Lockscreen,
+                    toScene = Scenes.Gone,
+                    currentScene = flowOf(Scenes.Gone),
+                    progress = flowOf(0.51f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                )
+            runCurrent()
+            // Lift finger
+            transitionStateFlow.value =
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Lockscreen,
+                    toScene = Scenes.Gone,
+                    currentScene = flowOf(Scenes.Gone),
+                    progress = flowOf(0.51f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(false),
+                )
+            runCurrent()
+
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+        }
+
+    @Test
     fun switchToGone_extendUnlock() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index f89f18a..3ded8a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -6,15 +6,27 @@
 import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.activityStarter
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.argThat
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -24,12 +36,16 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
 class ShadeHeaderViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val mobileIconsInteractor = kosmos.fakeMobileIconsInteractor
+    private val sceneInteractor = kosmos.sceneInteractor
+    private val deviceEntryInteractor = kosmos.deviceEntryInteractor
 
     private val underTest: ShadeHeaderViewModel = kosmos.shadeHeaderViewModel
 
@@ -77,6 +93,30 @@
                 )
         }
 
+    @Test
+    fun onSystemIconContainerClicked_locked_collapsesShadeToLockscreen() =
+        testScope.runTest {
+            setDeviceEntered(false)
+            setScene(Scenes.Shade)
+
+            underTest.onSystemIconContainerClicked()
+            runCurrent()
+
+            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen)
+        }
+
+    @Test
+    fun onSystemIconContainerClicked_unlocked_collapsesShadeToGone() =
+            testScope.runTest {
+                setDeviceEntered(true)
+                setScene(Scenes.Shade)
+
+                underTest.onSystemIconContainerClicked()
+                runCurrent()
+
+                assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
+            }
+
     companion object {
         private val SUB_1 =
             SubscriptionModel(
@@ -93,6 +133,32 @@
                 profileClass = PROFILE_CLASS_UNSET,
             )
     }
+
+    private fun setScene(key: SceneKey) {
+        sceneInteractor.changeScene(key, "test")
+        sceneInteractor.setTransitionState(
+                MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+        )
+        testScope.runCurrent()
+    }
+
+    private fun TestScope.setDeviceEntered(isEntered: Boolean) {
+        if (isEntered) {
+            // Unlock the device marking the device has entered.
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                    SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+        }
+        setScene(
+                if (isEntered) {
+                    Scenes.Gone
+                } else {
+                    Scenes.Lockscreen
+                }
+        )
+        assertThat(deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
+    }
 }
 
 private class IntentMatcherAction(private val action: String) : ArgumentMatcher<Intent> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index c35c165..497484f90 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.keyguard.shared.model.BurnInModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -842,6 +843,30 @@
         }
 
     @Test
+    @DisableSceneContainer
+    fun updateBounds_fromGone_withoutTransitions() =
+            testScope.runTest {
+                // Start step is already at 1.0
+                val runningStep = TransitionStep(GONE, AOD, 1.0f, TransitionState.RUNNING)
+                val finishStep = TransitionStep(GONE, AOD, 1.0f, TransitionState.FINISHED)
+
+                val bounds by collectLastValue(underTest.bounds)
+                val top = 123f
+                val bottom = 456f
+
+                kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(runningStep)
+                runCurrent()
+                kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(finishStep)
+                runCurrent()
+                keyguardRootViewModel.onNotificationContainerBoundsChanged(top, bottom)
+                runCurrent()
+
+                assertThat(bounds).isEqualTo(
+                        NotificationContainerBounds(top = top, bottom = bottom)
+                )
+            }
+
+    @Test
     fun alphaOnFullQsExpansion() =
         testScope.runTest {
             val viewState = ViewStateAccessor()
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index 221b791..fbb07be 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -62,7 +62,9 @@
     <com.android.systemui.keyguard.ui.view.KeyguardRootView
         android:id="@id/keyguard_root_view"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+        android:layout_height="match_parent"
+        android:clipChildren="false"
+        />
 
     <!-- Shared container for the notification stack. Can be positioned by either
          the keyguard_root_view or notification_panel -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt
index 7a8c82c..4fd5456 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt
@@ -37,10 +37,17 @@
     private lateinit var rootView: ViewGroup
     private var translationMax = 0f
 
+    /**
+     * Initializes the animator, it is allowed to call this method multiple times, for example
+     * to update the rootView or maximum translation
+     */
     fun init(rootView: ViewGroup, translationMax: Float) {
+        if (!::rootView.isInitialized) {
+            progressProvider.addCallback(this)
+        }
+
         this.rootView = rootView
         this.translationMax = translationMax
-        progressProvider.addCallback(this)
     }
 
     override fun onTransitionStarted() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
index 7170be61..19d918f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
@@ -17,16 +17,21 @@
 package com.android.keyguard
 
 import android.content.Context
-import android.view.ViewGroup
-import com.android.systemui.res.R
+import android.view.View
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.res.R
+import com.android.systemui.shared.R as sharedR
+import com.android.systemui.shade.NotificationShadeWindowView
 import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator
 import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END
 import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START
 import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
 import com.android.systemui.unfold.SysUIUnfoldScope
-import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.dagger.NaturalRotation
 import javax.inject.Inject
 
 /**
@@ -38,8 +43,10 @@
 @Inject
 constructor(
     private val context: Context,
+    private val keyguardRootView: KeyguardRootView,
+    private val shadeWindowView: NotificationShadeWindowView,
     statusBarStateController: StatusBarStateController,
-    unfoldProgressProvider: NaturalRotationUnfoldProgressProvider,
+    @NaturalRotation unfoldProgressProvider: UnfoldTransitionProgressProvider,
 ) {
 
     /** Certain views only need to move if they are not currently centered */
@@ -50,27 +57,94 @@
     private val filterKeyguard: () -> Boolean = { statusBarStateController.getState() == KEYGUARD }
 
     private val translateAnimator by lazy {
+        val smartSpaceViews = if (MigrateClocksToBlueprint.isEnabled) {
+            // Use scrollX instead of translationX as translation is already set by [AodBurnInLayer]
+            val scrollXTranslation = { view: View, translation: Float ->
+                view.scrollX = -translation.toInt()
+            }
+
+            setOf(
+                ViewIdToTranslate(
+                    viewId = sharedR.id.date_smartspace_view,
+                    direction = START,
+                    shouldBeAnimated = filterKeyguard,
+                    translateFunc = scrollXTranslation,
+                ),
+                ViewIdToTranslate(
+                    viewId = sharedR.id.bc_smartspace_view,
+                    direction = START,
+                    shouldBeAnimated = filterKeyguard,
+                    translateFunc = scrollXTranslation,
+                ),
+                ViewIdToTranslate(
+                    viewId = sharedR.id.weather_smartspace_view,
+                    direction = START,
+                    shouldBeAnimated = filterKeyguard,
+                    translateFunc = scrollXTranslation,
+                )
+            )
+        } else {
+            setOf(ViewIdToTranslate(
+                viewId = R.id.keyguard_status_area,
+                direction = START,
+                shouldBeAnimated = filterKeyguard,
+                translateFunc = { view, value ->
+                    (view as? KeyguardStatusAreaView)?.translateXFromUnfold = value
+                }
+            ))
+        }
+
         UnfoldConstantTranslateAnimator(
             viewsIdToTranslate =
                 setOf(
-                    ViewIdToTranslate(R.id.keyguard_status_area, START, filterKeyguard,
-                        { view, value ->
-                            (view as? KeyguardStatusAreaView)?.translateXFromUnfold = value
-                        }),
                     ViewIdToTranslate(
-                        R.id.lockscreen_clock_view_large, START, filterKeyguardAndSplitShadeOnly),
-                    ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterKeyguard),
+                        viewId = R.id.lockscreen_clock_view_large,
+                        direction = START,
+                        shouldBeAnimated = filterKeyguardAndSplitShadeOnly
+                    ),
                     ViewIdToTranslate(
-                        R.id.notification_stack_scroller, END, filterKeyguardAndSplitShadeOnly),
-                    ViewIdToTranslate(R.id.start_button, START, filterKeyguard),
-                    ViewIdToTranslate(R.id.end_button, END, filterKeyguard)),
-            progressProvider = unfoldProgressProvider)
+                        viewId = R.id.lockscreen_clock_view,
+                        direction = START,
+                        shouldBeAnimated = filterKeyguard
+                    ),
+                    ViewIdToTranslate(
+                        viewId = R.id.notification_stack_scroller,
+                        direction = END,
+                        shouldBeAnimated = filterKeyguardAndSplitShadeOnly
+                    )
+                ) + smartSpaceViews,
+            progressProvider = unfoldProgressProvider
+        )
     }
 
-    /** Relies on the [parent] to locate views to translate. */
-    fun setup(parent: ViewGroup) {
+    private val shortcutButtonsAnimator by lazy {
+        UnfoldConstantTranslateAnimator(
+            viewsIdToTranslate =
+            setOf(
+                ViewIdToTranslate(
+                    viewId = R.id.start_button,
+                    direction = START,
+                    shouldBeAnimated = filterKeyguard
+                ),
+                ViewIdToTranslate(
+                    viewId = R.id.end_button,
+                    direction = END,
+                    shouldBeAnimated = filterKeyguard
+                )
+            ),
+            progressProvider = unfoldProgressProvider
+        )
+    }
+
+    /** Initializes the keyguard fold/unfold transition */
+    fun setup() {
         val translationMax =
             context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat()
-        translateAnimator.init(parent, translationMax)
+
+        translateAnimator.init(shadeWindowView, translationMax)
+
+        // Use keyguard root view as there is another instance of start/end buttons with the same ID
+        // outside of the keyguard root view
+        shortcutButtonsAnimator.init(keyguardRootView, translationMax)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 9d110e6..10d6881 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.util.kotlin.Quad
+import com.android.systemui.utils.coroutines.flow.mapLatestConflated
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -37,6 +38,7 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -96,7 +98,16 @@
             .filter { currentScene ->
                 currentScene == Scenes.Gone || currentScene == Scenes.Lockscreen
             }
-            .map { it == Scenes.Gone }
+            .mapLatestConflated { scene ->
+                if (scene == Scenes.Gone) {
+                    // Make sure device unlock status is definitely unlocked before we consider the
+                    // device "entered".
+                    deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked }
+                    true
+                } else {
+                    false
+                }
+            }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.Eagerly,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 48660f2..a595eeb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -50,6 +50,7 @@
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.notification.NotificationUtils.interpolate
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
 import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.kotlin.sample
@@ -77,7 +78,7 @@
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
-import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
+import kotlinx.coroutines.flow.transform
 
 /**
  * Encapsulates business-logic related to the keyguard but not to a more specific part within it.
@@ -111,7 +112,7 @@
                 keyguardTransitionInteractor.transitionState.map { step ->
                     val startingProgress = lastChangeStep.value
                     val progress = step.value
-                    if (step.to == AOD && progress >= startingProgress) {
+                    if (step.to == AOD && progress >= startingProgress && startingProgress < 1f) {
                         val adjustedProgress =
                             ((progress - startingProgress) / (1F - startingProgress)).coerceIn(
                                 0F,
@@ -327,28 +328,35 @@
      * This uses legacyShadeExpansion to process swipe up events. In the future, the touch input
      * signal should be sent directly to transitions.
      */
-    val dismissAlpha: Flow<Float?> =
+    val dismissAlpha: Flow<Float> =
         shadeRepository.legacyShadeExpansion
-            .filter { it < 1f }
             .sampleCombine(
                 statusBarState,
                 keyguardTransitionInteractor.currentKeyguardState,
+                keyguardTransitionInteractor.transitionState,
                 isKeyguardDismissible,
             )
-            .map {
-                (legacyShadeExpansion, statusBarState, currentKeyguardState, isKeyguardDismissible)
-                ->
+            .filter { (_, _, _, step, _) -> !step.transitionState.isTransitioning() }
+            .transform {
+                (
+                    legacyShadeExpansion,
+                    statusBarState,
+                    currentKeyguardState,
+                    step,
+                    isKeyguardDismissible) ->
                 if (
                     statusBarState == StatusBarState.KEYGUARD &&
                         isKeyguardDismissible &&
-                        currentKeyguardState == LOCKSCREEN
+                        currentKeyguardState == LOCKSCREEN &&
+                        legacyShadeExpansion != 1f
                 ) {
-                    MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, legacyShadeExpansion)
-                } else {
-                    null
+                    emit(MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, legacyShadeExpansion))
+                } else if (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f) {
+                    // Resets alpha state
+                    emit(1f)
                 }
             }
-            .onStart { emit(null) }
+            .onStart { emit(1f) }
             .distinctUntilChanged()
 
     val keyguardTranslationY: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 5027524..aefff7d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -66,7 +66,6 @@
 import kotlinx.coroutines.flow.combineTransform
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
@@ -236,7 +235,7 @@
                 // value emitted by any of them. Do not add flows that cannot make this guarantee.
                 merge(
                         alphaOnShadeExpansion,
-                        keyguardInteractor.dismissAlpha.filterNotNull(),
+                        keyguardInteractor.dismissAlpha,
                         alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
index 220d326..6a91d1b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
@@ -218,7 +218,7 @@
                         mediaFromRecPackageName = null
                         _currentMedia.value = sortedMap.values.toList()
                     }
-                } else if (sortedMap.size > sortedMedia.size && it.active) {
+                } else if (sortedMap.size > _currentMedia.value.size && it.active) {
                     _currentMedia.value = sortedMap.values.toList()
                 } else {
                     // When loading an update for an existing media control.
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 41cd2c4..99c95b5 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -24,6 +24,7 @@
 import static com.android.systemui.classifier.Classifier.BACK_GESTURE;
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED;
 
 import static java.util.stream.Collectors.joining;
 
@@ -1021,8 +1022,10 @@
             if (mIsTrackpadThreeFingerSwipe) {
                 // Trackpad back gestures don't have zones, so we don't need to check if the down
                 // event is within insets.
-                mAllowGesture = isBackAllowedCommon && isValidTrackpadBackGesture(
-                        true /* isTrackpadEvent */);
+                boolean trackpadGesturesEnabled =
+                        (mSysUiFlags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) == 0;
+                mAllowGesture = isBackAllowedCommon && trackpadGesturesEnabled
+                        && isValidTrackpadBackGesture(true /* isTrackpadEvent */);
             } else {
                 mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets
                     && isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 0304e73..1e689bd 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor
 import com.android.systemui.model.SceneContainerPlugin
 import com.android.systemui.model.SysUiState
 import com.android.systemui.model.updateFlags
@@ -81,6 +82,7 @@
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -120,6 +122,7 @@
     private val uiEventLogger: UiEventLogger,
     private val sceneBackInteractor: SceneBackInteractor,
     private val shadeSessionStorage: SessionStorage,
+    private val windowMgrLockscreenVisInteractor: WindowManagerLockscreenVisibilityInteractor,
 ) : CoreStartable {
     private val centralSurfaces: CentralSurfaces?
         get() = centralSurfacesOptLazy.get().getOrNull()
@@ -227,6 +230,25 @@
         handleDeviceUnlockStatus()
         handlePowerState()
         handleShadeTouchability()
+        handleSurfaceBehindKeyguardVisibility()
+    }
+
+    private fun handleSurfaceBehindKeyguardVisibility() {
+        applicationScope.launch {
+            sceneInteractor.currentScene.collectLatest { currentScene ->
+                if (currentScene == Scenes.Lockscreen) {
+                    // Wait for surface to become visible
+                    windowMgrLockscreenVisInteractor.surfaceBehindVisibility.first { it }
+                    // Make sure the device is actually unlocked before force-changing the scene
+                    deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked }
+                    // Override the current transition, if any, by forcing the scene to Gone
+                    sceneInteractor.changeScene(
+                        toScene = Scenes.Gone,
+                        loggingReason = "surface behind keyguard is visible",
+                    )
+                }
+            }
+        }
     }
 
     private fun handleBouncerImeVisibility() {
@@ -329,8 +351,7 @@
                                 Scenes.Gone to "device was unlocked in Bouncer scene"
                             } else {
                                 val prevScene = previousScene.value
-                                (prevScene
-                                    ?: Scenes.Gone) to
+                                (prevScene ?: Scenes.Gone) to
                                     "device was unlocked in Bouncer scene, from sceneKey=$prevScene"
                             }
                         isOnLockscreen ->
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
index 14659e7..f463cb5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
@@ -47,6 +47,7 @@
             viewsIdToTranslate =
                 setOf(
                     ViewIdToTranslate(R.id.quick_settings_panel, START, filterShade),
+                    ViewIdToTranslate(R.id.qs_footer_actions, START, filterShade),
                     ViewIdToTranslate(R.id.notification_stack_scroller, END, filterShade)),
             progressProvider = progressProvider)
     }
@@ -55,9 +56,8 @@
         UnfoldConstantTranslateAnimator(
             viewsIdToTranslate =
             setOf(
-                ViewIdToTranslate(R.id.statusIcons, END, filterShade),
+                ViewIdToTranslate(R.id.shade_header_system_icons, END, filterShade),
                 ViewIdToTranslate(R.id.privacy_container, END, filterShade),
-                ViewIdToTranslate(R.id.batteryRemainingIcon, END, filterShade),
                 ViewIdToTranslate(R.id.carrier_group, END, filterShade),
                 ViewIdToTranslate(R.id.clock, START, filterShade),
                 ViewIdToTranslate(R.id.date, START, filterShade)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 3826b50..262befc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -537,8 +537,6 @@
     private final KeyguardMediaController mKeyguardMediaController;
 
     private final Optional<KeyguardUnfoldTransition> mKeyguardUnfoldTransition;
-    private final Optional<NotificationPanelUnfoldAnimationController>
-            mNotificationPanelUnfoldAnimationController;
 
     /** The drag distance required to fully expand the split shade. */
     private int mSplitShadeFullTransitionDistance;
@@ -964,8 +962,6 @@
 
         mKeyguardUnfoldTransition = unfoldComponent.map(
                 SysUIUnfoldComponent::getKeyguardUnfoldTransition);
-        mNotificationPanelUnfoldAnimationController = unfoldComponent.map(
-                SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
 
         updateUserSwitcherFlags();
         mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
@@ -1131,9 +1127,6 @@
         mShadeHeaderController.init();
         mShadeHeaderController.setShadeCollapseAction(
                 () -> collapse(/* delayed= */ false , /* speedUpFactor= */ 1.0f));
-        mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
-        mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
-                controller.setup(mNotificationContainerParent));
 
         // Dreaming->Lockscreen
         collectFlow(
@@ -1511,9 +1504,6 @@
         if (!KeyguardBottomAreaRefactor.isEnabled()) {
             setKeyguardBottomAreaVisibility(mBarState, false);
         }
-
-        mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
-        mNotificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView));
     }
 
     private void attachSplitShadeMediaPlayerContainer(FrameLayout container) {
@@ -1797,6 +1787,7 @@
 
     private void updateKeyguardStatusViewAlignment(boolean animate) {
         boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
+        mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered));
         if (MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
             return;
@@ -1804,7 +1795,6 @@
         ConstraintLayout layout = mNotificationContainerParent;
         mKeyguardStatusViewController.updateAlignment(
                 layout, mSplitShadeEnabled, shouldBeCentered, animate);
-        mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered));
     }
 
     private boolean shouldKeyguardStatusViewBeCentered() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 1df085b..e41f99b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -32,6 +32,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.AuthKeyguardMessageArea;
+import com.android.keyguard.KeyguardUnfoldTransition;
 import com.android.keyguard.LockIconViewController;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityTransitionAnimator;
@@ -72,6 +73,7 @@
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.util.time.SystemClock;
 
@@ -174,6 +176,7 @@
             DozeScrimController dozeScrimController,
             NotificationShadeWindowController controller,
             Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider,
+            Optional<SysUIUnfoldComponent> unfoldComponent,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             NotificationInsetsController notificationInsetsController,
             AmbientState ambientState,
@@ -234,6 +237,14 @@
                 notificationLaunchAnimationInteractor.isLaunchAnimationRunning(),
                 this::setExpandAnimationRunning);
 
+        var keyguardUnfoldTransition = unfoldComponent.map(
+                SysUIUnfoldComponent::getKeyguardUnfoldTransition);
+        var notificationPanelUnfoldAnimationController = unfoldComponent.map(
+                SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
+
+        keyguardUnfoldTransition.ifPresent(KeyguardUnfoldTransition::setup);
+        notificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView));
+
         mClock = clock;
         if (featureFlagsClassic.isEnabled(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION)) {
             unfoldTransitionProgressProvider.ifPresent(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 6c76061..b2e0cd0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -30,6 +30,9 @@
 import com.android.systemui.privacy.OngoingPrivacyChip
 import com.android.systemui.privacy.PrivacyItem
 import com.android.systemui.res.R
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.TransitionKeys
 import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
 import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -57,6 +60,7 @@
     @Application private val applicationScope: CoroutineScope,
     context: Context,
     private val activityStarter: ActivityStarter,
+    private val sceneInteractor: SceneInteractor,
     shadeInteractor: ShadeInteractor,
     mobileIconsInteractor: MobileIconsInteractor,
     val mobileIconsViewModel: MobileIconsViewModel,
@@ -139,6 +143,15 @@
         clockInteractor.launchClockActivity()
     }
 
+    /** Notifies that the system icons container was clicked. */
+    fun onSystemIconContainerClicked() {
+        sceneInteractor.changeScene(
+            SceneFamilies.Home,
+            "ShadeHeaderViewModel.onSystemIconContainerClicked",
+            TransitionKeys.SlightlyFasterShadeCollapse,
+        )
+    }
+
     /** Notifies that the shadeCarrierGroup was clicked. */
     fun onShadeCarrierGroupClicked() {
         activityStarter.postStartActivityDismissingKeyguard(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index d1fabb1..9394249 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -358,11 +358,7 @@
      * @param whenMillis
      */
     public void setNotificationWhen(long whenMillis) {
-        if (mNotificationHeader == null) {
-            return;
-        }
-
-        final View timeView = mNotificationHeader.findViewById(com.android.internal.R.id.time);
+        final View timeView = mView.findViewById(com.android.internal.R.id.time);
 
         if (timeView instanceof DateTimeView) {
             ((DateTimeView) timeView).setTime(whenMillis);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 6a8c43a..b13630f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -53,6 +53,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.GoneToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGoneTransitionViewModel
@@ -120,6 +121,7 @@
     private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
     private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
     private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
+    private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel,
     private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
     private val lockscreenToGlanceableHubTransitionViewModel:
         LockscreenToGlanceableHubTransitionViewModel,
@@ -472,6 +474,9 @@
         // All transition view models are mututally exclusive, and safe to merge
         val alphaTransitions =
             merge(
+                keyguardInteractor.dismissAlpha.dumpWhileCollecting(
+                    "keyguardInteractor.dismissAlpha"
+                ),
                 alternateBouncerToGoneTransitionViewModel.notificationAlpha(viewState),
                 aodToGoneTransitionViewModel.notificationAlpha(viewState),
                 aodToLockscreenTransitionViewModel.notificationAlpha,
@@ -482,6 +487,7 @@
                 goneToAodTransitionViewModel.notificationAlpha,
                 goneToDreamingTransitionViewModel.lockscreenAlpha,
                 goneToDozingTransitionViewModel.lockscreenAlpha,
+                goneToLockscreenTransitionViewModel.lockscreenAlpha,
                 lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
                 lockscreenToGoneTransitionViewModel.notificationAlpha(viewState),
                 lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
@@ -498,24 +504,12 @@
                 // These remaining cases handle alpha changes within an existing state, such as
                 // shade expansion or swipe to dismiss
                 combineTransform(
-                    isOnLockscreenWithoutShade,
                     isTransitioningToHiddenKeyguard,
-                    shadeCollapseFadeIn,
                     alphaForShadeAndQsExpansion,
-                    keyguardInteractor.dismissAlpha.dumpWhileCollecting(
-                        "keyguardInteractor.keyguardAlpha"
-                    ),
                 ) {
-                    isOnLockscreenWithoutShade,
                     isTransitioningToHiddenKeyguard,
-                    shadeCollapseFadeIn,
-                    alphaForShadeAndQsExpansion,
-                    dismissAlpha ->
-                    if (isOnLockscreenWithoutShade) {
-                        if (!shadeCollapseFadeIn && dismissAlpha != null) {
-                            emit(dismissAlpha)
-                        }
-                    } else if (!isTransitioningToHiddenKeyguard) {
+                    alphaForShadeAndQsExpansion ->
+                    if (!isTransitioningToHiddenKeyguard) {
                         emit(alphaForShadeAndQsExpansion)
                     }
                 },
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
index a27989d..291903d 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.shade.NotificationPanelUnfoldAnimationController
 import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController
+import com.android.systemui.unfold.dagger.NaturalRotation
 import com.android.systemui.unfold.dagger.UnfoldBg
 import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
@@ -108,6 +109,13 @@
     abstract fun bindsFoldLightRevealOverlayAnimation(
         anim: FoldLightRevealOverlayAnimation
     ): FullscreenLightRevealAnimation
+
+    @Binds
+    @NaturalRotation
+    @SysUIUnfoldScope
+    abstract fun bindNaturalRotationUnfoldProgressProvider(
+        provider: NaturalRotationUnfoldProgressProvider
+    ): UnfoldTransitionProgressProvider
 }
 
 @SysUIUnfoldScope
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
index 3afca59..336183d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
@@ -18,26 +18,25 @@
 
 import android.testing.AndroidTestingRunner
 import android.view.View
-import android.view.ViewGroup
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.res.R
+import com.android.systemui.shade.NotificationShadeWindowView
 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
 import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
-import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.whenever
 
 /**
  * Translates items away/towards the hinge when the device is opened/closed. This is controlled by
@@ -47,11 +46,16 @@
 @RunWith(AndroidTestingRunner::class)
 class KeyguardUnfoldTransitionTest : SysuiTestCase() {
 
-    @Mock private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
+    private val kosmos = Kosmos()
 
-    @Captor private lateinit var progressListenerCaptor: ArgumentCaptor<TransitionProgressListener>
+    private val progressProvider: FakeUnfoldTransitionProvider =
+        kosmos.fakeUnfoldTransitionProgressProvider
 
-    @Mock private lateinit var parent: ViewGroup
+    @Mock
+    private lateinit var keyguardRootView: KeyguardRootView
+
+    @Mock
+    private lateinit var notificationShadeWindowView: NotificationShadeWindowView
 
     @Mock private lateinit var statusBarStateController: StatusBarStateController
 
@@ -66,13 +70,15 @@
         xTranslationMax =
             context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat()
 
-        underTest = KeyguardUnfoldTransition(context, statusBarStateController, progressProvider)
+        underTest = KeyguardUnfoldTransition(
+            context, keyguardRootView, notificationShadeWindowView,
+            statusBarStateController, progressProvider
+        )
 
-        underTest.setup(parent)
+        underTest.setup()
         underTest.statusViewCentered = false
 
-        verify(progressProvider).addCallback(capture(progressListenerCaptor))
-        progressListener = progressListenerCaptor.value
+        progressListener = progressProvider
     }
 
     @Test
@@ -81,7 +87,9 @@
         underTest.statusViewCentered = true
 
         val view = View(context)
-        whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+        whenever(keyguardRootView.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(
+            view
+        )
 
         progressListener.onTransitionStarted()
         assertThat(view.translationX).isZero()
@@ -101,7 +109,9 @@
         whenever(statusBarStateController.getState()).thenReturn(SHADE)
 
         val view = View(context)
-        whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+        whenever(keyguardRootView.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(
+            view
+        )
 
         progressListener.onTransitionStarted()
         assertThat(view.translationX).isZero()
@@ -121,7 +131,10 @@
         whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
 
         val view = View(context)
-        whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+        whenever(
+            notificationShadeWindowView
+                .findViewById<View>(R.id.lockscreen_clock_view_large)
+        ).thenReturn(view)
 
         progressListener.onTransitionStarted()
         assertThat(view.translationX).isZero()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index e02fb29..89e0971 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -30,7 +30,6 @@
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
 import com.android.systemui.dock.fakeDockManager
 import com.android.systemui.flags.BrokenWithSceneContainer
 import com.android.systemui.flags.DisableSceneContainer
@@ -608,31 +607,6 @@
 
     /** This handles security method NONE and screen off with lock timeout */
     @Test
-    fun dozingToGoneWithKeyguardNotShowing() =
-        testScope.runTest {
-            // GIVEN a prior transition has run to DOZING
-            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DOZING)
-            runCurrent()
-
-            // WHEN the device wakes up without a keyguard
-            keyguardRepository.setKeyguardShowing(false)
-            keyguardRepository.setKeyguardDismissible(true)
-            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(false)
-            powerInteractor.setAwakeForTest()
-            advanceTimeBy(60L)
-
-            assertThat(transitionRepository)
-                .startedTransition(
-                    to = KeyguardState.GONE,
-                    from = KeyguardState.DOZING,
-                    animatorAssertion = { it.isNotNull() }
-                )
-
-            coroutineContext.cancelChildren()
-        }
-
-    /** This handles security method NONE and screen off with lock timeout */
-    @Test
     @DisableSceneContainer
     fun dreamingToGoneWithKeyguardNotShowing() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 586adbd..74a2999 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -70,6 +70,7 @@
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
+import com.android.systemui.unfold.SysUIUnfoldComponent
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
@@ -85,6 +86,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Answers
 import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.Mockito.anyFloat
@@ -132,6 +134,8 @@
     private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
     @Mock private lateinit var notificationInsetsController: NotificationInsetsController
     @Mock private lateinit var mGlanceableHubContainerController: GlanceableHubContainerController
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private lateinit var sysUiUnfoldComponent: SysUIUnfoldComponent
     @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
     @Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
@@ -209,6 +213,7 @@
                 dozeScrimController,
                 notificationShadeWindowController,
                 unfoldTransitionProgressProvider,
+                Optional.of(sysUiUnfoldComponent),
                 keyguardUnlockAnimationController,
                 notificationInsetsController,
                 ambientState,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index e83a46b..31bd12f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -58,6 +58,7 @@
 import com.android.systemui.statusbar.phone.DozeServiceHost
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
+import com.android.systemui.unfold.SysUIUnfoldComponent
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -112,6 +113,7 @@
     @Mock private lateinit var shadeLogger: ShadeLogger
     @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var pulsingGestureListener: PulsingGestureListener
+    @Mock private lateinit var sysUiUnfoldComponent: SysUIUnfoldComponent
     @Mock
     private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
     @Mock private lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
@@ -181,6 +183,7 @@
                 dozeScrimController,
                 notificationShadeWindowController,
                 unfoldTransitionProgressProvider,
+                Optional.of(sysUiUnfoldComponent),
                 keyguardUnlockAnimationController,
                 notificationInsetsController,
                 ambientState,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
index a05a23b..293dc04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
@@ -27,6 +27,9 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
@@ -78,6 +81,22 @@
     }
 
     @Test
+    fun initMultipleTimes_onTransition_translationIsSetOnlyOnce() {
+        animator.init(parent, MAX_TRANSLATION)
+        animator.init(parent, MAX_TRANSLATION)
+        animator.init(parent, MAX_TRANSLATION)
+
+        // GIVEN one view with a matching id
+        val view = spy(View(context))
+        whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(view)
+        progressProvider.onTransitionStarted()
+
+        // WHEN the transition progresses, translation is updated once
+        progressProvider.onTransitionProgress(.5f)
+        verify(view).translationX = anyFloat()
+    }
+
+    @Test
     fun onTransition_oneMovesStartWithRTL() {
         // GIVEN one view with a matching id
         val view = View(context)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
index 02842cc..b5ca964 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory
 import com.android.systemui.scene.domain.interactor.SceneInteractor
@@ -34,6 +35,7 @@
 import com.android.systemui.util.mockito.whenever
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.TestScope
 
 /**
@@ -60,9 +62,11 @@
     ): WithDependencies {
         // Mock these until they are replaced by kosmos
         val currentKeyguardStateFlow = MutableSharedFlow<KeyguardState>()
+        val transitionStateFlow = MutableStateFlow(TransitionStep())
         val keyguardTransitionInteractor =
             mock<KeyguardTransitionInteractor>().also {
                 whenever(it.currentKeyguardState).thenReturn(currentKeyguardStateFlow)
+                whenever(it.transitionState).thenReturn(transitionStateFlow)
             }
         val configurationDimensionFlow = MutableSharedFlow<ConfigurationBasedDimensions>()
         configurationDimensionFlow.tryEmit(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
index d82286f..cf18c0e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.windowManagerLockscreenVisibilityInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testScope
@@ -67,5 +68,6 @@
         uiEventLogger = uiEventLogger,
         sceneBackInteractor = sceneBackInteractor,
         shadeSessionStorage = shadeSessionStorage,
+        windowMgrLockscreenVisInteractor = windowManagerLockscreenVisibilityInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
index 8d653f7..0e21698 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.activityStarter
+import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.privacyChipInteractor
 import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -33,6 +34,7 @@
             applicationScope = applicationCoroutineScope,
             context = applicationContext,
             activityStarter = activityStarter,
+            sceneInteractor = sceneInteractor,
             shadeInteractor = shadeInteractor,
             mobileIconsInteractor = mobileIconsInteractor,
             mobileIconsViewModel = mobileIconsViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index d00eedf..299486f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.goneToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.goneToDozingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.goneToDreamingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.goneToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenToDreamingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGoneTransitionViewModel
@@ -70,6 +71,7 @@
         goneToAodTransitionViewModel = goneToAodTransitionViewModel,
         goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
         goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel,
+        goneToLockscreenTransitionViewModel = goneToLockscreenTransitionViewModel,
         glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
         lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
         lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/NaturalRotation.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/NaturalRotation.kt
new file mode 100644
index 0000000..be02487
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/NaturalRotation.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.unfold.dagger
+
+import javax.inject.Qualifier
+
+/** Qualifier annotation for a progress provider that emits animation events only when
+ * in natural rotation */
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class NaturalRotation
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 2607ed3..89c888c 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.MANAGE_CONTENT_CAPTURE;
 import static android.content.Context.CONTENT_CAPTURE_MANAGER_SERVICE;
+import static android.service.contentcapture.ContentCaptureService.ASSIST_CONTENT_ACTIVITY_START_KEY;
 import static android.service.contentcapture.ContentCaptureService.setClientState;
 import static android.view.contentcapture.ContentCaptureHelper.toList;
 import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_ALLOWLIST_DELAY_MS;
@@ -28,6 +29,7 @@
 import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_OPTIONAL_GROUPS_THRESHOLD;
 import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_REQUIRED_GROUPS_CONFIG;
 import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER;
+import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_ENABLE_ACTIVITY_START_ASSIST_CONTENT;
 import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE;
 import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_OK;
 import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_SECURITY_EXCEPTION;
@@ -44,6 +46,7 @@
 import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_WRITE_FINISHED;
 import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__REJECT_DATA_SHARE_REQUEST;
 import static com.android.internal.util.SyncResultReceiver.bundleFor;
+import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -52,10 +55,12 @@
 import android.app.ActivityThread;
 import android.app.admin.DevicePolicyCache;
 import android.app.assist.ActivityId;
+import android.app.assist.AssistContent;
 import android.content.ComponentName;
 import android.content.ContentCaptureOptions;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ActivityPresentationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -184,6 +189,9 @@
     @Nullable
     private boolean mDisabledByDeviceConfig;
 
+    @GuardedBy("mLock")
+    private boolean activityStartAssistDataEnabled;
+
     // Device-config settings that are cached and passed back to apps
     @GuardedBy("mLock")
     int mDevCfgLoggingLevel;
@@ -451,6 +459,8 @@
                 case DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_AUTO_DISCONNECT_TIMEOUT:
                     setFineTuneParamsFromDeviceConfig();
                     return;
+                case DEVICE_CONFIG_ENABLE_ACTIVITY_START_ASSIST_CONTENT:
+                    setActivityStartAssistDataEnabled();
                 default:
                     Slog.i(TAG, "Ignoring change on " + key);
             }
@@ -639,6 +649,15 @@
         final String enabled = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                 ContentCaptureManager.DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED);
         setDisabledByDeviceConfig(enabled);
+        setActivityStartAssistDataEnabled();
+    }
+
+    private void setActivityStartAssistDataEnabled() {
+        synchronized (mLock) {
+            this.activityStartAssistDataEnabled = DeviceConfig.getBoolean(
+                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                    DEVICE_CONFIG_ENABLE_ACTIVITY_START_ASSIST_CONTENT, false);
+        }
     }
 
     private void setDisabledByDeviceConfig(@Nullable String explicitlyEnabled) {
@@ -908,6 +927,9 @@
         pw.print(prefix2);
         pw.print("contentProtectionAutoDisconnectTimeoutMs: ");
         pw.println(mDevCfgContentProtectionAutoDisconnectTimeoutMs);
+        pw.print(prefix2);
+        pw.print("activityStartAssistDataEnabled: ");
+        pw.println(activityStartAssistDataEnabled);
         pw.print(prefix);
         pw.println("Global Options:");
         mGlobalContentCaptureOptions.dump(prefix2, pw);
@@ -1327,6 +1349,33 @@
         }
 
         @Override
+        @SuppressWarnings("GuardedBy")
+        public boolean sendActivityStartAssistData(@UserIdInt int userId,
+                @NonNull IBinder activityToken,
+                @NonNull Intent intentData) {
+            synchronized (mLock) {
+                if (!activityStartAssistDataEnabled) {
+                    return false;
+                }
+                Intent intent = new Intent(intentData);
+                intent.setFlags(intent.getFlags() & ~(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+                        | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION));
+                Bundle assistContentExtras = new Bundle();
+                assistContentExtras.putBoolean(ASSIST_CONTENT_ACTIVITY_START_KEY, true);
+                AssistContent assistContent = new AssistContent(assistContentExtras);
+                assistContent.setDefaultIntent(intent);
+
+                final Bundle activityAssistData = new Bundle();
+                activityAssistData.putParcelable(ASSIST_KEY_CONTENT, assistContent);
+                final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
+                if (service != null) {
+                    return service.sendActivityAssistDataLocked(activityToken, activityAssistData);
+                }
+            }
+            return false;
+        }
+
+        @Override
         public boolean sendActivityAssistData(@UserIdInt int userId, @NonNull IBinder activityToken,
                 @NonNull Bundle data) {
             synchronized (mLock) {
diff --git a/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java b/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java
index 0812fd9..de7341d 100644
--- a/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java
+++ b/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java
@@ -21,6 +21,7 @@
 import android.app.assist.ActivityId;
 import android.content.ComponentName;
 import android.content.ContentCaptureOptions;
+import android.content.Intent;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.service.contentcapture.ActivityEvent.ActivityEventType;
@@ -40,6 +41,14 @@
     public abstract boolean isContentCaptureServiceForUser(int uid, @UserIdInt int userId);
 
     /**
+     * Notifies the intelligence service of new intent data associated with an activity start event.
+     *
+     * @return {@code false} if there was no service set for the given user
+     */
+    public abstract boolean sendActivityStartAssistData(@UserIdInt int userId,
+            @NonNull IBinder activityToken, @NonNull Intent intentData);
+
+    /**
      * Notifies the intelligence service of new assist data for the given activity.
      *
      * @return {@code false} if there was no service set for the given user
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 5fd0253..2d5f38e 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3406,6 +3406,31 @@
         }
     }
 
+    boolean requestDisplayPower(int displayId, boolean on) {
+        synchronized (mSyncRoot) {
+            final var display = mLogicalDisplayMapper.getDisplayLocked(displayId);
+            if (display == null) {
+                Slog.w(TAG, "requestDisplayPower: Cannot find a display with displayId="
+                        + displayId);
+                return false;
+            }
+            final BrightnessPair brightnessPair = mDisplayBrightnesses.get(displayId);
+            var runnable = display.getPrimaryDisplayDeviceLocked().requestDisplayStateLocked(
+                    on ? Display.STATE_ON : Display.STATE_OFF,
+                    on ? brightnessPair.brightness : PowerManager.BRIGHTNESS_OFF_FLOAT,
+                    brightnessPair.sdrBrightness,
+                    display.getDisplayOffloadSessionLocked());
+            if (runnable == null) {
+                Slog.w(TAG, "requestDisplayPower: Cannot update the power state to ON=" + on
+                        + " for a display with displayId=" + displayId + ", runnable is null");
+                return false;
+            }
+            runnable.run();
+            Slog.i(TAG, "requestDisplayPower(displayId=" + displayId + ", on=" + on + ")");
+        }
+        return true;
+    }
+
     /**
      * This is the object that everything in the display manager locks on.
      * We make it an inner class within the {@link DisplayManagerService} to so that it is
@@ -4629,6 +4654,12 @@
             DisplayManagerService.this.enableConnectedDisplay(displayId, false);
         }
 
+        @EnforcePermission(MANAGE_DISPLAYS)
+        public boolean requestDisplayPower(int displayId, boolean on) {
+            requestDisplayPower_enforcePermission();
+            return DisplayManagerService.this.requestDisplayPower(displayId, on);
+        }
+
         @EnforcePermission(RESTRICT_DISPLAY_MODES)
         @Override // Binder call
         public void requestDisplayModes(IBinder token, int displayId, @Nullable int[] modeIds) {
diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
index 8c39d7d..d973b71 100644
--- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
+++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
@@ -106,6 +106,10 @@
                 return setDisplayEnabled(true);
             case "disable-display":
                 return setDisplayEnabled(false);
+            case "power-on":
+                return requestDisplayPower(true);
+            case "power-off":
+                return requestDisplayPower(false);
             default:
                 return handleDefaultCommands(cmd);
         }
@@ -592,4 +596,21 @@
         mService.enableConnectedDisplay(displayId, enable);
         return 0;
     }
+
+    private int requestDisplayPower(boolean enable) {
+        final String displayIdText = getNextArg();
+        if (displayIdText == null) {
+            getErrPrintWriter().println("Error: no displayId specified");
+            return 1;
+        }
+        final int displayId;
+        try {
+            displayId = Integer.parseInt(displayIdText);
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: invalid displayId: '" + displayIdText + "'");
+            return 1;
+        }
+        mService.requestDisplayPower(displayId, enable);
+        return 0;
+    }
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 0bd40d1..e5dbce9 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -3352,6 +3352,10 @@
         mPointerIconCache.setPointerFillStyle(fillStyle);
     }
 
+    void setPointerScale(float scale) {
+        mPointerIconCache.setPointerScale(scale);
+    }
+
     interface KeyboardBacklightControllerInterface {
         default void incrementKeyboardBacklight(int deviceId) {}
         default void decrementKeyboardBacklight(int deviceId) {}
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 9585b49..593b091 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -16,6 +16,7 @@
 
 package com.android.server.input;
 
+import static android.view.PointerIcon.DEFAULT_POINTER_SCALE;
 import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK;
 import static android.view.flags.Flags.enableVectorCursorA11ySettings;
 
@@ -101,7 +102,9 @@
                 Map.entry(Settings.Secure.getUriFor(Settings.Secure.STYLUS_POINTER_ICON_ENABLED),
                         (reason) -> updateStylusPointerIconEnabled()),
                 Map.entry(Settings.System.getUriFor(Settings.System.POINTER_FILL_STYLE),
-                        (reason) -> updatePointerFillStyleFromSettings()));
+                        (reason) -> updatePointerFillStyleFromSettings()),
+                Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SCALE),
+                        (reason) -> updatePointerScaleFromSettings()));
     }
 
     /**
@@ -277,4 +280,14 @@
                 UserHandle.USER_CURRENT);
         mService.setPointerFillStyle(pointerFillStyle);
     }
+
+    private void updatePointerScaleFromSettings() {
+        if (!enableVectorCursorA11ySettings()) {
+            return;
+        }
+        final float pointerScale = Settings.System.getFloatForUser(mContext.getContentResolver(),
+                Settings.System.POINTER_SCALE, DEFAULT_POINTER_SCALE,
+                UserHandle.USER_CURRENT);
+        mService.setPointerScale(pointerScale);
+    }
 }
diff --git a/services/core/java/com/android/server/input/PointerIconCache.java b/services/core/java/com/android/server/input/PointerIconCache.java
index 936e17f..44622d8 100644
--- a/services/core/java/com/android/server/input/PointerIconCache.java
+++ b/services/core/java/com/android/server/input/PointerIconCache.java
@@ -16,6 +16,7 @@
 
 package com.android.server.input;
 
+import static android.view.PointerIcon.DEFAULT_POINTER_SCALE;
 import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK;
 
 import android.annotation.NonNull;
@@ -63,6 +64,8 @@
     @GuardedBy("mLoadedPointerIconsByDisplayAndType")
     private @PointerIcon.PointerIconVectorStyleFill int mPointerIconFillStyle =
             POINTER_ICON_VECTOR_STYLE_FILL_BLACK;
+    @GuardedBy("mLoadedPointerIconsByDisplayAndType")
+    private float mPointerIconScale = DEFAULT_POINTER_SCALE;
 
     private final DisplayManager.DisplayListener mDisplayListener =
             new DisplayManager.DisplayListener() {
@@ -117,6 +120,11 @@
         mUiThreadHandler.post(() -> handleSetPointerFillStyle(fillStyle));
     }
 
+    /** Set the scale for vector pointer icons. */
+    public void setPointerScale(float scale) {
+        mUiThreadHandler.post(() -> handleSetPointerScale(scale));
+    }
+
     /**
      * Get a loaded system pointer icon. This will fetch the icon from the cache, or load it if
      * it isn't already cached.
@@ -137,7 +145,7 @@
                 theme.applyStyle(PointerIcon.vectorFillStyleToResource(mPointerIconFillStyle),
                         /* force= */ true);
                 icon = PointerIcon.getLoadedSystemIcon(new ContextThemeWrapper(context, theme),
-                        type, mUseLargePointerIcons);
+                        type, mUseLargePointerIcons, mPointerIconScale);
                 iconsByType.put(type, icon);
             }
             return Objects.requireNonNull(icon);
@@ -215,6 +223,19 @@
         mNative.reloadPointerIcons();
     }
 
+    @android.annotation.UiThread
+    private void handleSetPointerScale(float scale) {
+        synchronized (mLoadedPointerIconsByDisplayAndType) {
+            if (mPointerIconScale == scale) {
+                return;
+            }
+            mPointerIconScale = scale;
+            // Clear all cached icons on all displays.
+            mLoadedPointerIconsByDisplayAndType.clear();
+        }
+        mNative.reloadPointerIcons();
+    }
+
     // Updates the cached display density for the given displayId, and returns true if
     // the cached density changed.
     @GuardedBy("mLoadedPointerIconsByDisplayAndType")
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index d236d7a..1a0ffb7 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -5241,7 +5241,8 @@
             return;
         }
 
-        final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
+        final int userId = mCurrentUserId;
+        final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
 
         boolean reenableMinimumNonAuxSystemImes = false;
         // TODO: The following code should find better place to live.
@@ -5304,18 +5305,18 @@
         updateDefaultVoiceImeIfNeededLocked();
 
         // TODO: Instantiate mSwitchingController for each user.
-        if (settings.getUserId() == mSwitchingController.getUserId()) {
+        if (userId == mSwitchingController.getUserId()) {
             mSwitchingController.resetCircularListLocked(settings.getMethodMap());
         } else {
             mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
                     mContext, settings.getMethodMap(), mCurrentUserId);
         }
         // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
-        if (settings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) {
+        if (userId == mHardwareKeyboardShortcutController.getUserId()) {
             mHardwareKeyboardShortcutController.reset(settings.getMethodMap());
         } else {
             mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
-                    settings.getMethodMap(), settings.getUserId());
+                    settings.getMethodMap(), userId);
         }
 
         sendOnNavButtonFlagsChangedLocked();
@@ -5323,7 +5324,7 @@
         // Notify InputMethodListListeners of the new installed InputMethods.
         final List<InputMethodInfo> inputMethodList = settings.getMethodList();
         mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED,
-                settings.getUserId(), 0 /* unused */, inputMethodList).sendToTarget();
+                userId, 0 /* unused */, inputMethodList).sendToTarget();
     }
 
     @GuardedBy("ImfLock.class")
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index cd1d799..ea71953 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -165,6 +165,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
@@ -368,18 +369,32 @@
                 return false;
             }
 
+            int userId = UserHandle.getUserId(uid);
+
+            boolean isInSetup =
+                    getSecureInt(Settings.Secure.USER_SETUP_COMPLETE, userId)
+                             .map(setupState -> setupState == 0)
+                             .orElse(false);
+            if (isInSetup) {
+                return true;
+            }
+
+            boolean isInDeferredSetup =
+                    getSecureInt(Settings.Secure.USER_SETUP_PERSONALIZATION_STATE, userId)
+                            .map(state ->
+                                    state == Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED)
+                            .orElse(false);
+            return isInDeferredSetup;
+        }
+
+        private Optional<Integer> getSecureInt(String settingName, int userId) {
             try {
-                int userId = UserHandle.getUserId(uid);
-                boolean isInSetup = Settings.Secure.getIntForUser(mContext.getContentResolver(),
-                        Settings.Secure.USER_SETUP_COMPLETE, userId) == 0;
-                boolean isInDeferredSetup = Settings.Secure.getIntForUser(
-                        mContext.getContentResolver(),
-                        Settings.Secure.USER_SETUP_PERSONALIZATION_STATE, userId)
-                        == Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED;
-                return isInSetup || isInDeferredSetup;
+                return Optional.of(
+                        Settings.Secure.getIntForUser(
+                                mContext.getContentResolver(), settingName, userId));
             } catch (Settings.SettingNotFoundException e) {
-                Slog.w(LOG_TAG, "Failed to check if the user is in restore: " + e);
-                return false;
+                Slog.i(LOG_TAG, "Setting " + settingName + " not found", e);
+                return Optional.empty();
             }
         }
 
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 11b9e77..80c262a 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -17,6 +17,7 @@
 package com.android.server.power;
 
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
@@ -197,9 +198,10 @@
             FaceDownDetector faceDownDetector, ScreenUndimDetector screenUndimDetector,
             Executor backgroundExecutor, PowerManagerFlags powerManagerFlags, Injector injector) {
         mContext = context;
+        mInjector = (injector == null) ? new RealInjector() : injector;
         mFlags = powerManagerFlags;
         mBatteryStats = batteryStats;
-        mAppOps = mContext.getSystemService(AppOpsManager.class);
+        mAppOps = mInjector.getAppOpsManager(context);
         mSuspendBlocker = suspendBlocker;
         mPolicy = policy;
         mFaceDownDetector = faceDownDetector;
@@ -230,7 +232,6 @@
         mShowWirelessChargingAnimationConfig = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_showBuiltinWirelessChargingAnim);
 
-        mInjector = (injector == null) ? new RealInjector() : injector;
         mWakeLockLog = mInjector.getWakeLockLog(context);
         // Initialize interactive state for battery stats.
         try {
@@ -264,6 +265,7 @@
     /**
      * Called when a wake lock is acquired.
      */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     public void onWakeLockAcquired(int flags, String tag, String packageName,
             int ownerUid, int ownerPid, WorkSource workSource, String historyTag,
             IWakeLockCallback callback) {
@@ -273,27 +275,28 @@
                     + ", ownerUid=" + ownerUid + ", ownerPid=" + ownerPid
                     + ", workSource=" + workSource);
         }
-        notifyWakeLockListener(callback, tag, true, ownerUid, flags);
-        final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
-        if (monitorType >= 0) {
-            try {
-                final boolean unimportantForLogging = ownerUid == Process.SYSTEM_UID
-                        && (flags & PowerManager.UNIMPORTANT_FOR_LOGGING) != 0;
-                if (workSource != null) {
-                    mBatteryStats.noteStartWakelockFromSource(workSource, ownerPid, tag,
-                            historyTag, monitorType, unimportantForLogging);
-                } else {
-                    mBatteryStats.noteStartWakelock(ownerUid, ownerPid, tag, historyTag,
-                            monitorType, unimportantForLogging);
-                    // XXX need to deal with disabled operations.
-                    mAppOps.startOpNoThrow(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName);
-                }
-            } catch (RemoteException ex) {
-                // Ignore
-            }
-        }
-
+        notifyWakeLockListener(callback, tag, true, ownerUid, ownerPid, flags, workSource,
+                packageName, historyTag);
         if (!mFlags.improveWakelockLatency()) {
+            final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
+            if (monitorType >= 0) {
+                try {
+                    final boolean unimportantForLogging = ownerUid == Process.SYSTEM_UID
+                            && (flags & PowerManager.UNIMPORTANT_FOR_LOGGING) != 0;
+                    if (workSource != null) {
+                        mBatteryStats.noteStartWakelockFromSource(workSource, ownerPid, tag,
+                                historyTag, monitorType, unimportantForLogging);
+                    } else {
+                        mBatteryStats.noteStartWakelock(ownerUid, ownerPid, tag, historyTag,
+                                monitorType, unimportantForLogging);
+                        // XXX need to deal with disabled operations.
+                        mAppOps.startOpNoThrow(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName,
+                                false, null, null);
+                    }
+                } catch (RemoteException ex) {
+                    // Ignore
+                }
+            }
             mWakeLockLog.onWakeLockAcquired(tag, ownerUid, flags, /*eventTime=*/ -1);
         }
         mWakefulnessSessionObserver.onWakeLockAcquired(flags);
@@ -404,6 +407,7 @@
     /**
      * Called when a wake lock is released.
      */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     public void onWakeLockReleased(int flags, String tag, String packageName,
             int ownerUid, int ownerPid, WorkSource workSource, String historyTag,
             IWakeLockCallback callback, int releaseReason) {
@@ -413,23 +417,24 @@
                     + ", ownerUid=" + ownerUid + ", ownerPid=" + ownerPid
                     + ", workSource=" + workSource);
         }
-        notifyWakeLockListener(callback, tag, false, ownerUid, flags);
-        final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
-        if (monitorType >= 0) {
-            try {
-                if (workSource != null) {
-                    mBatteryStats.noteStopWakelockFromSource(workSource, ownerPid, tag,
-                            historyTag, monitorType);
-                } else {
-                    mBatteryStats.noteStopWakelock(ownerUid, ownerPid, tag,
-                            historyTag, monitorType);
-                    mAppOps.finishOp(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName);
-                }
-            } catch (RemoteException ex) {
-                // Ignore
-            }
-        }
+        notifyWakeLockListener(callback, tag, false, ownerUid, ownerPid, flags, workSource,
+                packageName, historyTag);
         if (!mFlags.improveWakelockLatency()) {
+            final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
+            if (monitorType >= 0) {
+                try {
+                    if (workSource != null) {
+                        mBatteryStats.noteStopWakelockFromSource(workSource, ownerPid, tag,
+                                historyTag, monitorType);
+                    } else {
+                        mBatteryStats.noteStopWakelock(ownerUid, ownerPid, tag,
+                                historyTag, monitorType);
+                        mAppOps.finishOp(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName, null);
+                    }
+                } catch (RemoteException ex) {
+                    // Ignore
+                }
+            }
             mWakeLockLog.onWakeLockReleased(tag, ownerUid, /*eventTime=*/ -1);
         }
         mWakefulnessSessionObserver.onWakeLockReleased(flags, releaseReason);
@@ -1049,24 +1054,75 @@
     }
 
     private void notifyWakeLockListener(IWakeLockCallback callback, String tag, boolean isEnabled,
-            int ownerUid, int flags) {
-        if (callback != null) {
-            long currentTime = mInjector.currentTimeMillis();
-            mHandler.post(() -> {
-                try {
-                    if (mFlags.improveWakelockLatency()) {
+            int ownerUid, int ownerPid, int flags, WorkSource workSource, String packageName,
+            String historyTag) {
+        if (mFlags.improveWakelockLatency()) {
+            if (callback != null) {
+                long currentTime = mInjector.currentTimeMillis();
+                mHandler.post(() -> {
+                    try {
                         if (isEnabled) {
-                            mWakeLockLog.onWakeLockAcquired(tag, ownerUid, flags, currentTime);
+                            notifyWakelockAcquisition(tag, ownerUid, ownerPid, flags,
+                                    workSource, packageName, historyTag, currentTime);
                         } else {
-                            mWakeLockLog.onWakeLockReleased(tag, ownerUid, currentTime);
+                            notifyWakelockRelease(tag, ownerUid, ownerPid, flags,
+                                    workSource, packageName, historyTag, currentTime);
                         }
+                        callback.onStateChanged(isEnabled);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Wakelock.mCallback [" + tag + "] is already dead.", e);
                     }
-                    callback.onStateChanged(isEnabled);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Wakelock.mCallback [" + tag + "] is already dead.", e);
-                }
-            });
+                });
+            }
         }
+
+    }
+
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    private void notifyWakelockAcquisition(String tag, int ownerUid, int ownerPid, int flags,
+            WorkSource workSource, String packageName, String historyTag, long currentTime) {
+        final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
+        if (monitorType >= 0) {
+            try {
+                final boolean unimportantForLogging = ownerUid == Process.SYSTEM_UID
+                        && (flags & PowerManager.UNIMPORTANT_FOR_LOGGING) != 0;
+                if (workSource != null) {
+                    mBatteryStats.noteStartWakelockFromSource(workSource, ownerPid, tag,
+                            historyTag, monitorType, unimportantForLogging);
+                } else {
+                    mBatteryStats.noteStartWakelock(ownerUid, ownerPid, tag, historyTag,
+                            monitorType, unimportantForLogging);
+                    // XXX need to deal with disabled operations.
+                    mAppOps.startOpNoThrow(
+                            AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName,
+                            false, null, null);
+                }
+            } catch (RemoteException ex) {
+                // Do Nothing
+            }
+        }
+        mWakeLockLog.onWakeLockAcquired(tag, ownerUid, flags, currentTime);
+    }
+
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    private void notifyWakelockRelease(String tag, int ownerUid, int ownerPid, int flags,
+            WorkSource workSource, String packageName, String historyTag, long currentTime) {
+        final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
+        if (monitorType >= 0) {
+            try {
+                if (workSource != null) {
+                    mBatteryStats.noteStopWakelockFromSource(workSource, ownerPid, tag,
+                            historyTag, monitorType);
+                } else {
+                    mBatteryStats.noteStopWakelock(ownerUid, ownerPid, tag,
+                            historyTag, monitorType);
+                    mAppOps.finishOp(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName, null);
+                }
+            } catch (RemoteException ex) {
+                // Ignore
+            }
+        }
+        mWakeLockLog.onWakeLockReleased(tag, ownerUid, currentTime);
     }
 
     private final class NotifierHandler extends Handler {
@@ -1114,6 +1170,11 @@
          * Gets the WakeLockLog object
          */
         WakeLockLog getWakeLockLog(Context context);
+
+        /**
+         * Gets the AppOpsManager system service
+         */
+        AppOpsManager getAppOpsManager(Context context);
     }
 
     static class RealInjector implements Injector {
@@ -1126,5 +1187,10 @@
         public WakeLockLog getWakeLockLog(Context context) {
             return new WakeLockLog(context);
         }
+
+        @Override
+        public AppOpsManager getAppOpsManager(Context context) {
+            return context.getSystemService(AppOpsManager.class);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 78a6816..15f4c8c 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -156,6 +156,7 @@
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.wm.ActivityRecord.State.DESTROYED;
@@ -6032,14 +6033,7 @@
                             true /* activityChange */, true /* updateOomAdj */,
                             true /* addPendingTopUid */);
                 }
-                final ContentCaptureManagerInternal contentCaptureService =
-                        LocalServices.getService(ContentCaptureManagerInternal.class);
-                if (contentCaptureService != null) {
-                    contentCaptureService.notifyActivityEvent(mUserId, mActivityComponent,
-                            ActivityEvent.TYPE_ACTIVITY_STARTED,
-                            new ActivityId(getTask() != null ? getTask().mTaskId : INVALID_TASK_ID,
-                                    shareableActivityToken));
-                }
+                mAtmService.mH.post(this::notifyActivityStartedToContentCaptureService);
                 break;
             case PAUSED:
                 mAtmService.updateBatteryStats(this, false);
@@ -6071,6 +6065,22 @@
         }
     }
 
+    private void notifyActivityStartedToContentCaptureService() {
+        final ContentCaptureManagerInternal contentCaptureService =
+                LocalServices.getService(ContentCaptureManagerInternal.class);
+        if (contentCaptureService != null) {
+            // For ACTIVITY_STARTED content capture is directly invoked to avoid persisting
+            // to UsageStats.
+            contentCaptureService.notifyActivityEvent(mUserId, mActivityComponent,
+                    ActivityEvent.TYPE_ACTIVITY_STARTED,
+                    new ActivityId(getTask() != null ? getTask().mTaskId : INVALID_TASK_ID,
+                            shareableActivityToken));
+
+            contentCaptureService.sendActivityStartAssistData(mUserId,
+                                shareableActivityToken, intent);
+        }
+    }
+
     State getState() {
         return mState;
     }
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index 6499556..78dbc60 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -533,8 +533,7 @@
         val packageState =
             packageManagerLocal.withFilteredSnapshot(Binder.getCallingUid(), userId).use {
                 it.getPackageState(packageName)
-            }
-                ?: return PackageManager.PERMISSION_DENIED
+            } ?: return PackageManager.PERMISSION_DENIED
 
         val isPermissionGranted =
             service.getState { isPermissionGranted(packageState, userId, permissionName, deviceId) }
@@ -1164,8 +1163,7 @@
         val packageState =
             packageManagerLocal.withFilteredSnapshot(Binder.getCallingUid(), userId).use {
                 it.getPackageState(packageName)
-            }
-                ?: return false
+            } ?: return false
 
         service.getState {
             if (isPermissionGranted(packageState, userId, permissionName, deviceId)) {
@@ -1216,8 +1214,7 @@
         val packageState =
             packageManagerLocal.withFilteredSnapshot(callingUid, userId).use {
                 it.getPackageState(packageName)
-            }
-                ?: return false
+            } ?: return false
         val appId = packageState.appId
         if (UserHandle.getAppId(callingUid) != appId) {
             return false
@@ -1546,8 +1543,7 @@
         val packageState =
             packageManagerLocal.withFilteredSnapshot(callingUid, userId).use {
                 it.getPackageState(packageName)
-            }
-                ?: return null
+            } ?: return null
         val androidPackage = packageState.androidPackage ?: return null
 
         val isCallerPrivileged =
@@ -1710,8 +1706,7 @@
                     PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER,
                     userId
                 )
-                ?.let { ArraySet(permissionNames).apply { this += it }.toList() }
-                ?: permissionNames
+                ?.let { ArraySet(permissionNames).apply { this += it }.toList() } ?: permissionNames
 
         setAllowlistedRestrictedPermissionsUnchecked(
             androidPackage,
@@ -2753,27 +2748,25 @@
             ) {
                 return false
             }
-            return try {
-                val contentResolver = context.contentResolver
-                val userId = UserHandle.getUserId(uid)
-                val isInSetup =
-                    Settings.Secure.getIntForUser(
-                        contentResolver,
-                        Settings.Secure.USER_SETUP_COMPLETE,
-                        userId
-                    ) == 0
-                val isInDeferredSetup =
-                    Settings.Secure.getIntForUser(
-                        contentResolver,
-                        Settings.Secure.USER_SETUP_PERSONALIZATION_STATE,
-                        userId
-                    ) == Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED
-                isInSetup || isInDeferredSetup
-            } catch (e: Settings.SettingNotFoundException) {
-                Slog.w(LOG_TAG, "Failed to check if the user is in restore: $e")
-                false
-            }
+
+            val userId = UserHandle.getUserId(uid)
+
+            val isInSetup = getSecureInt(Settings.Secure.USER_SETUP_COMPLETE, userId) == 0
+            if (isInSetup) return true
+
+            val isInDeferredSetup =
+                getSecureInt(Settings.Secure.USER_SETUP_PERSONALIZATION_STATE, userId) ==
+                    Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED
+            return isInDeferredSetup
         }
+
+        private fun getSecureInt(settingName: String, userId: Int): Int? =
+            try {
+                Settings.Secure.getIntForUser(context.contentResolver, settingName, userId)
+            } catch (e: Settings.SettingNotFoundException) {
+                Slog.i(LOG_TAG, "Setting $settingName not found", e)
+                null
+            }
     }
 
     private class OnPermissionsChangeListeners(looper: Looper) : Handler(looper) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index d0eb83a..211ab03 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -2569,6 +2569,63 @@
     }
 
     @Test
+    public void testPowerOnAndOffInternalDisplay() {
+        manageDisplaysPermission(/* granted= */ true);
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+
+        callback.expectsEvent(EVENT_DISPLAY_ADDED);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        callback.waitForExpectedEvent();
+
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+
+        assertThat(displayDevice.getDisplayDeviceInfoLocked().committedState)
+                .isEqualTo(Display.STATE_ON);
+
+        assertThat(displayManager.requestDisplayPower(display.getDisplayIdLocked(), false))
+                .isTrue();
+
+        assertThat(displayDevice.getDisplayDeviceInfoLocked().committedState)
+                .isEqualTo(Display.STATE_OFF);
+
+        assertThat(displayManager.requestDisplayPower(display.getDisplayIdLocked(), true))
+                .isTrue();
+
+        assertThat(displayDevice.getDisplayDeviceInfoLocked().committedState)
+                .isEqualTo(Display.STATE_ON);
+    }
+
+    @Test
+    public void testPowerOnAndOffInternalDisplay_withoutPermission_shouldThrowException() {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerService.BinderService bs = displayManager.new BinderService();
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+
+        callback.expectsEvent(EVENT_DISPLAY_ADDED);
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+        callback.waitForExpectedEvent();
+
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        var displayId = display.getDisplayIdLocked();
+
+        assertThat(displayDevice.getDisplayDeviceInfoLocked().committedState)
+                .isEqualTo(Display.STATE_ON);
+
+        assertThrows(SecurityException.class, () -> bs.requestDisplayPower(displayId, true));
+        assertThrows(SecurityException.class, () -> bs.requestDisplayPower(displayId, false));
+    }
+
+    @Test
     public void testEnableExternalDisplay_withDisplayManagement_shouldSignalDisplayAdded() {
         when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
         manageDisplaysPermission(/* granted= */ true);
@@ -3529,6 +3586,7 @@
 
         public void setDisplayDeviceInfo(DisplayDeviceInfo displayDeviceInfo) {
             mDisplayDeviceInfo = displayDeviceInfo;
+            mDisplayDeviceInfo.committedState = Display.STATE_ON;
         }
 
         @Override
@@ -3558,5 +3616,14 @@
         public Display.Mode getUserPreferredDisplayModeLocked() {
             return mPreferredMode;
         }
+
+        @Override
+        public Runnable requestDisplayStateLocked(
+                final int state,
+                final float brightnessState,
+                final float sdrBrightnessState,
+                @Nullable DisplayOffloadSessionImpl displayOffloadSession) {
+            return () -> mDisplayDeviceInfo.committedState = state;
+        }
     }
 }
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index 4460c6a..ce2bb95 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -31,6 +31,7 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.res.Resources;
 import android.hardware.SensorManager;
@@ -41,7 +42,6 @@
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.VibrationAttributes;
 import android.os.Vibrator;
 import android.os.test.TestLooper;
@@ -82,8 +82,12 @@
     @Mock private StatusBarManagerInternal mStatusBarManagerInternal;
     @Mock private WakeLockLog mWakeLockLog;
 
+    @Mock private IBatteryStats mBatteryStats;
+
     @Mock private PowerManagerFlags mPowerManagerFlags;
 
+    @Mock private AppOpsManager mAppOpsManager;
+
     private PowerManagerService mService;
     private Context mContextSpy;
     private Resources mResourcesSpy;
@@ -230,7 +234,7 @@
     public void testOnWakeLockListener_RemoteException_NoRethrow() {
         when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
         createNotifier();
-
+        clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
         IWakeLockCallback exceptingCallback = new IWakeLockCallback.Stub() {
             @Override public void onStateChanged(boolean enabled) throws RemoteException {
                 throw new RemoteException("Just testing");
@@ -245,6 +249,7 @@
         verifyZeroInteractions(mWakeLockLog);
         mTestLooper.dispatchAll();
         verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, 1);
+
         mNotifier.onWakeLockAcquired(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag",
                 "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
                 exceptingCallback);
@@ -277,6 +282,115 @@
         verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, -1);
     }
 
+
+    @Test
+    public void testOnWakeLockListener_FullWakeLock_ProcessesOnHandler() throws RemoteException {
+        when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
+        createNotifier();
+
+        IWakeLockCallback exceptingCallback = new IWakeLockCallback.Stub() {
+            @Override public void onStateChanged(boolean enabled) throws RemoteException {
+                throw new RemoteException("Just testing");
+            }
+        };
+        clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+        final int uid = 1234;
+        final int pid = 5678;
+
+        // Release the wakelock
+        mNotifier.onWakeLockReleased(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+                exceptingCallback);
+
+        // No interaction because we expect that to happen in async
+        verifyZeroInteractions(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+        // Progressing the looper, and validating all the interactions
+        mTestLooper.dispatchAll();
+        verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, 1);
+        verify(mBatteryStats).noteStopWakelock(uid, pid, "wakelockTag", /* historyTag= */ null,
+                BatteryStats.WAKE_TYPE_FULL);
+        verify(mAppOpsManager).finishOp(AppOpsManager.OP_WAKE_LOCK, uid,
+                "my.package.name", null);
+
+        clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+        // Acquire the wakelock
+        mNotifier.onWakeLockAcquired(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+                exceptingCallback);
+
+        // No interaction because we expect that to happen in async
+        verifyNoMoreInteractions(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+        // Progressing the looper, and validating all the interactions
+        mTestLooper.dispatchAll();
+        verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", uid,
+                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, 1);
+        verify(mBatteryStats).noteStartWakelock(uid, pid, "wakelockTag", /* historyTag= */ null,
+                BatteryStats.WAKE_TYPE_FULL, false);
+        verify(mAppOpsManager).startOpNoThrow(AppOpsManager.OP_WAKE_LOCK, uid,
+                "my.package.name", false, null, null);
+
+        // Test with improveWakelockLatency flag false, hence the wakelock log will run on the same
+        // thread
+        clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+        when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(false);
+
+        mNotifier.onWakeLockAcquired(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+                exceptingCallback);
+        verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", uid,
+                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, -1);
+
+        mNotifier.onWakeLockReleased(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+                exceptingCallback);
+        verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, -1);
+    }
+
+    @Test
+    public void testOnWakeLockListener_FullWakeLock_ProcessesInSync() throws RemoteException {
+        createNotifier();
+
+        IWakeLockCallback exceptingCallback = new IWakeLockCallback.Stub() {
+            @Override public void onStateChanged(boolean enabled) throws RemoteException {
+                throw new RemoteException("Just testing");
+            }
+        };
+        clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+        final int uid = 1234;
+        final int pid = 5678;
+
+        // Release the wakelock
+        mNotifier.onWakeLockReleased(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+                exceptingCallback);
+
+        verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, -1);
+        verify(mBatteryStats).noteStopWakelock(uid, pid, "wakelockTag", /* historyTag= */ null,
+                BatteryStats.WAKE_TYPE_FULL);
+        verify(mAppOpsManager).finishOp(AppOpsManager.OP_WAKE_LOCK, uid,
+                "my.package.name", null);
+
+        clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+        // Acquire the wakelock
+        mNotifier.onWakeLockAcquired(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+                exceptingCallback);
+
+        mTestLooper.dispatchAll();
+        verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", uid,
+                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, -1);
+        verify(mBatteryStats).noteStartWakelock(uid, pid, "wakelockTag", /* historyTag= */ null,
+                BatteryStats.WAKE_TYPE_FULL, false);
+        verify(mAppOpsManager).startOpNoThrow(AppOpsManager.OP_WAKE_LOCK, uid,
+                "my.package.name", false, null, null);
+    }
+
     private final PowerManagerService.Injector mInjector = new PowerManagerService.Injector() {
         @Override
         Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats,
@@ -365,13 +479,17 @@
             public WakeLockLog getWakeLockLog(Context context) {
                 return mWakeLockLog;
             }
+
+            @Override
+            public AppOpsManager getAppOpsManager(Context context) {
+                return mAppOpsManager;
+            }
         };
 
         mNotifier = new Notifier(
                 mTestLooper.getLooper(),
                 mContextSpy,
-                IBatteryStats.Stub.asInterface(ServiceManager.getService(
-                        BatteryStats.SERVICE_NAME)),
+                mBatteryStats,
                 mInjector.createSuspendBlocker(mService, "testBlocker"),
                 null,
                 null,
diff --git a/tests/Input/assets/testPointerScale.png b/tests/Input/assets/testPointerScale.png
new file mode 100644
index 0000000..54d37c2
--- /dev/null
+++ b/tests/Input/assets/testPointerScale.png
Binary files differ
diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
index dac4253..d196b85 100644
--- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
+++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
@@ -93,7 +93,29 @@
             PointerIcon.getLoadedSystemIcon(
                 ContextThemeWrapper(context, theme),
                 PointerIcon.TYPE_ARROW,
-                /* useLargeIcons= */ false)
+                /* useLargeIcons= */ false,
+                /* pointerScale= */ 1f)
+
+        pointerIcon.getBitmap().assertAgainstGolden(
+            screenshotRule,
+            testName.methodName,
+            exactScreenshotMatcher
+        )
+    }
+
+    @Test
+    fun testPointerScale() {
+        assumeTrue(enableVectorCursors())
+        assumeTrue(enableVectorCursorA11ySettings())
+
+        val pointerScale = 2f
+
+        val pointerIcon =
+            PointerIcon.getLoadedSystemIcon(
+                context,
+                PointerIcon.TYPE_ARROW,
+                /* useLargeIcons= */ false,
+                pointerScale)
 
         pointerIcon.getBitmap().assertAgainstGolden(
             screenshotRule,