Merge "Auto hide IllustrationPreference when lottie file is empty" into main
diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java
index 7724c23..92543b1 100644
--- a/core/java/android/app/AppCompatTaskInfo.java
+++ b/core/java/android/app/AppCompatTaskInfo.java
@@ -32,6 +32,11 @@
     public boolean topActivityEligibleForLetterboxEducation;
 
     /**
+     * Whether the letterbox education is enabled
+     */
+    public boolean isLetterboxEducationEnabled;
+
+    /**
      * Whether the direct top activity is in size compat mode on foreground.
      */
     public boolean topActivityInSizeCompat;
@@ -178,6 +183,7 @@
                     == that.topActivityEligibleForUserAspectRatioButton
                 && topActivityEligibleForLetterboxEducation
                     == that.topActivityEligibleForLetterboxEducation
+                && isLetterboxEducationEnabled == that.isLetterboxEducationEnabled
                 && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition
                 && topActivityLetterboxHorizontalPosition
                     == that.topActivityLetterboxHorizontalPosition
@@ -192,6 +198,7 @@
      * Reads the AppCompatTaskInfo from a parcel.
      */
     void readFromParcel(Parcel source) {
+        isLetterboxEducationEnabled = source.readBoolean();
         topActivityInSizeCompat = source.readBoolean();
         topActivityEligibleForLetterboxEducation = source.readBoolean();
         isLetterboxDoubleTapEnabled = source.readBoolean();
@@ -212,6 +219,7 @@
      */
     @Override
     public void writeToParcel(Parcel dest, int flags) {
+        dest.writeBoolean(isLetterboxEducationEnabled);
         dest.writeBoolean(topActivityInSizeCompat);
         dest.writeBoolean(topActivityEligibleForLetterboxEducation);
         dest.writeBoolean(isLetterboxDoubleTapEnabled);
@@ -232,6 +240,7 @@
         return "AppCompatTaskInfo { topActivityInSizeCompat=" + topActivityInSizeCompat
                 + " topActivityEligibleForLetterboxEducation= "
                 + topActivityEligibleForLetterboxEducation
+                + "isLetterboxEducationEnabled= " + isLetterboxEducationEnabled
                 + " isLetterboxDoubleTapEnabled= " + isLetterboxDoubleTapEnabled
                 + " topActivityEligibleForUserAspectRatioButton= "
                 + topActivityEligibleForUserAspectRatioButton
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 2d0f6fc..54f6909 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -482,7 +482,8 @@
             UID_STATE_FOREGROUND_SERVICE,
             UID_STATE_FOREGROUND,
             UID_STATE_BACKGROUND,
-            UID_STATE_CACHED
+            UID_STATE_CACHED,
+            UID_STATE_NONEXISTENT
     })
     public @interface UidState {}
 
@@ -566,6 +567,12 @@
     public static final int MIN_PRIORITY_UID_STATE = UID_STATE_CACHED;
 
     /**
+     * Special uid state: The UID is not running
+     * @hide
+     */
+    public static final int UID_STATE_NONEXISTENT = Integer.MAX_VALUE;
+
+    /**
      * Resolves the first unrestricted state given an app op.
      * @param op The op to resolve.
      * @return The last restricted UID state.
@@ -596,6 +603,7 @@
             UID_STATE_FOREGROUND,
             UID_STATE_BACKGROUND,
             UID_STATE_CACHED
+            // UID_STATE_NONEXISTENT isn't a real UID state, so it is excluded
     };
 
     /** @hide */
@@ -615,6 +623,8 @@
                 return "bg";
             case UID_STATE_CACHED:
                 return "cch";
+            case UID_STATE_NONEXISTENT:
+                return "gone";
             default:
                 return "unknown";
         }
diff --git a/core/java/android/companion/flags.aconfig b/core/java/android/companion/flags.aconfig
index 8458857..36d0e08 100644
--- a/core/java/android/companion/flags.aconfig
+++ b/core/java/android/companion/flags.aconfig
@@ -39,3 +39,11 @@
     description: "Expose perm sync user consent API"
     bug: "309528663"
 }
+
+flag {
+    name: "ongoing_perm_sync"
+    is_exported: true
+    namespace: "companion"
+    description: "Enable ongoing perm sync"
+    bug: "338469649"
+}
\ No newline at end of file
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index cee8d96..061e7f7 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -254,6 +254,14 @@
 }
 
 flag {
+    name: "wait_application_killed"
+    namespace: "package_manager_service"
+    description: "Feature flag to control whether to wait until the application is killed when clear application data"
+    bug: "31009094"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "component_state_changed_metrics"
     namespace: "package_manager_service"
     description: "Feature flag to log the metrics when the component state is changed."
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 0e28560..2ca58d1 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -166,6 +166,16 @@
 }
 
 flag {
+    name: "finish_running_ops_for_killed_packages"
+    namespace: "permissions"
+    description: "Finish all appops for a dead app process"
+    bug: "234630570"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "runtime_permission_appops_mapping_enabled"
     is_fixed_read_only: true
     namespace: "permissions"
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
index 29a6db6..8237b20 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
@@ -105,6 +105,21 @@
     public static final String SERVICE_INTERFACE =
             "android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService";
 
+    // TODO(339594686): make API
+    /**
+     * @hide
+     */
+    public static final String REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY =
+            "register_model_update_callback";
+    /**
+     * @hide
+     */
+    public static final String MODEL_LOADED_BUNDLE_KEY = "model_loaded";
+    /**
+     * @hide
+     */
+    public static final String MODEL_UNLOADED_BUNDLE_KEY = "model_unloaded";
+
     private IRemoteStorageService mRemoteStorageService;
 
     /**
diff --git a/core/java/android/window/TransitionFilter.java b/core/java/android/window/TransitionFilter.java
index 64fe66e..ec4e3e9 100644
--- a/core/java/android/window/TransitionFilter.java
+++ b/core/java/android/window/TransitionFilter.java
@@ -25,6 +25,7 @@
 import android.app.ActivityManager;
 import android.app.WindowConfiguration;
 import android.content.ComponentName;
+import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.view.WindowManager;
@@ -180,6 +181,7 @@
 
         public @ContainerOrder int mOrder = CONTAINER_ORDER_ANY;
         public ComponentName mTopActivity;
+        public IBinder mLaunchCookie;
 
         public Requirement() {
         }
@@ -193,6 +195,7 @@
             mMustBeTask = in.readBoolean();
             mOrder = in.readInt();
             mTopActivity = in.readTypedObject(ComponentName.CREATOR);
+            mLaunchCookie = in.readStrongBinder();
         }
 
         /** Go through changes and find if at-least one change matches this filter */
@@ -231,6 +234,9 @@
                 if (mMustBeTask && change.getTaskInfo() == null) {
                     continue;
                 }
+                if (!matchesCookie(change.getTaskInfo())) {
+                    continue;
+                }
                 return true;
             }
             return false;
@@ -247,13 +253,25 @@
             return false;
         }
 
+        private boolean matchesCookie(ActivityManager.RunningTaskInfo info) {
+            if (mLaunchCookie == null) return true;
+            if (info == null) return false;
+            for (IBinder cookie : info.launchCookies) {
+                if (mLaunchCookie.equals(cookie)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
         /** Check if the request matches this filter. It may generate false positives */
         boolean matches(@NonNull TransitionRequestInfo request) {
             // Can't check modes/order since the transition hasn't been built at this point.
             if (mActivityType == ACTIVITY_TYPE_UNDEFINED) return true;
             return request.getTriggerTask() != null
                     && request.getTriggerTask().getActivityType() == mActivityType
-                    && matchesTopActivity(request.getTriggerTask(), null /* activityCmp */);
+                    && matchesTopActivity(request.getTriggerTask(), null /* activityCmp */)
+                    && matchesCookie(request.getTriggerTask());
         }
 
         @Override
@@ -267,6 +285,7 @@
             dest.writeBoolean(mMustBeTask);
             dest.writeInt(mOrder);
             dest.writeTypedObject(mTopActivity, flags);
+            dest.writeStrongBinder(mLaunchCookie);
         }
 
         @NonNull
@@ -307,6 +326,7 @@
             out.append(" mustBeTask=" + mMustBeTask);
             out.append(" order=" + containerOrderToString(mOrder));
             out.append(" topActivity=").append(mTopActivity);
+            out.append(" launchCookie=").append(mLaunchCookie);
             out.append("}");
             return out.toString();
         }
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index f931a76..e29f256 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -47,7 +47,7 @@
     void animateExpandNotificationsPanel();
     void animateExpandSettingsPanel(String subPanel);
     void animateCollapsePanels();
-    void togglePanel();
+    void toggleNotificationsPanel();
 
     void showWirelessChargingAnimation(int batteryLevel);
 
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1ef5c3f..5bd2033 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4652,6 +4652,19 @@
      -->
     <string-array name="config_companionDeviceCerts" translatable="false"></string-array>
 
+    <!-- A list of packages that auto-enable permissions sync feature.
+         Note that config_companionPermSyncEnabledPackages and config_companionPermSyncEnabledCerts
+         are parallel arrays.
+     -->
+    <string-array name="config_companionPermSyncEnabledPackages" translatable="false"></string-array>
+
+    <!-- A list of SHA256 Certificates corresponding to config_companionPermSyncEnabledPackages.
+         Note that config_companionPermSyncEnabledPackages and config_companionPermSyncEnabledCerts
+         are parallel arrays.
+         Example: "1A:2B:3C:4D"
+     -->
+    <string-array name="config_companionPermSyncEnabledCerts" translatable="false"></string-array>
+
     <!-- The package name for the default wellbeing app.
          This package must be trusted, as it has the permissions to control other applications
          on the device.
@@ -4704,6 +4717,13 @@
     <!-- The component name for the default system on-device sandboxed inference service. -->
     <string name="config_defaultOnDeviceSandboxedInferenceService" translatable="false"></string>
 
+    <!-- The broadcast intent name for notifying when the on-device model is loading  -->
+    <string name="config_onDeviceIntelligenceModelLoadedBroadcastKey" translatable="false"></string>
+
+    <!-- The broadcast intent name for notifying when the on-device model has been unloaded  -->
+    <string name="config_onDeviceIntelligenceModelUnloadedBroadcastKey" translatable="false"></string>
+
+
     <!-- Component name that accepts ACTION_SEND intents for requesting ambient context consent for
          wearable sensing. -->
     <string translatable="false" name="config_defaultWearableSensingConsentComponent"></string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ec63ea3..ae79a4c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -675,6 +675,8 @@
   <java-symbol type="string" name="config_companionDeviceManagerPackage" />
   <java-symbol type="array" name="config_companionDevicePackages" />
   <java-symbol type="array" name="config_companionDeviceCerts" />
+  <java-symbol type="array" name="config_companionPermSyncEnabledPackages" />
+  <java-symbol type="array" name="config_companionPermSyncEnabledCerts" />
   <java-symbol type="string" name="config_default_dns_server" />
   <java-symbol type="string" name="config_ethernet_iface_regex" />
   <java-symbol type="string" name="not_checked" />
@@ -3941,6 +3943,8 @@
   <java-symbol type="string" name="config_defaultWearableSensingService" />
   <java-symbol type="string" name="config_defaultOnDeviceIntelligenceService" />
   <java-symbol type="string" name="config_defaultOnDeviceSandboxedInferenceService" />
+  <java-symbol type="string" name="config_onDeviceIntelligenceModelLoadedBroadcastKey" />
+  <java-symbol type="string" name="config_onDeviceIntelligenceModelUnloadedBroadcastKey" />
   <java-symbol type="string" name="config_retailDemoPackage" />
   <java-symbol type="string" name="config_retailDemoPackageSignature" />
 
diff --git a/core/tests/coretests/src/android/graphics/drawable/IconTest.java b/core/tests/coretests/src/android/graphics/drawable/IconTest.java
index 950925f..e0c3b04 100644
--- a/core/tests/coretests/src/android/graphics/drawable/IconTest.java
+++ b/core/tests/coretests/src/android/graphics/drawable/IconTest.java
@@ -475,8 +475,8 @@
         final Icon ic = Icon.createWithBitmap(bm);
         final Drawable drawable = ic.loadDrawable(mContext);
 
-        assertThat(drawable.getIntrinsicWidth()).isEqualTo(maxWidth);
-        assertThat(drawable.getIntrinsicHeight()).isEqualTo(maxHeight);
+        assertThat(Math.abs(drawable.getIntrinsicWidth() - maxWidth)).isLessThan(2);
+        assertThat(Math.abs(drawable.getIntrinsicHeight() - maxHeight)).isLessThan(2);
     }
 
     @Test
diff --git a/graphics/java/android/graphics/RecordingCanvas.java b/graphics/java/android/graphics/RecordingCanvas.java
index 635e78e..cc5b3b9 100644
--- a/graphics/java/android/graphics/RecordingCanvas.java
+++ b/graphics/java/android/graphics/RecordingCanvas.java
@@ -40,7 +40,7 @@
 
     /** @hide */
     private static int getPanelFrameSize() {
-        final int DefaultSize = 100 * 1024 * 1024; // 100 MB;
+        final int DefaultSize = 150 * 1024 * 1024; // 150 MB;
         return Math.max(SystemProperties.getInt("ro.hwui.max_texture_allocation_size", DefaultSize),
                 DefaultSize);
     }
@@ -262,7 +262,7 @@
     protected void throwIfCannotDraw(Bitmap bitmap) {
         super.throwIfCannotDraw(bitmap);
         int bitmapSize = bitmap.getByteCount();
-        if (bitmapSize > MAX_BITMAP_SIZE) {
+        if (bitmap.getConfig() != Bitmap.Config.HARDWARE && bitmapSize > MAX_BITMAP_SIZE) {
             throw new RuntimeException(
                     "Canvas: trying to draw too large(" + bitmapSize + "bytes) bitmap.");
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 5c292f1..bfac24b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -188,6 +188,11 @@
      */
     private boolean mHasShownUserAspectRatioSettingsButton = false;
 
+    /**
+     * This is true when the rechability education is displayed for the first time.
+     */
+    private boolean mIsFirstReachabilityEducationRunning;
+
     public CompatUIController(@NonNull Context context,
             @NonNull ShellInit shellInit,
             @NonNull ShellController shellController,
@@ -252,9 +257,35 @@
             removeLayouts(taskInfo.taskId);
             return;
         }
-
+        // We're showing the first reachability education so we ignore incoming TaskInfo
+        // until the education flow has completed or we double tap.
+        if (mIsFirstReachabilityEducationRunning) {
+            return;
+        }
+        if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) {
+            if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled) {
+                createOrUpdateLetterboxEduLayout(taskInfo, taskListener);
+            } else if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap) {
+                // In this case the app is letterboxed and the letterbox education
+                // is disabled. In this case we need to understand if it's the first
+                // time we show the reachability education. When this is happening
+                // we need to ignore all the incoming TaskInfo until the education
+                // completes. If we come from a double tap we follow the normal flow.
+                final boolean topActivityPillarboxed =
+                        taskInfo.appCompatTaskInfo.isTopActivityPillarboxed();
+                final boolean isFirstTimeHorizontalReachabilityEdu = topActivityPillarboxed
+                        && !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(taskInfo);
+                final boolean isFirstTimeVerticalReachabilityEdu = !topActivityPillarboxed
+                        && !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(taskInfo);
+                if (isFirstTimeHorizontalReachabilityEdu || isFirstTimeVerticalReachabilityEdu) {
+                    mIsFirstReachabilityEducationRunning = true;
+                    mCompatUIConfiguration.setSeenLetterboxEducation(taskInfo.userId);
+                    createOrUpdateReachabilityEduLayout(taskInfo, taskListener);
+                    return;
+                }
+            }
+        }
         createOrUpdateCompatLayout(taskInfo, taskListener);
-        createOrUpdateLetterboxEduLayout(taskInfo, taskListener);
         createOrUpdateRestartDialogLayout(taskInfo, taskListener);
         if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) {
             createOrUpdateReachabilityEduLayout(taskInfo, taskListener);
@@ -589,6 +620,7 @@
     private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo,
             @NonNull ShellTaskOrganizer.TaskListener taskListener) {
         // We need to update the UI otherwise it will not be shown until the user relaunches the app
+        mIsFirstReachabilityEducationRunning = false;
         createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index b41454d..5af4c3b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -41,15 +41,6 @@
     public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean(
             "persist.wm.debug.desktop_change_display", false);
 
-
-    /**
-     * Flag to indicate that desktop stashing is enabled.
-     * When enabled, swiping home from desktop stashes the open apps. Next app that launches,
-     * will be added to the desktop.
-     */
-    private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean(
-            "persist.wm.debug.desktop_stashing", false);
-
     /**
      * Flag to indicate whether to apply shadows to windows in desktop mode.
      */
@@ -109,14 +100,6 @@
     }
 
     /**
-     * Return {@code true} if desktop task stashing is enabled when going home.
-     * Allows users to use home screen to add tasks to desktop.
-     */
-    public static boolean isStashingEnabled() {
-        return IS_STASHING_ENABLED;
-    }
-
-    /**
      * Return whether to use window shadows.
      *
      * @param isFocusedWindow whether the window to apply shadows to is focused
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 2d508b2..6bbc8fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -48,7 +48,6 @@
         val activeTasks: ArraySet<Int> = ArraySet(),
         val visibleTasks: ArraySet<Int> = ArraySet(),
         val minimizedTasks: ArraySet<Int> = ArraySet(),
-        var stashed: Boolean = false
     )
 
     // Token of the current wallpaper activity, used to remove it when the last task is removed
@@ -95,10 +94,8 @@
         visibleTasksListeners[visibleTasksListener] = executor
         displayData.keyIterator().forEach { displayId ->
             val visibleTasksCount = getVisibleTaskCount(displayId)
-            val stashed = isStashed(displayId)
             executor.execute {
                 visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount)
-                visibleTasksListener.onStashedChanged(displayId, stashed)
             }
         }
     }
@@ -400,26 +397,6 @@
     }
 
     /**
-     * Update stashed status on display with id [displayId]
-     */
-    fun setStashed(displayId: Int, stashed: Boolean) {
-        val data = displayData.getOrCreate(displayId)
-        val oldValue = data.stashed
-        data.stashed = stashed
-        if (oldValue != stashed) {
-            KtProtoLog.d(
-                    WM_SHELL_DESKTOP_MODE,
-                    "DesktopTaskRepo: mark stashed=%b displayId=%d",
-                    stashed,
-                    displayId
-            )
-            visibleTasksListeners.forEach { (listener, executor) ->
-                executor.execute { listener.onStashedChanged(displayId, stashed) }
-            }
-        }
-    }
-
-    /**
      * Removes and returns the bounds saved before maximizing the given task.
      */
     fun removeBoundsBeforeMaximize(taskId: Int): Rect? {
@@ -433,13 +410,6 @@
         boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds))
     }
 
-    /**
-     * Check if display with id [displayId] has desktop tasks stashed
-     */
-    fun isStashed(displayId: Int): Boolean {
-        return displayData[displayId]?.stashed ?: false
-    }
-
     internal fun dump(pw: PrintWriter, prefix: String) {
         val innerPrefix = "$prefix  "
         pw.println("${prefix}DesktopModeTaskRepository")
@@ -455,7 +425,6 @@
             pw.println("${prefix}Display $displayId:")
             pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}")
             pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}")
-            pw.println("${innerPrefix}stashed=${data.stashed}")
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index b0d5923..b2bdbfe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -240,34 +240,6 @@
         }
     }
 
-    /**
-     * Stash desktop tasks on display with id [displayId].
-     *
-     * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps
-     * launched in this state will be added to the desktop. Existing desktop tasks will be brought
-     * back to front during the launch.
-     */
-    fun stashDesktopApps(displayId: Int) {
-        if (DesktopModeStatus.isStashingEnabled()) {
-            KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps")
-            desktopModeTaskRepository.setStashed(displayId, true)
-        }
-    }
-
-    /**
-     * Clear the stashed state for the given display
-     */
-    fun hideStashedDesktopApps(displayId: Int) {
-        if (DesktopModeStatus.isStashingEnabled()) {
-            KtProtoLog.v(
-                    WM_SHELL_DESKTOP_MODE,
-                    "DesktopTasksController: hideStashedApps displayId=%d",
-                    displayId
-            )
-            desktopModeTaskRepository.setStashed(displayId, false)
-        }
-    }
-
     /** Get number of tasks that are marked as visible */
     fun getVisibleTaskCount(displayId: Int): Int {
         return desktopModeTaskRepository.getVisibleTaskCount(displayId)
@@ -871,8 +843,6 @@
         val result = triggerTask?.let { task ->
             when {
                 request.type == TRANSIT_TO_BACK -> handleBackNavigation(task)
-                // If display has tasks stashed, handle as stashed launch
-                task.isStashed -> handleStashedTaskLaunch(task, transition)
                 // Check if the task has a top transparent activity
                 shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task)
                 // Check if fullscreen task should be updated
@@ -911,12 +881,8 @@
                 .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
     }
 
-    private val TaskInfo.isStashed: Boolean
-        get() = desktopModeTaskRepository.isStashed(displayId)
-
-    private fun shouldLaunchAsModal(task: TaskInfo): Boolean {
-        return Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)
-    }
+    private fun shouldLaunchAsModal(task: TaskInfo) =
+        Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)
 
     private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean {
         return Flags.enableDesktopWindowingWallpaperActivity() &&
@@ -976,24 +942,6 @@
         return null
     }
 
-    private fun handleStashedTaskLaunch(
-            task: RunningTaskInfo,
-            transition: IBinder
-    ): WindowContainerTransaction {
-        KtProtoLog.d(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: launch apps with stashed on transition taskId=%d",
-                task.taskId
-        )
-        val wct = WindowContainerTransaction()
-        val taskToMinimize =
-                bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
-        addMoveToDesktopChanges(wct, task)
-        desktopModeTaskRepository.setStashed(task.displayId, false)
-        addPendingMinimizeTransition(transition, taskToMinimize)
-        return wct
-    }
-
     // Always launch transparent tasks in fullscreen.
     private fun handleTransparentTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
         // Already fullscreen, no-op.
@@ -1467,20 +1415,6 @@
             ) { c -> c.showDesktopApps(displayId, remoteTransition) }
         }
 
-        override fun stashDesktopApps(displayId: Int) {
-            ExecutorUtils.executeRemoteCallWithTaskPermission(
-                    controller,
-                    "stashDesktopApps"
-            ) { c -> c.stashDesktopApps(displayId) }
-        }
-
-        override fun hideStashedDesktopApps(displayId: Int) {
-            ExecutorUtils.executeRemoteCallWithTaskPermission(
-                    controller,
-                    "hideStashedDesktopApps"
-            ) { c -> c.hideStashedDesktopApps(displayId) }
-        }
-
         override fun showDesktopApp(taskId: Int) {
             ExecutorUtils.executeRemoteCallWithTaskPermission(
                     controller,
@@ -1488,6 +1422,20 @@
             ) { c -> c.moveTaskToFront(taskId) }
         }
 
+        override fun stashDesktopApps(displayId: Int) {
+            KtProtoLog.w(
+                WM_SHELL_DESKTOP_MODE,
+                "IDesktopModeImpl: stashDesktopApps is deprecated"
+            )
+        }
+
+        override fun hideStashedDesktopApps(displayId: Int) {
+            KtProtoLog.w(
+                WM_SHELL_DESKTOP_MODE,
+                "IDesktopModeImpl: hideStashedDesktopApps is deprecated"
+            )
+        }
+
         override fun getVisibleTaskCount(displayId: Int): Int {
             val result = IntArray(1)
             ExecutorUtils.executeRemoteCallWithTaskPermission(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index fa43522..c36f8de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -28,10 +28,10 @@
     /** Show apps on the desktop on the given display */
     void showDesktopApps(int displayId, in RemoteTransition remoteTransition);
 
-    /** Stash apps on the desktop to allow launching another app from home screen */
+    /** @deprecated use {@link #showDesktopApps} instead. */
     void stashDesktopApps(int displayId);
 
-    /** Hide apps that may be stashed */
+    /** @deprecated this is no longer supported. */
     void hideStashedDesktopApps(int displayId);
 
     /** Bring task with the given id to front */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index 7b1ef5c..a749019 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -39,7 +39,7 @@
      * @param isSysUiStateValid Is SysUI state valid or not.
      * @param flag Current SysUI state.
      */
-    default void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) {
+    default void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 139cde2..85f9194 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -847,7 +847,7 @@
         }
     }
 
-    private void onSystemUiStateChanged(boolean isValidState, int flag) {
+    private void onSystemUiStateChanged(boolean isValidState, long flag) {
         mTouchHandler.onSystemUiStateChanged(isValidState);
     }
 
@@ -1195,7 +1195,7 @@
         }
 
         @Override
-        public void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) {
+        public void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {
             mMainExecutor.execute(() -> {
                 PipController.this.onSystemUiStateChanged(isSysUiStateValid, flag);
             });
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index afae653..9c00864 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -668,6 +668,18 @@
         Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton());
     }
 
+    @Test
+    public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() {
+        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
+                CAMERA_COMPAT_CONTROL_HIDDEN);
+        taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = false;
+
+        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+
+        verify(mController, never()).createLetterboxEduWindowManager(any(), eq(taskInfo),
+                eq(mMockTaskListener));
+    }
+
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
             @CameraCompatControlState int cameraCompatControlState) {
         return createTaskInfo(displayId, taskId, hasSizeCompat, cameraCompatControlState,
@@ -694,6 +706,8 @@
         taskInfo.isVisible = isVisible;
         taskInfo.isFocused = isFocused;
         taskInfo.isTopActivityTransparent = isTopActivityTransparent;
+        taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = true;
+        taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed = true;
         return taskInfo;
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index dca7be1..8f59f30 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -182,18 +182,6 @@
     }
 
     @Test
-    fun addListener_notifiesStashed() {
-        repo.setStashed(DEFAULT_DISPLAY, true)
-        val listener = TestVisibilityListener()
-        val executor = TestShellExecutor()
-        repo.addVisibleTasksListener(listener, executor)
-        executor.flushAll()
-
-        assertThat(listener.stashedOnDefaultDisplay).isTrue()
-        assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
-    }
-
-    @Test
     fun addListener_tasksOnDifferentDisplay_doesNotNotify() {
         repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true)
         val listener = TestVisibilityListener()
@@ -400,65 +388,6 @@
     }
 
     @Test
-    fun setStashed_stateIsUpdatedForTheDisplay() {
-        repo.setStashed(DEFAULT_DISPLAY, true)
-        assertThat(repo.isStashed(DEFAULT_DISPLAY)).isTrue()
-        assertThat(repo.isStashed(SECOND_DISPLAY)).isFalse()
-
-        repo.setStashed(DEFAULT_DISPLAY, false)
-        assertThat(repo.isStashed(DEFAULT_DISPLAY)).isFalse()
-    }
-
-    @Test
-    fun setStashed_notifyListener() {
-        val listener = TestVisibilityListener()
-        val executor = TestShellExecutor()
-        repo.addVisibleTasksListener(listener, executor)
-        repo.setStashed(DEFAULT_DISPLAY, true)
-        executor.flushAll()
-        assertThat(listener.stashedOnDefaultDisplay).isTrue()
-        assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
-
-        repo.setStashed(DEFAULT_DISPLAY, false)
-        executor.flushAll()
-        assertThat(listener.stashedOnDefaultDisplay).isFalse()
-        assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(2)
-    }
-
-    @Test
-    fun setStashed_secondCallDoesNotNotify() {
-        val listener = TestVisibilityListener()
-        val executor = TestShellExecutor()
-        repo.addVisibleTasksListener(listener, executor)
-        repo.setStashed(DEFAULT_DISPLAY, true)
-        repo.setStashed(DEFAULT_DISPLAY, true)
-        executor.flushAll()
-        assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
-    }
-
-    @Test
-    fun setStashed_tracksPerDisplay() {
-        val listener = TestVisibilityListener()
-        val executor = TestShellExecutor()
-        repo.addVisibleTasksListener(listener, executor)
-
-        repo.setStashed(DEFAULT_DISPLAY, true)
-        executor.flushAll()
-        assertThat(listener.stashedOnDefaultDisplay).isTrue()
-        assertThat(listener.stashedOnSecondaryDisplay).isFalse()
-
-        repo.setStashed(SECOND_DISPLAY, true)
-        executor.flushAll()
-        assertThat(listener.stashedOnDefaultDisplay).isTrue()
-        assertThat(listener.stashedOnSecondaryDisplay).isTrue()
-
-        repo.setStashed(DEFAULT_DISPLAY, false)
-        executor.flushAll()
-        assertThat(listener.stashedOnDefaultDisplay).isFalse()
-        assertThat(listener.stashedOnSecondaryDisplay).isTrue()
-    }
-
-    @Test
     fun removeFreeformTask_removesTaskBoundsBeforeMaximize() {
         val taskId = 1
         repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200))
@@ -598,12 +527,6 @@
         var visibleChangesOnDefaultDisplay = 0
         var visibleChangesOnSecondaryDisplay = 0
 
-        var stashedOnDefaultDisplay = false
-        var stashedOnSecondaryDisplay = false
-
-        var stashedChangesOnDefaultDisplay = 0
-        var stashedChangesOnSecondaryDisplay = 0
-
         override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
             when (displayId) {
                 DEFAULT_DISPLAY -> {
@@ -617,20 +540,6 @@
                 else -> fail("Visible task listener received unexpected display id: $displayId")
             }
         }
-
-        override fun onStashedChanged(displayId: Int, stashed: Boolean) {
-            when (displayId) {
-                DEFAULT_DISPLAY -> {
-                    stashedOnDefaultDisplay = stashed
-                    stashedChangesOnDefaultDisplay++
-                }
-                SECOND_DISPLAY -> {
-                    stashedOnSecondaryDisplay = stashed
-                    stashedChangesOnDefaultDisplay++
-                }
-                else -> fail("Visible task listener received unexpected display id: $displayId")
-            }
-        }
     }
 
     companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 3f76c4f..7e55628 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -1044,29 +1044,6 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-    fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
-        assumeTrue(ENABLE_SHELL_TRANSITIONS)
-        whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
-
-        val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
-        markTaskHidden(stashedFreeformTask)
-
-        val fullscreenTask = createFullscreenTask(DEFAULT_DISPLAY)
-
-        controller.stashDesktopApps(DEFAULT_DISPLAY)
-
-        val result = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-        assertThat(result).isNotNull()
-        result!!.assertReorderSequence(stashedFreeformTask, fullscreenTask)
-        assertThat(result.changes[fullscreenTask.token.asBinder()]?.windowingMode)
-                .isEqualTo(WINDOWING_MODE_FREEFORM)
-
-        // Stashed state should be cleared
-        assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
-    }
-
-    @Test
     fun handleRequest_freeformTask_freeformVisible_returnNull() {
         assumeTrue(ENABLE_SHELL_TRANSITIONS)
 
@@ -1133,27 +1110,6 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-    fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
-        assumeTrue(ENABLE_SHELL_TRANSITIONS)
-        whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
-
-        val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
-        markTaskHidden(stashedFreeformTask)
-
-        val freeformTask = createFreeformTask(DEFAULT_DISPLAY)
-
-        controller.stashDesktopApps(DEFAULT_DISPLAY)
-
-        val result = controller.handleRequest(Binder(), createTransition(freeformTask))
-        assertThat(result).isNotNull()
-        result?.assertReorderSequence(stashedFreeformTask, freeformTask)
-
-        // Stashed state should be cleared
-        assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
-    }
-
-    @Test
     fun handleRequest_notOpenOrToFrontTransition_returnNull() {
         assumeTrue(ENABLE_SHELL_TRANSITIONS)
 
@@ -1269,29 +1225,6 @@
     }
 
     @Test
-    fun stashDesktopApps_stateUpdates() {
-        whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
-
-        controller.stashDesktopApps(DEFAULT_DISPLAY)
-
-        assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue()
-        assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse()
-    }
-
-    @Test
-    fun hideStashedDesktopApps_stateUpdates() {
-        whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
-
-        desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true)
-        desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true)
-        controller.hideStashedDesktopApps(DEFAULT_DISPLAY)
-
-        assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
-        // Check that second display is not affected
-        assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue()
-    }
-
-    @Test
     fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() {
         val task = setUpFreeformTask()
         clearInvocations(launchAdjacentController)
diff --git a/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp b/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp
index b511244..6196589 100644
--- a/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp
+++ b/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp
@@ -19,6 +19,7 @@
     // to get the below license kinds:
     //   SPDX-license-identifier-Apache-2.0
     default_applicable_licenses: ["frameworks_base_libs_androidfw_license"],
+    default_team: "trendy_team_android_resources",
 }
 
 cc_fuzz {
@@ -31,7 +32,7 @@
     static_libs: ["libgmock"],
     target: {
         android: {
-            shared_libs:[
+            shared_libs: [
                 "libandroidfw",
                 "libbase",
                 "libcutils",
@@ -52,4 +53,15 @@
             ],
         },
     },
+    fuzz_config: {
+        cc: [
+            "android-resources@google.com",
+        ],
+        componentid: 568761,
+        description: "The fuzzer targets the APIs of libandroidfw",
+        vector: "local_no_privileges_required",
+        service_privilege: "privileged",
+        users: "multi_user",
+        fuzzed_code_usage: "shipped",
+    },
 }
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index 6d4cc3a..cf7aea4 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -64,8 +64,10 @@
   }
 
   public final class NfcAdapter {
+    method @FlaggedApi("android.nfc.nfc_state_change") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable();
     method public void disableForegroundDispatch(android.app.Activity);
     method public void disableReaderMode(android.app.Activity);
+    method @FlaggedApi("android.nfc.nfc_state_change") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable();
     method public void enableForegroundDispatch(android.app.Activity, android.app.PendingIntent, android.content.IntentFilter[], String[][]);
     method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle);
     method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context);
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 310130e..a33e225 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -3,9 +3,7 @@
 
   public final class NfcAdapter {
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean addNfcUnlockHandler(android.nfc.NfcAdapter.NfcUnlockHandler, String[]);
-    method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable();
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable(boolean);
-    method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable();
     method @FlaggedApi("android.nfc.enable_nfc_reader_option") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableReaderOption(boolean);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableSecureNfc(boolean);
     method @FlaggedApi("android.nfc.enable_nfc_mainline") public int getAdapterState();
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index e43d104..06098de 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -1106,6 +1106,9 @@
      * {@link #ACTION_ADAPTER_STATE_CHANGED} broadcasts to find out when the
      * operation is complete.
      *
+     * <p>This API is only allowed to be called by system apps
+     * or apps which are Device Owner or Profile Owner.
+     *
      * <p>If this returns true, then either NFC is already on, or
      * a {@link #ACTION_ADAPTER_STATE_CHANGED} broadcast will be sent
      * to indicate a state transition. If this returns false, then
@@ -1113,9 +1116,8 @@
      * NFC on (for example we are in airplane mode and NFC is not
      * toggleable in airplane mode on this platform).
      *
-     * @hide
      */
-    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_STATE_CHANGE)
     @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
     public boolean enable() {
         try {
@@ -1146,15 +1148,17 @@
      * {@link #ACTION_ADAPTER_STATE_CHANGED} broadcasts to find out when the
      * operation is complete.
      *
+     * <p>This API is only allowed to be called by system apps
+     * or apps which are Device Owner or Profile Owner.
+     *
      * <p>If this returns true, then either NFC is already off, or
      * a {@link #ACTION_ADAPTER_STATE_CHANGED} broadcast will be sent
      * to indicate a state transition. If this returns false, then
      * there is some problem that prevents an attempt to turn
      * NFC off.
      *
-     * @hide
      */
-    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_STATE_CHANGE)
     @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
     public boolean disable() {
         try {
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index 73b29db..cb2a48c 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -93,3 +93,11 @@
     description: "Enable NFC OEM extension support"
     bug: "331206243"
 }
+
+flag {
+    name: "nfc_state_change"
+    is_exported: true
+    namespace: "nfc"
+    description: "Enable nfc state change API"
+    bug: "319934052"
+}
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
index 25ac3c9..635dc42 100644
--- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
+++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
@@ -172,7 +172,7 @@
 
     // This is for testing only now
     private boolean mEnableWhenCompleted;
-    private boolean mOneShot;
+    private boolean mOneShot = true;
     private boolean mHideNotification;
 
     private InstallationAsyncTask.Progress mInstallTaskProgress;
diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts
index 4147813..e7823df 100644
--- a/packages/SettingsLib/Spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/build.gradle.kts
@@ -29,7 +29,7 @@
 
 allprojects {
     extra["androidTop"] = androidTop
-    extra["jetpackComposeVersion"] = "1.7.0-alpha05"
+    extra["jetpackComposeVersion"] = "1.7.0-alpha08"
 }
 
 subprojects {
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
index 6344501..4aa57b3 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -53,17 +53,17 @@
 
 dependencies {
     api(project(":SettingsLibColor"))
-    api("androidx.appcompat:appcompat:1.7.0-alpha03")
+    api("androidx.appcompat:appcompat:1.7.0-beta01")
     api("androidx.slice:slice-builders:1.1.0-alpha02")
     api("androidx.slice:slice-core:1.1.0-alpha02")
     api("androidx.slice:slice-view:1.1.0-alpha02")
-    api("androidx.compose.material3:material3:1.3.0-alpha03")
+    api("androidx.compose.material3:material3:1.3.0-alpha06")
     api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion")
     api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
     api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
     api("androidx.lifecycle:lifecycle-livedata-ktx")
     api("androidx.lifecycle:lifecycle-runtime-compose")
-    api("androidx.navigation:navigation-compose:2.8.0-alpha05")
+    api("androidx.navigation:navigation-compose:2.8.0-alpha08")
     api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
     api("com.google.android.material:material:1.11.0")
     debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/AnnotatedStringResource.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/AnnotatedStringResource.kt
index 88ba4b0..1a10bf0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/AnnotatedStringResource.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/AnnotatedStringResource.kt
@@ -27,14 +27,13 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.LinkAnnotation
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextDecoration
 
-const val URL_SPAN_TAG = "URL_SPAN_TAG"
-
 @Composable
 fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
     val resources = LocalContext.current.resources
@@ -97,12 +96,9 @@
     start: Int,
     end: Int,
 ) {
-    addStyle(
-        SpanStyle(color = urlSpanColor, textDecoration = TextDecoration.Underline),
-        start,
-        end,
+    val url = LinkAnnotation.Url(
+        url = urlSpan.url,
+        style = SpanStyle(color = urlSpanColor, textDecoration = TextDecoration.Underline),
     )
-    if (!urlSpan.url.isNullOrEmpty()) {
-        addStringAnnotation(URL_SPAN_TAG, urlSpan.url, start, end)
-    }
+    addLink(url, start, end)
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/AnnotatedText.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/AnnotatedText.kt
index 82ac7e3..f864fa9 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/AnnotatedText.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/AnnotatedText.kt
@@ -17,26 +17,17 @@
 package com.android.settingslib.spa.widget.ui
 
 import androidx.annotation.StringRes
-import androidx.compose.foundation.text.ClickableText
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalUriHandler
-import com.android.settingslib.spa.framework.util.URL_SPAN_TAG
 import com.android.settingslib.spa.framework.util.annotatedStringResource
 
 @Composable
 fun AnnotatedText(@StringRes id: Int) {
-    val uriHandler = LocalUriHandler.current
-    val annotatedString = annotatedStringResource(id)
-    ClickableText(
-        text = annotatedString,
+    Text(
+        text = annotatedStringResource(id),
         style = MaterialTheme.typography.bodyMedium.copy(
             color = MaterialTheme.colorScheme.onSurfaceVariant,
         ),
-    ) { offset ->
-        // Gets the url at the clicked position.
-        annotatedString.getStringAnnotations(URL_SPAN_TAG, offset, offset)
-            .firstOrNull()
-            ?.let { uriHandler.openUri(it.item) }
-    }
+    )
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedStringResourceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedStringResourceTest.kt
index 9928355..612f9e5 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedStringResourceTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/AnnotatedStringResourceTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.LinkAnnotation
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
@@ -36,18 +37,23 @@
     val composeTestRule = createComposeRule()
 
     @Test
-    fun testAnnotatedStringResource() {
+    fun annotatedStringResource() {
         composeTestRule.setContent {
             val annotatedString =
                 annotatedStringResource(R.string.test_annotated_string_resource)
 
-            val annotations = annotatedString.getStringAnnotations(0, annotatedString.length)
+            val annotations = annotatedString.getLinkAnnotations(0, annotatedString.length)
             assertThat(annotations).containsExactly(
                 AnnotatedString.Range(
-                    item = "https://www.android.com/",
+                    item = LinkAnnotation.Url(
+                        url = "https://www.android.com/",
+                        style = SpanStyle(
+                            color = MaterialTheme.colorScheme.primary,
+                            textDecoration = TextDecoration.Underline,
+                        ),
+                    ),
                     start = 31,
                     end = 35,
-                    tag = URL_SPAN_TAG,
                 )
             )
 
@@ -57,14 +63,6 @@
                     start = 22,
                     end = 26,
                 ),
-                AnnotatedString.Range(
-                    item = SpanStyle(
-                        color = MaterialTheme.colorScheme.primary,
-                        textDecoration = TextDecoration.Underline,
-                    ),
-                    start = 31,
-                    end = 35,
-                ),
             )
         }
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
index 822a608..1040ac6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
@@ -36,6 +36,7 @@
 import android.util.ArraySet;
 import android.util.Log;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.telephony.SmsApplication;
@@ -56,13 +57,22 @@
 
     private static PowerAllowlistBackend sInstance;
 
+    private final Object mAllowlistedAppsLock = new Object();
+    private final Object mSysAllowlistedAppsLock = new Object();
+    private final Object mDefaultActiveAppsLock = new Object();
+
     private final Context mAppContext;
     private final IDeviceIdleController mDeviceIdleService;
+
+    @GuardedBy("mAllowlistedAppsLock")
     private final ArraySet<String> mAllowlistedApps = new ArraySet<>();
+    @GuardedBy("mSysAllowlistedAppsLock")
     private final ArraySet<String> mSysAllowlistedApps = new ArraySet<>();
+    @GuardedBy("mDefaultActiveAppsLock")
     private final ArraySet<String> mDefaultActiveApps = new ArraySet<>();
 
-    public PowerAllowlistBackend(Context context) {
+    @VisibleForTesting
+    PowerAllowlistBackend(Context context) {
         this(context, IDeviceIdleController.Stub.asInterface(
                 ServiceManager.getService(DEVICE_IDLE_SERVICE)));
     }
@@ -75,24 +85,25 @@
     }
 
     public int getAllowlistSize() {
-        return mAllowlistedApps.size();
-    }
-
-    /**
-    * Check if target package is in System allow list
-    */
-    public boolean isSysAllowlisted(String pkg) {
-        return mSysAllowlistedApps.contains(pkg);
-    }
-
-    /**
-     * Check if target package is in allow list
-     */
-    public boolean isAllowlisted(String pkg, int uid) {
-        if (mAllowlistedApps.contains(pkg)) {
-            return true;
+        synchronized (mAllowlistedAppsLock) {
+            return mAllowlistedApps.size();
         }
+    }
 
+    /** Check if target package is in System allow list */
+    public boolean isSysAllowlisted(String pkg) {
+        synchronized (mSysAllowlistedAppsLock) {
+            return mSysAllowlistedApps.contains(pkg);
+        }
+    }
+
+    /** Check if target package is in allow list */
+    public boolean isAllowlisted(String pkg, int uid) {
+        synchronized (mAllowlistedAppsLock) {
+            if (mAllowlistedApps.contains(pkg)) {
+                return true;
+            }
+        }
         if (isDefaultActiveApp(pkg, uid)) {
             return true;
         }
@@ -100,16 +111,16 @@
         return false;
     }
 
-    /**
-     * Check if it is default active app in multiple area(i.e. SMS, Dialer, Device admin..)
-     */
+    /** Check if it is default active app in multiple area */
     public boolean isDefaultActiveApp(String pkg, int uid) {
         // Additionally, check if pkg is default dialer/sms. They are considered essential apps and
         // should be automatically allowlisted (otherwise user may be able to set restriction on
         // them, leading to bad device behavior.)
 
-        if (mDefaultActiveApps.contains(pkg)) {
-            return true;
+        synchronized (mDefaultActiveAppsLock) {
+            if (mDefaultActiveApps.contains(pkg)) {
+                return true;
+            }
         }
 
         final DevicePolicyManager devicePolicyManager = mAppContext.getSystemService(
@@ -143,9 +154,7 @@
                 DEFAULT_SYSTEM_EXEMPT_POWER_RESTRICTIONS_ENABLED);
     }
 
-    /**
-     * Check if target package is in allow list except idle app
-     */
+    /** Check if target package is in allow list except idle app */
     public boolean isAllowlistedExceptIdle(String pkg) {
         try {
             return mDeviceIdleService.isPowerSaveWhitelistExceptIdleApp(pkg);
@@ -156,6 +165,7 @@
     }
 
     /**
+     * Check if target package is in allow list except idle app
      *
      * @param pkgs a list of packageName
      * @return true when one of package is in allow list
@@ -174,20 +184,21 @@
     }
 
     /**
-     * Add app into power save allow list.
+     * Add app into power save allow list
+     *
      * @param pkg packageName of the app
      */
-    // TODO: Fix all callers to pass in UID
     public void addApp(String pkg) {
         addApp(pkg, Process.INVALID_UID);
     }
 
     /**
-     * Add app into power save allow list.
+     * Add app into power save allow list
+     *
      * @param pkg packageName of the app
      * @param uid uid of the app
      */
-    public void addApp(String pkg, int uid) {
+    public synchronized void addApp(String pkg, int uid) {
         try {
             if (android.app.Flags.appRestrictionsApi()) {
                 if (uid == Process.INVALID_UID) {
@@ -204,7 +215,9 @@
             }
 
             mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
-            mAllowlistedApps.add(pkg);
+            synchronized (mAllowlistedAppsLock) {
+                mAllowlistedApps.add(pkg);
+            }
         } catch (RemoteException e) {
             Log.w(TAG, "Unable to reach IDeviceIdleController", e);
         } catch (NameNotFoundException e) {
@@ -213,7 +226,8 @@
     }
 
     /**
-     * Remove package from power save allow list.
+     * Remove package from power save allow list
+     *
      * @param pkg packageName of the app
      */
     public void removeApp(String pkg) {
@@ -222,10 +236,11 @@
 
     /**
      * Remove package from power save allow list.
+     *
      * @param pkg packageName of the app
      * @param uid uid of the app
      */
-    public void removeApp(String pkg, int uid) {
+    public synchronized void removeApp(String pkg, int uid) {
         try {
             if (android.app.Flags.appRestrictionsApi()) {
                 if (uid == Process.INVALID_UID) {
@@ -241,7 +256,9 @@
             }
 
             mDeviceIdleService.removePowerSaveWhitelistApp(pkg);
-            mAllowlistedApps.remove(pkg);
+            synchronized (mAllowlistedAppsLock) {
+                mAllowlistedApps.remove(pkg);
+            }
         } catch (RemoteException e) {
             Log.w(TAG, "Unable to reach IDeviceIdleController", e);
         } catch (NameNotFoundException e) {
@@ -249,25 +266,33 @@
         }
     }
 
-    /**
-     * Refresh all of lists
-     */
+    /** Refresh all of lists */
     @VisibleForTesting
-    public void refreshList() {
-        mSysAllowlistedApps.clear();
-        mAllowlistedApps.clear();
-        mDefaultActiveApps.clear();
+    public synchronized void refreshList() {
+        synchronized (mSysAllowlistedAppsLock) {
+            mSysAllowlistedApps.clear();
+        }
+        synchronized (mAllowlistedAppsLock) {
+            mAllowlistedApps.clear();
+        }
+        synchronized (mDefaultActiveAppsLock) {
+            mDefaultActiveApps.clear();
+        }
         if (mDeviceIdleService == null) {
             return;
         }
         try {
             final String[] allowlistedApps = mDeviceIdleService.getFullPowerWhitelist();
-            for (String app : allowlistedApps) {
-                mAllowlistedApps.add(app);
+            synchronized (mAllowlistedAppsLock) {
+                for (String app : allowlistedApps) {
+                    mAllowlistedApps.add(app);
+                }
             }
             final String[] sysAllowlistedApps = mDeviceIdleService.getSystemPowerWhitelist();
-            for (String app : sysAllowlistedApps) {
-                mSysAllowlistedApps.add(app);
+            synchronized (mSysAllowlistedAppsLock) {
+                for (String app : sysAllowlistedApps) {
+                    mSysAllowlistedApps.add(app);
+                }
             }
             final boolean hasTelephony = mAppContext.getPackageManager().hasSystemFeature(
                     PackageManager.FEATURE_TELEPHONY);
@@ -278,26 +303,28 @@
 
             if (hasTelephony) {
                 if (defaultSms != null) {
-                    mDefaultActiveApps.add(defaultSms.getPackageName());
+                    synchronized (mDefaultActiveAppsLock) {
+                        mDefaultActiveApps.add(defaultSms.getPackageName());
+                    }
                 }
                 if (!TextUtils.isEmpty(defaultDialer)) {
-                    mDefaultActiveApps.add(defaultDialer);
+                    synchronized (mDefaultActiveAppsLock) {
+                        mDefaultActiveApps.add(defaultDialer);
+                    }
                 }
             }
-        } catch (RemoteException e) {
-            Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to invoke refreshList()", e);
         }
     }
 
-    /**
-     * @param context
-     * @return a PowerAllowlistBackend object
-     */
+    /** Get the {@link PowerAllowlistBackend} instance */
     public static PowerAllowlistBackend getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new PowerAllowlistBackend(context);
+        synchronized (PowerAllowlistBackend.class) {
+            if (sInstance == null) {
+                sInstance = new PowerAllowlistBackend(context);
+            }
+            return sInstance;
         }
-        return sInstance;
     }
-
 }
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 626e219..0dd956c 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -891,4 +891,14 @@
   metadata {
     purpose: PURPOSE_BUGFIX
   }
-}
\ No newline at end of file
+}
+
+flag {
+  name: "media_controls_user_initiated_dismiss"
+  namespace: "systemui"
+  description: "Only dismiss media notifications when the control was removed by the user."
+  bug: "335875159"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index d4660fa..23df26f 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -23,6 +23,7 @@
 import android.graphics.Matrix
 import android.graphics.Rect
 import android.graphics.RectF
+import android.os.Binder
 import android.os.Build
 import android.os.Handler
 import android.os.Looper
@@ -36,7 +37,11 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.animation.PathInterpolator
+import android.window.RemoteTransition
+import android.window.TransitionFilter
 import androidx.annotation.AnyThread
 import androidx.annotation.BinderThread
 import androidx.annotation.UiThread
@@ -44,6 +49,9 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.systemui.Flags.activityTransitionUseLargestWindow
+import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
+import com.android.wm.shell.shared.IShellTransitions
+import com.android.wm.shell.shared.ShellTransitions
 import java.util.concurrent.Executor
 import kotlin.math.roundToInt
 
@@ -59,6 +67,9 @@
     /** The executor that runs on the main thread. */
     private val mainExecutor: Executor,
 
+    /** The object used to register ephemeral returns and long-lived transitions. */
+    private val transitionRegister: TransitionRegister? = null,
+
     /** The animator used when animating a View into an app. */
     private val transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),
 
@@ -74,6 +85,36 @@
     // TODO(b/301385865): Remove this flag.
     private val disableWmTimeout: Boolean = false,
 ) {
+    @JvmOverloads
+    constructor(
+        mainExecutor: Executor,
+        shellTransitions: ShellTransitions,
+        transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),
+        dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor),
+        disableWmTimeout: Boolean = false,
+    ) : this(
+        mainExecutor,
+        TransitionRegister.fromShellTransitions(shellTransitions),
+        transitionAnimator,
+        dialogToAppAnimator,
+        disableWmTimeout,
+    )
+
+    @JvmOverloads
+    constructor(
+        mainExecutor: Executor,
+        iShellTransitions: IShellTransitions,
+        transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),
+        dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor),
+        disableWmTimeout: Boolean = false,
+    ) : this(
+        mainExecutor,
+        TransitionRegister.fromIShellTransitions(iShellTransitions),
+        transitionAnimator,
+        dialogToAppAnimator,
+        disableWmTimeout,
+    )
+
     companion object {
         /** The timings when animating a View into an app. */
         @JvmField
@@ -233,6 +274,10 @@
             }
         }
 
+        if (animationAdapter != null && controller.transitionCookie != null) {
+            registerEphemeralReturnAnimation(controller, transitionRegister)
+        }
+
         val launchResult = intentStarter(animationAdapter)
 
         // Only animate if the app is not already on top and will be opened, unless we are on the
@@ -302,6 +347,66 @@
         }
     }
 
+    /**
+     * Uses [transitionRegister] to set up the return animation for the given [launchController].
+     *
+     * De-registration is set up automatically once the return animation is run.
+     *
+     * TODO(b/339194555): automatically de-register when the launchable is detached.
+     */
+    private fun registerEphemeralReturnAnimation(
+        launchController: Controller,
+        transitionRegister: TransitionRegister?
+    ) {
+        if (!returnAnimationFrameworkLibrary()) return
+
+        var cleanUpRunnable: Runnable? = null
+        val returnRunner =
+            createRunner(
+                object : DelegateTransitionAnimatorController(launchController) {
+                    override val isLaunching = false
+
+                    override fun onTransitionAnimationCancelled(
+                        newKeyguardOccludedState: Boolean?
+                    ) {
+                        super.onTransitionAnimationCancelled(newKeyguardOccludedState)
+                        cleanUp()
+                    }
+
+                    override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+                        super.onTransitionAnimationEnd(isExpandingFullyAbove)
+                        cleanUp()
+                    }
+
+                    private fun cleanUp() {
+                        cleanUpRunnable?.run()
+                    }
+                }
+            )
+
+        // mTypeSet and mModes match back signals only, and not home. This is on purpose, because
+        // we only want ephemeral return animations triggered in these scenarios.
+        val filter =
+            TransitionFilter().apply {
+                mTypeSet = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK)
+                mRequirements =
+                    arrayOf(
+                        TransitionFilter.Requirement().apply {
+                            mLaunchCookie = launchController.transitionCookie
+                            mModes = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK)
+                        }
+                    )
+            }
+        val transition =
+            RemoteTransition(
+                RemoteAnimationRunnerCompat.wrap(returnRunner),
+                "${launchController.transitionCookie}_returnTransition"
+            )
+
+        transitionRegister?.register(filter, transition)
+        cleanUpRunnable = Runnable { transitionRegister?.unregister(transition) }
+    }
+
     /** Add a [Listener] that can listen to transition animations. */
     fun addListener(listener: Listener) {
         listeners.add(listener)
@@ -386,8 +491,14 @@
              * Note: The background of [view] should be a (rounded) rectangle so that it can be
              * properly animated.
              */
+            @JvmOverloads
             @JvmStatic
-            fun fromView(view: View, cujType: Int? = null): Controller? {
+            fun fromView(
+                view: View,
+                cujType: Int? = null,
+                cookie: TransitionCookie? = null,
+                returnCujType: Int? = null
+            ): Controller? {
                 // Make sure the View we launch from implements LaunchableView to avoid visibility
                 // issues.
                 if (view !is LaunchableView) {
@@ -408,7 +519,7 @@
                     return null
                 }
 
-                return GhostedViewTransitionAnimatorController(view, cujType)
+                return GhostedViewTransitionAnimatorController(view, cujType, cookie, returnCujType)
             }
         }
 
@@ -432,6 +543,17 @@
             get() = false
 
         /**
+         * The cookie associated with the transition controlled by this [Controller].
+         *
+         * This should be defined for all return [Controller] (when [isLaunching] is false) and for
+         * their associated launch [Controller]s.
+         *
+         * For the recommended format, see [TransitionCookie].
+         */
+        val transitionCookie: TransitionCookie?
+            get() = null
+
+        /**
          * The intent was started. If [willAnimate] is false, nothing else will happen and the
          * animation will not be started.
          */
@@ -652,7 +774,7 @@
                 return
             }
 
-            val window = findRootTaskIfPossible(apps)
+            val window = findTargetWindowIfPossible(apps)
             if (window == null) {
                 Log.i(TAG, "Aborting the animation as no window is opening")
                 callback?.invoke()
@@ -676,7 +798,7 @@
             startAnimation(window, navigationBar, callback)
         }
 
-        private fun findRootTaskIfPossible(
+        private fun findTargetWindowIfPossible(
             apps: Array<out RemoteAnimationTarget>?
         ): RemoteAnimationTarget? {
             if (apps == null) {
@@ -694,6 +816,19 @@
             for (it in apps) {
                 if (it.mode == targetMode) {
                     if (activityTransitionUseLargestWindow()) {
+                        if (returnAnimationFrameworkLibrary()) {
+                            // If the controller contains a cookie, _only_ match if the candidate
+                            // contains the matching cookie.
+                            if (
+                                controller.transitionCookie != null &&
+                                    it.taskInfo
+                                        ?.launchCookies
+                                        ?.contains(controller.transitionCookie) != true
+                            ) {
+                                continue
+                            }
+                        }
+
                         if (
                             candidate == null ||
                                 !it.hasAnimatingParent && candidate.hasAnimatingParent
@@ -806,11 +941,7 @@
                         progress: Float,
                         linearProgress: Float
                     ) {
-                        // Apply the state to the window only if it is visible, i.e. when the
-                        // expanding view is *not* visible.
-                        if (!state.visible) {
-                            applyStateToWindow(window, state, linearProgress)
-                        }
+                        applyStateToWindow(window, state, linearProgress)
                         navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
 
                         listener?.onTransitionAnimationProgress(linearProgress)
@@ -1048,4 +1179,72 @@
             return (this.width() * this.height()) > (other.width() * other.height())
         }
     }
+
+    /**
+     * Wraps one of the two methods we have to register remote transitions with WM Shell:
+     * - for in-process registrations (e.g. System UI) we use [ShellTransitions]
+     * - for cross-process registrations (e.g. Launcher) we use [IShellTransitions]
+     *
+     * Important: each instance of this class must wrap exactly one of the two.
+     */
+    class TransitionRegister
+    private constructor(
+        private val shellTransitions: ShellTransitions? = null,
+        private val iShellTransitions: IShellTransitions? = null,
+    ) {
+        init {
+            assert((shellTransitions != null).xor(iShellTransitions != null))
+        }
+
+        companion object {
+            /** Provides a [TransitionRegister] instance wrapping [ShellTransitions]. */
+            fun fromShellTransitions(shellTransitions: ShellTransitions): TransitionRegister {
+                return TransitionRegister(shellTransitions = shellTransitions)
+            }
+
+            /** Provides a [TransitionRegister] instance wrapping [IShellTransitions]. */
+            fun fromIShellTransitions(iShellTransitions: IShellTransitions): TransitionRegister {
+                return TransitionRegister(iShellTransitions = iShellTransitions)
+            }
+        }
+
+        /** Register [remoteTransition] with WM Shell using the given [filter]. */
+        internal fun register(
+            filter: TransitionFilter,
+            remoteTransition: RemoteTransition,
+        ) {
+            shellTransitions?.registerRemote(filter, remoteTransition)
+            iShellTransitions?.registerRemote(filter, remoteTransition)
+        }
+
+        /** Unregister [remoteTransition] from WM Shell. */
+        internal fun unregister(remoteTransition: RemoteTransition) {
+            shellTransitions?.unregisterRemote(remoteTransition)
+            iShellTransitions?.unregisterRemote(remoteTransition)
+        }
+    }
+
+    /**
+     * A cookie used to uniquely identify a task launched using an
+     * [ActivityTransitionAnimator.Controller].
+     *
+     * The [String] encapsulated by this class should be formatted in such a way to be unique across
+     * the system, but reliably constant for the same associated launchable.
+     *
+     * Recommended naming scheme:
+     * - DO use the fully qualified name of the class that owns the instance of the launchable,
+     *   along with a concise and precise description of the purpose of the launchable in question.
+     * - DO NOT introduce uniqueness through the use of timestamps or other runtime variables that
+     *   will change if the instance is destroyed and re-created.
+     *
+     * Example: "com.not.the.real.class.name.ShadeController_openSettingsButton"
+     *
+     * Note that sometimes (e.g. in recycler views) there could be multiple instances of the same
+     * launchable, and no static knowledge to adequately differentiate between them using a single
+     * description. In this case, the recommendation is to append a unique identifier related to the
+     * contents of the launchable.
+     *
+     * Example: “com.not.the.real.class.name.ToastWebResult_launchAga_id143256”
+     */
+    data class TransitionCookie(private val cookie: String) : Binder()
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
index e4bb2ad..21557b8 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
@@ -25,10 +25,30 @@
      * [Expandable] into an Activity, or return `null` if this [Expandable] should not be animated
      * (e.g. if it is currently not attached or visible).
      *
-     * @param cujType the CUJ type from the [com.android.internal.jank.InteractionJankMonitor]
+     * @param launchCujType The CUJ type from the [com.android.internal.jank.InteractionJankMonitor]
      *   associated to the launch that will use this controller.
+     * @param cookie The unique cookie associated with the launch that will use this controller.
+     *   This is required iff the a return animation should be included.
+     * @param returnCujType The CUJ type from the [com.android.internal.jank.InteractionJankMonitor]
+     *   associated to the return animation that will use this controller.
      */
-    fun activityTransitionController(cujType: Int? = null): ActivityTransitionAnimator.Controller?
+    fun activityTransitionController(
+        launchCujType: Int? = null,
+        cookie: ActivityTransitionAnimator.TransitionCookie? = null,
+        returnCujType: Int? = null
+    ): ActivityTransitionAnimator.Controller?
+
+    /**
+     * See [activityTransitionController] above.
+     *
+     * Interfaces don't support [JvmOverloads], so this is a useful overload for Java usages that
+     * don't use the return-related parameters.
+     */
+    fun activityTransitionController(
+        launchCujType: Int? = null
+    ): ActivityTransitionAnimator.Controller? {
+        return activityTransitionController(launchCujType, cookie = null, returnCujType = null)
+    }
 
     /**
      * Create a [DialogTransitionAnimator.Controller] that can be used to expand this [Expandable]
@@ -48,9 +68,16 @@
         fun fromView(view: View): Expandable {
             return object : Expandable {
                 override fun activityTransitionController(
-                    cujType: Int?,
+                    launchCujType: Int?,
+                    cookie: ActivityTransitionAnimator.TransitionCookie?,
+                    returnCujType: Int?
                 ): ActivityTransitionAnimator.Controller? {
-                    return ActivityTransitionAnimator.Controller.fromView(view, cujType)
+                    return ActivityTransitionAnimator.Controller.fromView(
+                        view,
+                        launchCujType,
+                        cookie,
+                        returnCujType
+                    )
                 }
 
                 override fun dialogTransitionController(
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
index fd79f62..9d45073 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
@@ -59,8 +59,12 @@
     /** The view that will be ghosted and from which the background will be extracted. */
     private val ghostedView: View,
 
-    /** The [CujType] associated to this animation. */
-    private val cujType: Int? = null,
+    /** The [CujType] associated to this launch animation. */
+    private val launchCujType: Int? = null,
+    override val transitionCookie: ActivityTransitionAnimator.TransitionCookie? = null,
+
+    /** The [CujType] associated to this return animation. */
+    private val returnCujType: Int? = null,
     private var interactionJankMonitor: InteractionJankMonitor =
         InteractionJankMonitor.getInstance(),
 ) : ActivityTransitionAnimator.Controller {
@@ -104,6 +108,15 @@
      */
     private val background: Drawable?
 
+    /** CUJ identifier accounting for whether this controller is for a launch or a return. */
+    private val cujType: Int?
+        get() =
+            if (isLaunching) {
+                launchCujType
+            } else {
+                returnCujType
+            }
+
     init {
         // Make sure the View we launch from implements LaunchableView to avoid visibility issues.
         if (ghostedView !is LaunchableView) {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
index c7f0a96..17a6061 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
@@ -134,13 +134,15 @@
     override val expandable: Expandable =
         object : Expandable {
             override fun activityTransitionController(
-                cujType: Int?,
+                launchCujType: Int?,
+                cookie: ActivityTransitionAnimator.TransitionCookie?,
+                returnCujType: Int?
             ): ActivityTransitionAnimator.Controller? {
                 if (!isComposed.value) {
                     return null
                 }
 
-                return activityController(cujType)
+                return activityController(launchCujType, cookie, returnCujType)
             }
 
             override fun dialogTransitionController(
@@ -262,10 +264,27 @@
     }
 
     /** Create an [ActivityTransitionAnimator.Controller] that can be used to animate activities. */
-    private fun activityController(cujType: Int?): ActivityTransitionAnimator.Controller {
+    private fun activityController(
+        launchCujType: Int?,
+        cookie: ActivityTransitionAnimator.TransitionCookie?,
+        returnCujType: Int?
+    ): ActivityTransitionAnimator.Controller {
         val delegate = transitionController()
         return object :
             ActivityTransitionAnimator.Controller, TransitionAnimator.Controller by delegate {
+            /**
+             * CUJ identifier accounting for whether this controller is for a launch or a return.
+             */
+            private val cujType: Int?
+                get() =
+                    if (isLaunching) {
+                        launchCujType
+                    } else {
+                        returnCujType
+                    }
+
+            override val transitionCookie = cookie
+
             override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
                 delegate.onTransitionAnimationStart(isExpandingFullyAbove)
                 overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
index 79d17ef..44b221c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.panel.component.anc.ui.composable
 
+import android.view.Gravity
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
@@ -27,9 +28,15 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.clearAndSetSemantics
@@ -39,6 +46,7 @@
 import androidx.compose.ui.unit.dp
 import com.android.systemui.res.R
 import com.android.systemui.volume.panel.component.anc.ui.viewmodel.AncViewModel
+import com.android.systemui.volume.panel.component.popup.ui.composable.VolumePanelPopup
 import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
 import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
 import javax.inject.Inject
@@ -54,15 +62,22 @@
     override fun VolumePanelComposeScope.Content(modifier: Modifier) {
         val slice by viewModel.buttonSlice.collectAsState()
         val label = stringResource(R.string.volume_panel_noise_control_title)
+        val screenWidth: Float =
+            with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.toPx() }
+        var gravity by remember { mutableIntStateOf(Gravity.CENTER_HORIZONTAL) }
         val isClickable = viewModel.isClickable(slice)
         val onClick =
             if (isClickable) {
-                { ancPopup.show(null) }
+                { with(ancPopup) { show(null, gravity) } }
             } else {
                 null
             }
+
         Column(
-            modifier = modifier,
+            modifier =
+                modifier.onGloballyPositioned {
+                    gravity = VolumePanelPopup.calculateGravity(it, screenWidth)
+                },
             verticalArrangement = Arrangement.spacedBy(12.dp),
             horizontalAlignment = Alignment.CenterHorizontally,
         ) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt
index e1ee01e..d53dbf9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.panel.component.anc.ui.composable
 
+import android.view.Gravity
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.material3.MaterialTheme
@@ -47,9 +48,10 @@
 ) {
 
     /** Shows a popup with the [expandable] animation. */
-    fun show(expandable: Expandable?) {
+    fun show(expandable: Expandable?, horizontalGravity: Int) {
         uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_ANC_POPUP_SHOWN)
-        volumePanelPopup.show(expandable, { Title() }, { Content(it) })
+        val gravity = horizontalGravity or Gravity.BOTTOM
+        volumePanelPopup.show(expandable, gravity, { Title() }, { Content(it) })
     }
 
     @Composable
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt
index 0893b9d..f11c3a5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.panel.component.button.ui.composable
 
+import android.view.Gravity
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -29,8 +30,14 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.contentDescription
@@ -41,6 +48,7 @@
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.volume.panel.component.button.ui.viewmodel.ButtonViewModel
+import com.android.systemui.volume.panel.component.popup.ui.composable.VolumePanelPopup.Companion.calculateGravity
 import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
 import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
 import kotlinx.coroutines.flow.StateFlow
@@ -48,7 +56,7 @@
 /** [ComposeVolumePanelUiComponent] implementing a clickable button from a bottom row. */
 class ButtonComponent(
     private val viewModelFlow: StateFlow<ButtonViewModel?>,
-    private val onClick: (Expandable) -> Unit
+    private val onClick: (expandable: Expandable, horizontalGravity: Int) -> Unit
 ) : ComposeVolumePanelUiComponent {
 
     @Composable
@@ -57,8 +65,13 @@
         val viewModel = viewModelByState ?: return
         val label = viewModel.label.toString()
 
+        val screenWidth: Float =
+            with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.toPx() }
+        var gravity by remember { mutableIntStateOf(Gravity.CENTER_HORIZONTAL) }
+
         Column(
-            modifier = modifier,
+            modifier =
+                modifier.onGloballyPositioned { gravity = calculateGravity(it, screenWidth) },
             verticalArrangement = Arrangement.spacedBy(12.dp),
             horizontalAlignment = Alignment.CenterHorizontally,
         ) {
@@ -82,7 +95,7 @@
                         } else {
                             MaterialTheme.colorScheme.onSurface
                         },
-                    onClick = onClick,
+                    onClick = { onClick(it, gravity) },
                 ) {
                     Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                         Icon(modifier = Modifier.size(24.dp), icon = viewModel.icon)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt
index bb4e957..3b1bf2a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.volume.panel.component.popup.ui.composable
 
 import android.view.Gravity
+import androidx.annotation.GravityInt
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -31,6 +32,8 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInRoot
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.paneTitle
@@ -60,13 +63,14 @@
      */
     fun show(
         expandable: Expandable?,
+        @GravityInt gravity: Int,
         title: @Composable (SystemUIDialog) -> Unit,
         content: @Composable (SystemUIDialog) -> Unit,
     ) {
         val dialog =
             dialogFactory.create(
                 theme = R.style.Theme_VolumePanel_Popup,
-                dialogGravity = Gravity.BOTTOM,
+                dialogGravity = gravity,
             ) {
                 PopupComposable(it, title, content)
             }
@@ -122,4 +126,23 @@
             }
         }
     }
+
+    companion object {
+
+        /**
+         * Returns absolute ([Gravity.LEFT], [Gravity.RIGHT] or [Gravity.CENTER_HORIZONTAL])
+         * [GravityInt] for the popup based on the [coordinates] global position relative to the
+         * [screenWidthPx].
+         */
+        @GravityInt
+        fun calculateGravity(coordinates: LayoutCoordinates, screenWidthPx: Float): Int {
+            val bottomCenter: Float = coordinates.boundsInRoot().bottomCenter.x
+            val rootBottomCenter: Float = screenWidthPx / 2
+            return when {
+                bottomCenter < rootBottomCenter -> Gravity.LEFT
+                bottomCenter > rootBottomCenter -> Gravity.RIGHT
+                else -> Gravity.CENTER_HORIZONTAL
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt
index 12d2bc2..d41acd9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.panel.component.spatialaudio.ui.composable
 
+import android.view.Gravity
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.MaterialTheme
@@ -47,14 +48,15 @@
 ) {
 
     /** Shows a popup with the [expandable] animation. */
-    fun show(expandable: Expandable) {
+    fun show(expandable: Expandable, horizontalGravity: Int) {
         uiEventLogger.logWithPosition(
             VolumePanelUiEvent.VOLUME_PANEL_SPATIAL_AUDIO_POP_UP_SHOWN,
             0,
             null,
             viewModel.spatialAudioButtons.value.indexOfFirst { it.button.isActive }
         )
-        volumePanelPopup.show(expandable, { Title() }, { Content(it) })
+        val gravity = horizontalGravity or Gravity.BOTTOM
+        volumePanelPopup.show(expandable, gravity, { Title() }, { Content(it) })
     }
 
     @Composable
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
index 1cdc2b6..407bf4c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
@@ -114,7 +114,7 @@
 
             // Change to media unavailable and notify the listener.
             whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false)
-            mediaDataListenerCaptor.value.onMediaDataRemoved("key")
+            mediaDataListenerCaptor.value.onMediaDataRemoved("key", false)
             runCurrent()
 
             // Media active now returns false.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
index d9224d7..bd3b77a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
@@ -27,12 +27,16 @@
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
 import com.android.systemui.bluetooth.mockBroadcastDialogController
+import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.data.repository.mediaDataRepository
 import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
+import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor
 import com.android.systemui.media.controls.domain.pipeline.interactor.mediaControlInteractor
 import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
+import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.util.mediaInstanceId
 import com.android.systemui.media.mediaOutputDialogManager
@@ -211,6 +215,21 @@
             )
     }
 
+    @Test
+    fun removeMediaControl() {
+        val listener = mock<MediaDataProcessor.Listener>()
+        kosmos.mediaDataProcessor.addInternalListener(listener)
+
+        var mediaData = MediaData(userId = USER_ID, instanceId = instanceId, artist = ARTIST)
+        kosmos.mediaDataRepository.addMediaEntry(KEY, mediaData)
+
+        underTest.removeMediaControl(null, instanceId, 0L)
+        kosmos.fakeExecutor.advanceClockToNext()
+        kosmos.fakeExecutor.runAllReady()
+
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(true))
+    }
+
     companion object {
         private const val USER_ID = 0
         private const val KEY = "key"
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 93465c8..677477d 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
@@ -437,12 +437,12 @@
             runCurrent()
             assertThat(
                     sysUiState.flags and
-                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED != 0
+                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED != 0L
                 )
                 .isTrue()
             assertThat(
                     sysUiState.flags and
-                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING != 0
+                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING != 0L
                 )
                 .isFalse()
 
@@ -450,12 +450,12 @@
             runCurrent()
             assertThat(
                     sysUiState.flags and
-                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED != 0
+                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED != 0L
                 )
                 .isFalse()
             assertThat(
                     sysUiState.flags and
-                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING != 0
+                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING != 0L
                 )
                 .isTrue()
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt
new file mode 100644
index 0000000..8cb811d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.service.notification.NotificationListenerService
+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.dump.dumpManager
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.mockNotifCollection
+import com.android.systemui.statusbar.notification.collection.notifPipeline
+import com.android.systemui.statusbar.notification.collection.render.notificationVisibilityProvider
+import com.android.systemui.testKosmos
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationMediaManagerTest : SysuiTestCase() {
+
+    private val KEY = "KEY"
+
+    private val kosmos = testKosmos()
+    private val visibilityProvider = kosmos.notificationVisibilityProvider
+    private val notifPipeline = kosmos.notifPipeline
+    private val notifCollection = kosmos.mockNotifCollection
+    private val dumpManager = kosmos.dumpManager
+    private val mediaDataManager = mock<MediaDataManager>()
+    private val backgroundExecutor = FakeExecutor(FakeSystemClock())
+
+    private var listenerCaptor = argumentCaptor<MediaDataManager.Listener>()
+
+    private lateinit var notificationMediaManager: NotificationMediaManager
+
+    @Before
+    fun setup() {
+        notificationMediaManager =
+            NotificationMediaManager(
+                context,
+                visibilityProvider,
+                notifPipeline,
+                notifCollection,
+                mediaDataManager,
+                dumpManager,
+                backgroundExecutor,
+            )
+
+        verify(mediaDataManager).addListener(listenerCaptor.capture())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DISMISS)
+    fun mediaDataRemoved_userInitiated_dismissNotif() {
+        val notifEntryCaptor = argumentCaptor<NotificationEntry>()
+        val notifEntry = mock<NotificationEntry>()
+        whenever(notifEntry.key).thenReturn(KEY)
+        whenever(notifEntry.ranking).thenReturn(NotificationListenerService.Ranking())
+        whenever(notifPipeline.allNotifs).thenReturn(listOf(notifEntry))
+
+        listenerCaptor.lastValue.onMediaDataRemoved(KEY, true)
+
+        verify(notifCollection).dismissNotification(notifEntryCaptor.capture(), any())
+        assertThat(notifEntryCaptor.lastValue.key).isEqualTo(KEY)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DISMISS)
+    fun mediaDataRemoved_notUserInitiated_doesNotDismissNotif() {
+        listenerCaptor.lastValue.onMediaDataRemoved(KEY, false)
+
+        verify(notifCollection, never()).dismissNotification(any(), any())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DISMISS)
+    fun mediaDataRemoved_notUserInitiated_flagOff_dismissNotif() {
+        val notifEntryCaptor = argumentCaptor<NotificationEntry>()
+        val notifEntry = mock<NotificationEntry>()
+        whenever(notifEntry.key).thenReturn(KEY)
+        whenever(notifEntry.ranking).thenReturn(NotificationListenerService.Ranking())
+        whenever(notifPipeline.allNotifs).thenReturn(listOf(notifEntry))
+
+        listenerCaptor.lastValue.onMediaDataRemoved(KEY, false)
+
+        verify(notifCollection).dismissNotification(notifEntryCaptor.capture(), any())
+        assertThat(notifEntryCaptor.lastValue.key).isEqualTo(KEY)
+    }
+}
diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
index 76d10cc..a598007 100644
--- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
@@ -136,7 +136,8 @@
             <TextView
                 android:id="@+id/bluetooth_auto_on_toggle_title"
                 android:layout_width="0dp"
-                android:layout_height="68dp"
+                android:layout_height="wrap_content"
+                android:minHeight="68dp"
                 android:layout_marginBottom="20dp"
                 android:maxLines="2"
                 android:ellipsize="end"
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index d191a3c..d5bc10a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -65,7 +65,7 @@
     /**
      * Sent when some system ui state changes.
      */
-    void onSystemUiStateChanged(int stateFlags) = 16;
+    void onSystemUiStateChanged(long stateFlags) = 16;
 
     /**
      * Sent when suggested rotation button could be shown
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 94b6fd4..090033d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -162,5 +162,10 @@
     oneway void setOverrideHomeButtonLongPress(long duration, float slopMultiplier, boolean haptic)
             = 55;
 
-    // Next id = 56
+    /**
+     * Notifies to toggle quick settings panel.
+     */
+    oneway void toggleQuickSettingsPanel() = 56;
+
+    // Next id = 57
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index 69aa909..b4377ea 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -22,7 +22,7 @@
 
 import static com.android.systemui.shared.Flags.shadeAllowBackGesture;
 
-import android.annotation.IntDef;
+import android.annotation.LongDef;
 import android.content.Context;
 import android.content.res.Resources;
 import android.view.ViewConfiguration;
@@ -51,92 +51,94 @@
 
     // Overview is disabled, either because the device is in lock task mode, or because the device
     // policy has disabled the feature
-    public static final int SYSUI_STATE_SCREEN_PINNING = 1 << 0;
+    public static final long SYSUI_STATE_SCREEN_PINNING = 1L << 0;
     // The navigation bar is hidden due to immersive mode
-    public static final int SYSUI_STATE_NAV_BAR_HIDDEN = 1 << 1;
+    public static final long SYSUI_STATE_NAV_BAR_HIDDEN = 1L << 1;
     // The notification panel is expanded and interactive (either locked or unlocked), and the
     // quick settings is not expanded
-    public static final int SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED = 1 << 2;
+    public static final long SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED = 1L << 2;
     // The keyguard bouncer is showing
-    public static final int SYSUI_STATE_BOUNCER_SHOWING = 1 << 3;
+    public static final long SYSUI_STATE_BOUNCER_SHOWING = 1L << 3;
     // The navigation bar a11y button should be shown
-    public static final int SYSUI_STATE_A11Y_BUTTON_CLICKABLE = 1 << 4;
+    public static final long SYSUI_STATE_A11Y_BUTTON_CLICKABLE = 1L << 4;
     // The navigation bar a11y button shortcut is available
-    public static final int SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE = 1 << 5;
+    public static final long SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE = 1L << 5;
     // The keyguard is showing and not occluded
-    public static final int SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING = 1 << 6;
+    public static final long SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING = 1L << 6;
     // The recents feature is disabled (either by SUW/SysUI/device policy)
-    public static final int SYSUI_STATE_OVERVIEW_DISABLED = 1 << 7;
+    public static final long SYSUI_STATE_OVERVIEW_DISABLED = 1L << 7;
     // The home feature is disabled (either by SUW/SysUI/device policy)
-    public static final int SYSUI_STATE_HOME_DISABLED = 1 << 8;
+    public static final long SYSUI_STATE_HOME_DISABLED = 1L << 8;
     // The keyguard is showing, but occluded
-    public static final int SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED = 1 << 9;
+    public static final long SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED = 1L << 9;
     // The search feature is disabled (either by SUW/SysUI/device policy)
-    public static final int SYSUI_STATE_SEARCH_DISABLED = 1 << 10;
+    public static final long SYSUI_STATE_SEARCH_DISABLED = 1L << 10;
     // The notification panel is expanded and interactive (either locked or unlocked), and quick
     // settings is expanded.
-    public static final int SYSUI_STATE_QUICK_SETTINGS_EXPANDED = 1 << 11;
+    public static final long SYSUI_STATE_QUICK_SETTINGS_EXPANDED = 1L << 11;
     // Winscope tracing is enabled
-    public static final int SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION = 1 << 12;
+    public static final long SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION = 1L << 12;
     // The Assistant gesture should be constrained. It is up to the launcher implementation to
     // decide how to constrain it
-    public static final int SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED = 1 << 13;
+    public static final long SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED = 1L << 13;
     // The bubble stack is expanded. This means that the home gesture should be ignored, since a
     // swipe up is an attempt to close the bubble stack, but that the back gesture should remain
     // enabled (since it's used to navigate back within the bubbled app, or to collapse the bubble
     // stack.
-    public static final int SYSUI_STATE_BUBBLES_EXPANDED = 1 << 14;
+    public static final long SYSUI_STATE_BUBBLES_EXPANDED = 1L << 14;
     // A SysUI dialog is showing.
-    public static final int SYSUI_STATE_DIALOG_SHOWING = 1 << 15;
+    public static final long SYSUI_STATE_DIALOG_SHOWING = 1L << 15;
     // The one-handed mode is active
-    public static final int SYSUI_STATE_ONE_HANDED_ACTIVE = 1 << 16;
+    public static final long SYSUI_STATE_ONE_HANDED_ACTIVE = 1L << 16;
     // Allow system gesture no matter the system bar(s) is visible or not
-    public static final int SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY = 1 << 17;
+    public static final long SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY = 1L << 17;
     // The IME is showing
-    public static final int SYSUI_STATE_IME_SHOWING = 1 << 18;
+    public static final long SYSUI_STATE_IME_SHOWING = 1L << 18;
     // The window magnification is overlapped with system gesture insets at the bottom.
-    public static final int SYSUI_STATE_MAGNIFICATION_OVERLAP = 1 << 19;
+    public static final long SYSUI_STATE_MAGNIFICATION_OVERLAP = 1L << 19;
     // ImeSwitcher is showing
-    public static final int SYSUI_STATE_IME_SWITCHER_SHOWING = 1 << 20;
+    public static final long SYSUI_STATE_IME_SWITCHER_SHOWING = 1L << 20;
     // Device dozing/AOD state
-    public static final int SYSUI_STATE_DEVICE_DOZING = 1 << 21;
+    public static final long SYSUI_STATE_DEVICE_DOZING = 1L << 21;
     // The home feature is disabled (either by SUW/SysUI/device policy)
-    public static final int SYSUI_STATE_BACK_DISABLED = 1 << 22;
+    public static final long SYSUI_STATE_BACK_DISABLED = 1L << 22;
     // The bubble stack is expanded AND the mange menu for bubbles is expanded on top of it.
-    public static final int SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED = 1 << 23;
+    public static final long SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED = 1L << 23;
     // The voice interaction session window is showing
-    public static final int SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 25;
+    public static final long SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1L << 25;
     // Freeform windows are showing in desktop mode
-    public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26;
+    public static final long SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1L << 26;
     // Device dreaming state
-    public static final int SYSUI_STATE_DEVICE_DREAMING = 1 << 27;
+    public static final long SYSUI_STATE_DEVICE_DREAMING = 1L << 27;
     // Whether the device is currently awake (as opposed to asleep, see WakefulnessLifecycle).
     // Note that the device is awake on while waking up on, but not while going to sleep.
-    public static final int SYSUI_STATE_AWAKE = 1 << 28;
+    public static final long SYSUI_STATE_AWAKE = 1L << 28;
     // Whether the device is currently transitioning between awake/asleep indicated by
     // SYSUI_STATE_AWAKE.
-    public static final int SYSUI_STATE_WAKEFULNESS_TRANSITION = 1 << 29;
+    public static final long SYSUI_STATE_WAKEFULNESS_TRANSITION = 1L << 29;
     // The notification panel expansion fraction is > 0
-    public static final int SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE = 1 << 30;
+    public static final long SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE = 1L << 30;
     // When keyguard will be dismissed but didn't start animation yet
-    public static final int SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY = 1 << 31;
+    public static final long SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY = 1L << 31;
+    // Physical keyboard shortcuts helper is showing
+    public static final long SYSUI_STATE_SHORTCUT_HELPER_SHOWING = 1L << 32;
 
     // Mask for SystemUiStateFlags to isolate SYSUI_STATE_AWAKE and
     // SYSUI_STATE_WAKEFULNESS_TRANSITION, to match WAKEFULNESS_* constants
-    public static final int SYSUI_STATE_WAKEFULNESS_MASK =
+    public static final long SYSUI_STATE_WAKEFULNESS_MASK =
             SYSUI_STATE_AWAKE | SYSUI_STATE_WAKEFULNESS_TRANSITION;
     // Mirroring the WakefulnessLifecycle#Wakefulness states
-    public static final int WAKEFULNESS_ASLEEP = 0;
-    public static final int WAKEFULNESS_AWAKE = SYSUI_STATE_AWAKE;
-    public static final int WAKEFULNESS_GOING_TO_SLEEP = SYSUI_STATE_WAKEFULNESS_TRANSITION;
-    public static final int WAKEFULNESS_WAKING =
+    public static final long WAKEFULNESS_ASLEEP = 0;
+    public static final long WAKEFULNESS_AWAKE = SYSUI_STATE_AWAKE;
+    public static final long WAKEFULNESS_GOING_TO_SLEEP = SYSUI_STATE_WAKEFULNESS_TRANSITION;
+    public static final long WAKEFULNESS_WAKING =
             SYSUI_STATE_WAKEFULNESS_TRANSITION | SYSUI_STATE_AWAKE;
 
     // Whether the back gesture is allowed (or ignored) by the Shade
     public static final boolean ALLOW_BACK_GESTURE_IN_SHADE = shadeAllowBackGesture();
 
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({SYSUI_STATE_SCREEN_PINNING,
+    @LongDef({SYSUI_STATE_SCREEN_PINNING,
             SYSUI_STATE_NAV_BAR_HIDDEN,
             SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
             SYSUI_STATE_QUICK_SETTINGS_EXPANDED,
@@ -167,10 +169,11 @@
             SYSUI_STATE_WAKEFULNESS_TRANSITION,
             SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE,
             SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY,
+            SYSUI_STATE_SHORTCUT_HELPER_SHOWING,
     })
     public @interface SystemUiStateFlags {}
 
-    public static String getSystemUiStateString(int flags) {
+    public static String getSystemUiStateString(long flags) {
         StringJoiner str = new StringJoiner("|");
         if ((flags & SYSUI_STATE_SCREEN_PINNING) != 0) {
             str.add("screen_pinned");
@@ -265,6 +268,9 @@
         if ((flags & SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY) != 0) {
             str.add("keygrd_going_away");
         }
+        if ((flags & SYSUI_STATE_SHORTCUT_HELPER_SHOWING) != 0) {
+            str.add("shortcut_helper_showing");
+        }
 
         return str.toString();
     }
@@ -285,13 +291,13 @@
      * Returns whether the specified sysui state is such that the assistant gesture should be
      * disabled.
      */
-    public static boolean isAssistantGestureDisabled(int sysuiStateFlags) {
+    public static boolean isAssistantGestureDisabled(long sysuiStateFlags) {
         if ((sysuiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) {
             sysuiStateFlags &= ~SYSUI_STATE_NAV_BAR_HIDDEN;
         }
         // Disable when in quick settings, screen pinning, immersive, the bouncer is showing, 
         // or search is disabled
-        int disableFlags = SYSUI_STATE_SCREEN_PINNING
+        long disableFlags = SYSUI_STATE_SCREEN_PINNING
                 | SYSUI_STATE_NAV_BAR_HIDDEN
                 | SYSUI_STATE_BOUNCER_SHOWING
                 | SYSUI_STATE_SEARCH_DISABLED
@@ -313,7 +319,7 @@
      * Returns whether the specified sysui state is such that the back gesture should be
      * disabled.
      */
-    public static boolean isBackGestureDisabled(int sysuiStateFlags, boolean forTrackpad) {
+    public static boolean isBackGestureDisabled(long sysuiStateFlags, boolean forTrackpad) {
         // Always allow when the bouncer/global actions/voice session is showing (even on top of
         // the keyguard)
         if ((sysuiStateFlags & SYSUI_STATE_BOUNCER_SHOWING) != 0
@@ -328,9 +334,9 @@
         return (sysuiStateFlags & getBackGestureDisabledMask(forTrackpad)) != 0;
     }
 
-    private static int getBackGestureDisabledMask(boolean forTrackpad) {
+    private static long getBackGestureDisabledMask(boolean forTrackpad) {
         // Disable when in immersive, or the notifications are interactive
-        int disableFlags = SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
+        long disableFlags = SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
         if (!forTrackpad) {
             disableFlags |= SYSUI_STATE_NAV_BAR_HIDDEN;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index dd8c0df..f0230be 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -204,11 +204,7 @@
         isEnabled: Boolean,
         @StringRes infoResId: Int
     ) {
-        getAutoOnToggle(dialog).apply {
-            isChecked = isEnabled
-            setEnabled(true)
-            alpha = ENABLED_ALPHA
-        }
+        getAutoOnToggle(dialog).isChecked = isEnabled
         getAutoOnToggleInfoTextView(dialog).text = dialog.context.getString(infoResId)
     }
 
@@ -236,12 +232,8 @@
         }
 
         getAutoOnToggleView(dialog).visibility = initialUiProperties.autoOnToggleVisibility
-        getAutoOnToggle(dialog).setOnCheckedChangeListener { view, isChecked ->
+        getAutoOnToggle(dialog).setOnCheckedChangeListener { _, isChecked ->
             mutableBluetoothAutoOnToggle.value = isChecked
-            view.apply {
-                isEnabled = false
-                alpha = DISABLED_ALPHA
-            }
             uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
index 5653bc2..2eca02c 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
@@ -31,9 +31,12 @@
 import com.android.systemui.communal.domain.interactor.CommunalInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -88,6 +91,8 @@
     private final JavaAdapter mJavaAdapter;
     private final SystemClock mSystemClock;
     private final Lazy<SelectedUserInteractor> mUserInteractor;
+    private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractor;
+    private final Lazy<SceneContainerOcclusionInteractor> mSceneContainerOcclusionInteractor;
 
     private int mState;
     private boolean mShowingAod;
@@ -170,7 +175,9 @@
             JavaAdapter javaAdapter,
             SystemClock systemClock,
             Lazy<SelectedUserInteractor> userInteractor,
-            Lazy<CommunalInteractor> communalInteractorLazy) {
+            Lazy<CommunalInteractor> communalInteractorLazy,
+            Lazy<DeviceEntryInteractor> deviceEntryInteractor,
+            Lazy<SceneContainerOcclusionInteractor> sceneContainerOcclusionInteractor) {
         mFalsingDataProvider = falsingDataProvider;
         mFalsingManager = falsingManager;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -186,6 +193,8 @@
         mSystemClock = systemClock;
         mUserInteractor = userInteractor;
         mCommunalInteractorLazy = communalInteractorLazy;
+        mDeviceEntryInteractor = deviceEntryInteractor;
+        mSceneContainerOcclusionInteractor = sceneContainerOcclusionInteractor;
     }
 
     @Override
@@ -196,7 +205,18 @@
         mStatusBarStateController.addCallback(mStatusBarStateListener);
         mState = mStatusBarStateController.getState();
 
-        mKeyguardStateController.addCallback(mKeyguardStateControllerCallback);
+        if (SceneContainerFlag.isEnabled()) {
+            mJavaAdapter.alwaysCollectFlow(
+                    mDeviceEntryInteractor.get().isDeviceEntered(),
+                    this::isDeviceEnteredChanged
+            );
+            mJavaAdapter.alwaysCollectFlow(
+                    mSceneContainerOcclusionInteractor.get().getInvisibleDueToOcclusion(),
+                    this::isInvisibleDueToOcclusionChanged
+            );
+        } else {
+            mKeyguardStateController.addCallback(mKeyguardStateControllerCallback);
+        }
 
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
 
@@ -216,6 +236,14 @@
         mDockManager.addListener(mDockEventListener);
     }
 
+    public void isDeviceEnteredChanged(boolean unused) {
+        updateSensorRegistration();
+    }
+
+    public void isInvisibleDueToOcclusionChanged(boolean unused) {
+        updateSensorRegistration();
+    }
+
     @Override
     public void onSuccessfulUnlock() {
         logDebug("REAL: onSuccessfulUnlock");
@@ -302,7 +330,7 @@
     @Override
     public void onTouchEvent(MotionEvent ev) {
         logDebug("REAL: onTouchEvent(" + MotionEvent.actionToString(ev.getActionMasked()) + ")");
-        if (!mKeyguardStateController.isShowing()) {
+        if (!isKeyguardShowing()) {
             avoidGesture();
             return;
         }
@@ -402,8 +430,8 @@
         final boolean isKeyguard = mState == StatusBarState.KEYGUARD;
 
         final boolean isShadeOverOccludedKeyguard = mState == StatusBarState.SHADE
-                && mKeyguardStateController.isShowing()
-                && mKeyguardStateController.isOccluded();
+                && isKeyguardShowing()
+                && isKeyguardOccluded();
 
         return mScreenOn && !mShowingAod && (isKeyguard || isShadeOverOccludedKeyguard);
     }
@@ -447,6 +475,32 @@
         mFalsingManager.onProximityEvent(new ProximityEventImpl(proximityEvent));
     }
 
+    /**
+     * Returns {@code true} if the keyguard is showing (whether or not the screen is on, whether or
+     * not an activity is occluding the keyguard, and whether or not the shade is open on top of the
+     * keyguard), or {@code false} if the user has dismissed the keyguard by authenticating or
+     * swiping up.
+     */
+    private boolean isKeyguardShowing() {
+        if (SceneContainerFlag.isEnabled()) {
+            return !mDeviceEntryInteractor.get().isDeviceEntered().getValue();
+        } else {
+            return mKeyguardStateController.isShowing();
+        }
+    }
+
+    /**
+     * Returns {@code true} if there is an activity display on top of ("occluding") the keyguard, or
+     * {@code false} if an activity is not occluding the keyguard (including if the keyguard is not
+     * showing at all).
+     */
+    private boolean isKeyguardOccluded() {
+        if (SceneContainerFlag.isEnabled()) {
+            return mSceneContainerOcclusionInteractor.get().getInvisibleDueToOcclusion().getValue();
+        } else {
+            return mKeyguardStateController.isOccluded();
+        }
+    }
 
     static void logDebug(String msg) {
         if (DEBUG) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
index e2fed6d..e5a0e50 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
@@ -53,7 +53,7 @@
                 updateMediaModel(data)
             }
 
-            override fun onMediaDataRemoved(key: String) {
+            override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
                 updateMediaModel()
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 7f3274c..7655d7a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -247,7 +247,7 @@
         state: TransitionState
     ) {
         if (updateTransitionId != transitionId) {
-            Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
+            Log.w(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
             return
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
new file mode 100644
index 0000000..80bdc65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@SysUISingleton
+class LockscreenSceneTransitionRepository @Inject constructor() {
+
+    /**
+     * This [KeyguardState] will indicate which sub state within KTF should be navigated to when the
+     * next transition into the Lockscreen scene is started. It will be consumed exactly once and
+     * after that the state will be set back to [DEFAULT_STATE].
+     */
+    val nextLockscreenTargetState: MutableStateFlow<KeyguardState> = MutableStateFlow(DEFAULT_STATE)
+
+    companion object {
+        val DEFAULT_STATE = KeyguardState.LOCKSCREEN
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 2c05d49..30c6718 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -17,6 +17,8 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.annotation.FloatRange
+import android.annotation.SuppressLint
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -34,6 +36,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.util.kotlin.pairwise
+import java.util.UUID
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -76,6 +79,8 @@
      * single state. This prevent the redundant filters from running.
      */
     private val transitionValueCache = mutableMapOf<KeyguardState, MutableSharedFlow<Float>>()
+
+    @SuppressLint("SharedFlowCreation")
     private fun getTransitionValueFlow(state: KeyguardState): MutableSharedFlow<Float> {
         return transitionValueCache.getOrPut(state) {
             MutableSharedFlow<Float>(
@@ -90,6 +95,9 @@
     @Deprecated("Not performant - Use something else in this class")
     val transitions = repository.transitions
 
+    val transitionState: StateFlow<TransitionStep> =
+        transitions.stateIn(scope, SharingStarted.Eagerly, TransitionStep())
+
     /**
      * A pair of the most recent STARTED step, and the transition step immediately preceding it. The
      * transition framework enforces that the previous step is either a CANCELED or FINISHED step,
@@ -99,6 +107,7 @@
      * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming
      * from when we were canceled.
      */
+    @SuppressLint("SharedFlowCreation")
     val startedStepWithPrecedingStep =
         repository.transitions
             .pairwise()
@@ -144,9 +153,10 @@
     }
 
     /** Given an [edge], return a SharedFlow to collect only relevant [TransitionStep]. */
+    @SuppressLint("SharedFlowCreation")
     fun getOrCreateFlow(edge: Edge): MutableSharedFlow<TransitionStep> {
         return transitionMap.getOrPut(edge) {
-            MutableSharedFlow<TransitionStep>(
+            MutableSharedFlow(
                 extraBufferCapacity = 10,
                 onBufferOverflow = BufferOverflow.DROP_OLDEST
             )
@@ -180,6 +190,7 @@
      * AOD<->* transition information, mapped to dozeAmount range of AOD (1f) <->
      * * (0f).
      */
+    @SuppressLint("SharedFlowCreation")
     val dozeAmountTransition: Flow<TransitionStep> =
         repository.transitions
             .filter { step -> step.from == AOD || step.to == AOD }
@@ -201,11 +212,20 @@
         repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
 
     /** The destination state of the last [TransitionState.STARTED] transition. */
+    @SuppressLint("SharedFlowCreation")
     val startedKeyguardState: SharedFlow<KeyguardState> =
         startedKeyguardTransitionStep
             .map { step -> step.to }
             .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
+    /** The from state of the last [TransitionState.STARTED] transition. */
+    // TODO: is it performant to have several SharedFlows side by side instead of one?
+    @SuppressLint("SharedFlowCreation")
+    val startedKeyguardFromState: SharedFlow<KeyguardState> =
+        startedKeyguardTransitionStep
+            .map { step -> step.from }
+            .shareIn(scope, SharingStarted.Eagerly, replay = 1)
+
     /** Which keyguard state to use when the device goes to sleep. */
     val asleepKeyguardState: StateFlow<KeyguardState> =
         keyguardRepository.isAodAvailable
@@ -243,6 +263,7 @@
      * sufficient. However, if you're having issues with state *during* transitions started after
      * one or more canceled transitions, you probably need to use [currentKeyguardState].
      */
+    @SuppressLint("SharedFlowCreation")
     val finishedKeyguardState: SharedFlow<KeyguardState> =
         finishedKeyguardTransitionStep
             .map { step -> step.to }
@@ -491,7 +512,19 @@
         return startedKeyguardState.replayCache.last()
     }
 
+    fun getStartedFromState(): KeyguardState {
+        return startedKeyguardFromState.replayCache.last()
+    }
+
     fun getFinishedState(): KeyguardState {
         return finishedKeyguardState.replayCache.last()
     }
+
+    suspend fun startTransition(info: TransitionInfo) = repository.startTransition(info)
+
+    fun updateTransition(
+        transitionId: UUID,
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        state: TransitionState
+    ) = repository.updateTransition(transitionId, value, state)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 2d944c6..9443570 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -103,6 +103,7 @@
             KeyguardState.LOCKSCREEN -> true
             KeyguardState.GONE -> true
             KeyguardState.OCCLUDED -> true
+            KeyguardState.UNDEFINED -> true
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
index d95c38e..3c66186 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import com.android.systemui.CoreStartable
+import com.android.systemui.keyguard.domain.interactor.scenetransition.LockscreenSceneTransitionInteractor
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.ClassKey
@@ -31,6 +32,13 @@
     abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable
 
     @Binds
+    @IntoMap
+    @ClassKey(LockscreenSceneTransitionInteractor::class)
+    abstract fun bindLockscreenSceneTransitionInteractor(
+        impl: LockscreenSceneTransitionInteractor
+    ): CoreStartable
+
+    @Binds
     @IntoSet
     abstract fun fromPrimaryBouncer(
         impl: FromPrimaryBouncerTransitionInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
new file mode 100644
index 0000000..6e00aa7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor.scenetransition
+
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.LockscreenSceneTransitionRepository
+import com.android.systemui.keyguard.data.repository.LockscreenSceneTransitionRepository.Companion.DEFAULT_STATE
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.util.kotlin.pairwise
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/**
+ * This class listens to scene framework scene transitions and manages keyguard transition framework
+ * (KTF) states accordingly.
+ *
+ * There are a few rules:
+ * - When scene framework is on a scene outside of Lockscreen, then KTF is in state UNDEFINED
+ * - When scene framework is on Lockscreen, KTF is allowed to change its scenes freely
+ * - When scene framework is transitioning away from Lockscreen, then KTF transitions to UNDEFINED
+ *   and shares its progress.
+ * - When scene framework is transitioning to Lockscreen, then KTF starts a transition to LOCKSCREEN
+ *   but it is allowed to interrupt this transition and transition to other internal KTF states
+ *
+ * There are a few notable differences between SceneTransitionLayout (STL) and KTF that require
+ * special treatment when synchronizing both state machines.
+ * - STL does not emit cancelations as KTF does
+ * - Both STL and KTF require state continuity, though the rules from where starting the next
+ *   transition is allowed is different on each side:
+ *     - STL has a concept of "currentScene" which can be chosen to be either A or B in a A -> B
+ *       transition. The currentScene determines which transition can be started next. In KTF the
+ *       currentScene is always the `to` state. Which means transitions can only be started from B.
+ *       This also holds true when A -> B was canceled: the next transition needs to start from B.
+ *     - KTF can not settle back in its from scene, instead it needs to cancel and start a reversed
+ *       transition.
+ */
+@SysUISingleton
+class LockscreenSceneTransitionInteractor
+@Inject
+constructor(
+    val transitionInteractor: KeyguardTransitionInteractor,
+    @Application private val applicationScope: CoroutineScope,
+    private val sceneInteractor: SceneInteractor,
+    private val repository: LockscreenSceneTransitionRepository,
+) : CoreStartable, SceneInteractor.OnSceneAboutToChangeListener {
+
+    private var currentTransitionId: UUID? = null
+    private var progressJob: Job? = null
+
+    override fun start() {
+        sceneInteractor.registerSceneStateProcessor(this)
+        listenForSceneTransitionProgress()
+    }
+
+    override fun onSceneAboutToChange(toScene: SceneKey, sceneState: Any?) {
+        if (toScene != Scenes.Lockscreen || sceneState == null) return
+        if (sceneState !is KeyguardState) {
+            throw IllegalArgumentException("Lockscreen sceneState needs to be a KeyguardState.")
+        }
+        repository.nextLockscreenTargetState.value = sceneState
+    }
+
+    private fun listenForSceneTransitionProgress() {
+        applicationScope.launch {
+            sceneInteractor.transitionState
+                .pairwise(ObservableTransitionState.Idle(Scenes.Lockscreen))
+                .collect { (prevTransition, transition) ->
+                    when (transition) {
+                        is ObservableTransitionState.Idle -> handleIdle(prevTransition, transition)
+                        is ObservableTransitionState.Transition -> handleTransition(transition)
+                    }
+                }
+        }
+    }
+
+    private suspend fun handleIdle(
+        prevTransition: ObservableTransitionState,
+        idle: ObservableTransitionState.Idle
+    ) {
+        if (currentTransitionId == null) return
+        if (prevTransition !is ObservableTransitionState.Transition) return
+
+        if (idle.currentScene == prevTransition.toScene) {
+            finishCurrentTransition()
+        } else {
+            val targetState =
+                if (idle.currentScene == Scenes.Lockscreen) {
+                    transitionInteractor.getStartedFromState()
+                } else {
+                    UNDEFINED
+                }
+            finishReversedTransitionTo(targetState)
+        }
+    }
+
+    private fun finishCurrentTransition() {
+        transitionInteractor.updateTransition(currentTransitionId!!, 1f, FINISHED)
+        resetTransitionData()
+    }
+
+    private suspend fun finishReversedTransitionTo(state: KeyguardState) {
+        val newTransition =
+            TransitionInfo(
+                ownerName = this::class.java.simpleName,
+                from = transitionInteractor.getStartedState(),
+                to = state,
+                animator = null,
+                modeOnCanceled = TransitionModeOnCanceled.REVERSE
+            )
+        currentTransitionId = transitionInteractor.startTransition(newTransition)
+        transitionInteractor.updateTransition(currentTransitionId!!, 1f, FINISHED)
+        resetTransitionData()
+    }
+
+    private fun resetTransitionData() {
+        progressJob?.cancel()
+        progressJob = null
+        currentTransitionId = null
+    }
+
+    private suspend fun handleTransition(transition: ObservableTransitionState.Transition) {
+        if (transition.fromScene == Scenes.Lockscreen) {
+            if (currentTransitionId != null) {
+                val currentToState = transitionInteractor.getStartedState()
+                if (currentToState == UNDEFINED) {
+                    transitionKtfTo(transitionInteractor.getStartedFromState())
+                }
+            }
+            startTransitionFromLockscreen()
+            collectProgress(transition)
+        } else if (transition.toScene == Scenes.Lockscreen) {
+            if (currentTransitionId != null) {
+                transitionKtfTo(UNDEFINED)
+            }
+            startTransitionToLockscreen()
+            collectProgress(transition)
+        } else {
+            transitionKtfTo(UNDEFINED)
+        }
+    }
+
+    private suspend fun transitionKtfTo(state: KeyguardState) {
+        val currentTransition = transitionInteractor.transitionState.value
+        if (currentTransition.isFinishedIn(state)) {
+            // This is already the state we want to be in
+            resetTransitionData()
+        } else if (currentTransition.isTransitioning(to = state)) {
+            finishCurrentTransition()
+        } else {
+            finishReversedTransitionTo(state)
+        }
+    }
+
+    private fun collectProgress(transition: ObservableTransitionState.Transition) {
+        progressJob?.cancel()
+        progressJob = applicationScope.launch { transition.progress.collect { updateProgress(it) } }
+    }
+
+    private suspend fun startTransitionToLockscreen() {
+        val newTransition =
+            TransitionInfo(
+                ownerName = this::class.java.simpleName,
+                from = UNDEFINED,
+                to = repository.nextLockscreenTargetState.value,
+                animator = null,
+                modeOnCanceled = TransitionModeOnCanceled.RESET
+            )
+        repository.nextLockscreenTargetState.value = DEFAULT_STATE
+        startTransition(newTransition)
+    }
+
+    private suspend fun startTransitionFromLockscreen() {
+        val currentState = transitionInteractor.getStartedState()
+        val newTransition =
+            TransitionInfo(
+                ownerName = this::class.java.simpleName,
+                from = currentState,
+                to = UNDEFINED,
+                animator = null,
+                modeOnCanceled = TransitionModeOnCanceled.RESET
+            )
+        startTransition(newTransition)
+    }
+
+    private suspend fun startTransition(transitionInfo: TransitionInfo) {
+        if (currentTransitionId != null) {
+            resetTransitionData()
+        }
+        currentTransitionId = transitionInteractor.startTransition(transitionInfo)
+    }
+
+    private fun updateProgress(progress: Float) {
+        if (currentTransitionId == null) return
+        transitionInteractor.updateTransition(
+            currentTransitionId!!,
+            progress.coerceIn(0f, 1f),
+            RUNNING
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
index 7d05539..6d96db3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -17,7 +17,7 @@
 
 /** List of all possible states to transition to/from */
 enum class KeyguardState {
-    /*
+    /**
      * The display is completely off, as well as any sensors that would trigger the device to wake
      * up.
      */
@@ -29,13 +29,13 @@
      * notifications is enabled, allowing the device to quickly wake up.
      */
     DOZING,
-    /*
+    /**
      * A device state after the device times out, which can be from both LOCKSCREEN or GONE states.
      * DOZING is an example of special version of this state. Dreams may be implemented by third
      * parties to present their own UI over keyguard, like a screensaver.
      */
     DREAMING,
-    /*
+    /**
      * A device state after the device times out, which can be from both LOCKSCREEN or GONE states.
      * It is a special version of DREAMING state but not DOZING. The active dream will be windowless
      * and hosted in the lockscreen.
@@ -47,17 +47,17 @@
      * low-power mode without a UI, then it is DOZING.
      */
     AOD,
-    /*
+    /**
      * The security screen prompt containing UI to prompt the user to use a biometric credential
      * (ie: fingerprint). When supported, this may show before showing the primary bouncer.
      */
     ALTERNATE_BOUNCER,
-    /*
+    /**
      * The security screen prompt UI, containing PIN, Password, Pattern for the user to verify their
      * credentials.
      */
     PRIMARY_BOUNCER,
-    /*
+    /**
      * Device is actively displaying keyguard UI and is not in low-power mode. Device may be
      * unlocked if SWIPE security method is used, or if face lockscreen bypass is false.
      */
@@ -68,15 +68,20 @@
      * or dream, as well as swipe down for the notifications and up for the bouncer.
      */
     GLANCEABLE_HUB,
-    /*
-     * Keyguard is no longer visible. In most cases the user has just authenticated and keyguard
-     * is being removed, but there are other cases where the user is swiping away keyguard, such as
+    /**
+     * Keyguard is no longer visible. In most cases the user has just authenticated and keyguard is
+     * being removed, but there are other cases where the user is swiping away keyguard, such as
      * with SWIPE security method or face unlock without bypass.
      */
     GONE,
-    /*
-     * An activity is displaying over the keyguard.
+    /**
+     * Only used in scene framework. This means we are currently on any scene framework scene that
+     * is not Lockscreen. Transitions to and from UNDEFINED are always bound to the
+     * [SceneTransitionLayout] scene transition that either transitions to or from the Lockscreen
+     * scene. These transitions are automatically handled by [LockscreenSceneTransitionInteractor].
      */
+    UNDEFINED,
+    /** An activity is displaying over the keyguard. */
     OCCLUDED;
 
     companion object {
@@ -109,6 +114,7 @@
                 LOCKSCREEN -> true
                 GONE -> true
                 OCCLUDED -> true
+                UNDEFINED -> true
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
index 0fa6f4f..2b4c4af 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
@@ -30,4 +30,12 @@
         value: Float,
         transitionState: TransitionState,
     ) : this(info.from, info.to, value, transitionState, info.ownerName)
+
+    fun isTransitioning(from: KeyguardState? = null, to: KeyguardState? = null): Boolean {
+        return (from == null || this.from == from) && (to == null || this.to == to)
+    }
+
+    fun isFinishedIn(state: KeyguardState): Boolean {
+        return to == state && transitionState == TransitionState.FINISHED
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index 87324a2..6f8389f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -117,7 +117,8 @@
                             KeyguardState.DOZING,
                             KeyguardState.DREAMING,
                             KeyguardState.PRIMARY_BOUNCER,
-                            KeyguardState.AOD -> emit(0f)
+                            KeyguardState.AOD,
+                            KeyguardState.UNDEFINED -> emit(0f)
                             KeyguardState.ALTERNATE_BOUNCER,
                             KeyguardState.LOCKSCREEN -> emit(1f)
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index 53b2697..ae83c9e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -148,10 +148,11 @@
             KeyguardState.GLANCEABLE_HUB,
             KeyguardState.GONE,
             KeyguardState.OCCLUDED,
-            KeyguardState.DREAMING_LOCKSCREEN_HOSTED, -> 0f
+            KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
+            KeyguardState.UNDEFINED, -> 0f
             KeyguardState.AOD,
             KeyguardState.ALTERNATE_BOUNCER,
-            KeyguardState.LOCKSCREEN -> 1f
+            KeyguardState.LOCKSCREEN, -> 1f
         }
     }
     val useBackgroundProtection: StateFlow<Boolean> = isUdfpsSupported
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
index c02478b..96ef7d2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
@@ -206,11 +206,11 @@
         listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
     }
 
-    override fun onMediaDataRemoved(key: String) {
+    override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
         allEntries.remove(key)
         userEntries.remove(key)?.let {
             // Only notify listeners if something actually changed
-            listeners.forEach { it.onMediaDataRemoved(key) }
+            listeners.forEach { it.onMediaDataRemoved(key, userInitiated) }
         }
     }
 
@@ -246,7 +246,7 @@
                 // Only remove media when the profile is unavailable.
                 if (DEBUG) Log.d(TAG, "Removing $key after profile change")
                 userEntries.remove(key, data)
-                listeners.forEach { listener -> listener.onMediaDataRemoved(key) }
+                listeners.forEach { listener -> listener.onMediaDataRemoved(key, false) }
             }
         }
     }
@@ -261,7 +261,7 @@
         userEntries.clear()
         keyCopy.forEach {
             if (DEBUG) Log.d(TAG, "Removing $it after user change")
-            listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) }
+            listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it, false) }
         }
 
         allEntries.forEach { (key, data) ->
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 3a831156..143d66b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -545,8 +545,8 @@
      * External listeners registered with [addListener] will be notified after the event propagates
      * through the internal listener pipeline.
      */
-    private fun notifyMediaDataRemoved(key: String) {
-        internalListeners.forEach { it.onMediaDataRemoved(key) }
+    private fun notifyMediaDataRemoved(key: String, userInitiated: Boolean = false) {
+        internalListeners.forEach { it.onMediaDataRemoved(key, userInitiated) }
     }
 
     /**
@@ -578,7 +578,7 @@
             if (it.active == !timedOut && !forceUpdate) {
                 if (it.resumption) {
                     if (DEBUG) Log.d(TAG, "timing out resume player $key")
-                    dismissMediaData(key, 0L /* delay */)
+                    dismissMediaData(key, delay = 0L, userInitiated = false)
                 }
                 return
             }
@@ -627,17 +627,17 @@
         }
     }
 
-    private fun removeEntry(key: String, logEvent: Boolean = true) {
+    private fun removeEntry(key: String, logEvent: Boolean = true, userInitiated: Boolean = false) {
         mediaEntries.remove(key)?.let {
             if (logEvent) {
                 logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
             }
         }
-        notifyMediaDataRemoved(key)
+        notifyMediaDataRemoved(key, userInitiated)
     }
 
     /** Dismiss a media entry. Returns false if the key was not found. */
-    override fun dismissMediaData(key: String, delay: Long): Boolean {
+    override fun dismissMediaData(key: String, delay: Long, userInitiated: Boolean): Boolean {
         val existed = mediaEntries[key] != null
         backgroundExecutor.execute {
             mediaEntries[key]?.let { mediaData ->
@@ -649,7 +649,10 @@
                 }
             }
         }
-        foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
+        foregroundExecutor.executeDelayed(
+            { removeEntry(key = key, userInitiated = userInitiated) },
+            delay
+        )
         return existed
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatest.kt
index ad70db5..88910f9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatest.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatest.kt
@@ -53,8 +53,8 @@
         listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, data) }
     }
 
-    override fun onMediaDataRemoved(key: String) {
-        remove(key)
+    override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
+        remove(key, userInitiated)
     }
 
     override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
@@ -71,8 +71,8 @@
         }
     }
 
-    override fun onKeyRemoved(key: String) {
-        remove(key)
+    override fun onKeyRemoved(key: String, userInitiated: Boolean) {
+        remove(key, userInitiated)
     }
 
     /**
@@ -92,10 +92,10 @@
         }
     }
 
-    private fun remove(key: String) {
+    private fun remove(key: String, userInitiated: Boolean) {
         entries.remove(key)?.let {
             val listenersCopy = listeners.toSet()
-            listenersCopy.forEach { it.onMediaDataRemoved(key) }
+            listenersCopy.forEach { it.onMediaDataRemoved(key, userInitiated) }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
index 5432a18..8d19ce8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
@@ -213,7 +213,7 @@
         listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
     }
 
-    override fun onMediaDataRemoved(key: String) {
+    override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
         mediaFilterRepository.removeMediaEntry(key)?.let { mediaData ->
             val instanceId = mediaData.instanceId
             mediaFilterRepository.removeSelectedUserMediaEntry(instanceId)?.let {
@@ -221,7 +221,7 @@
                     MediaDataLoadingModel.Removed(instanceId)
                 )
                 // Only notify listeners if something actually changed
-                listeners.forEach { it.onMediaDataRemoved(key) }
+                listeners.forEach { it.onMediaDataRemoved(key, userInitiated) }
             }
         }
     }
@@ -270,7 +270,7 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Removed(data.instanceId)
                 )
-                listeners.forEach { listener -> listener.onMediaDataRemoved(key) }
+                listeners.forEach { listener -> listener.onMediaDataRemoved(key, false) }
             }
         }
     }
@@ -288,7 +288,7 @@
                 MediaDataLoadingModel.Removed(instanceId)
             )
             getKey(instanceId)?.let {
-                listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) }
+                listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it, false) }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
index 2331aa21..8099e59 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
@@ -60,7 +60,7 @@
     )
 
     /** Dismiss a media entry. Returns false if the key was not found. */
-    fun dismissMediaData(key: String, delay: Long): Boolean
+    fun dismissMediaData(key: String, delay: Long, userInitiated: Boolean): Boolean
 
     /**
      * Called whenever the recommendation has been expired or removed by the user. This will remove
@@ -136,7 +136,7 @@
         ) {}
 
         /** Called whenever a previously existing Media notification was removed. */
-        override fun onMediaDataRemoved(key: String) {}
+        override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {}
 
         /**
          * Called whenever a previously existing Smartspace media data was removed.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 1d7c025..eed7752 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -498,8 +498,8 @@
      * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
      * after the event propagates through the internal listener pipeline.
      */
-    private fun notifyMediaDataRemoved(key: String) {
-        internalListeners.forEach { it.onMediaDataRemoved(key) }
+    private fun notifyMediaDataRemoved(key: String, userInitiated: Boolean = false) {
+        internalListeners.forEach { it.onMediaDataRemoved(key, userInitiated) }
     }
 
     /**
@@ -531,7 +531,7 @@
             if (it.active == !timedOut && !forceUpdate) {
                 if (it.resumption) {
                     if (DEBUG) Log.d(TAG, "timing out resume player $key")
-                    dismissMediaData(key, 0L /* delay */)
+                    dismissMediaData(key, delayMs = 0L, userInitiated = false)
                 }
                 return
             }
@@ -580,17 +580,17 @@
         }
     }
 
-    private fun removeEntry(key: String, logEvent: Boolean = true) {
+    private fun removeEntry(key: String, logEvent: Boolean = true, userInitiated: Boolean = false) {
         mediaDataRepository.removeMediaEntry(key)?.let {
             if (logEvent) {
                 logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
             }
         }
-        notifyMediaDataRemoved(key)
+        notifyMediaDataRemoved(key, userInitiated)
     }
 
     /** Dismiss a media entry. Returns false if the key was not found. */
-    fun dismissMediaData(key: String, delayMs: Long): Boolean {
+    fun dismissMediaData(key: String, delayMs: Long, userInitiated: Boolean): Boolean {
         val existed = mediaDataRepository.mediaEntries.value[key] != null
         backgroundExecutor.execute {
             mediaDataRepository.mediaEntries.value[key]?.let { mediaData ->
@@ -602,16 +602,19 @@
                 }
             }
         }
-        foregroundExecutor.executeDelayed({ removeEntry(key) }, delayMs)
+        foregroundExecutor.executeDelayed(
+            { removeEntry(key, userInitiated = userInitiated) },
+            delayMs
+        )
         return existed
     }
 
     /** Dismiss a media entry. Returns false if the corresponding key was not found. */
-    fun dismissMediaData(instanceId: InstanceId, delayMs: Long): Boolean {
+    fun dismissMediaData(instanceId: InstanceId, delayMs: Long, userInitiated: Boolean): Boolean {
         val mediaEntries = mediaDataRepository.mediaEntries.value
         val filteredEntries = mediaEntries.filter { (_, data) -> data.instanceId == instanceId }
         return if (filteredEntries.isNotEmpty()) {
-            dismissMediaData(filteredEntries.keys.first(), delayMs)
+            dismissMediaData(filteredEntries.keys.first(), delayMs, userInitiated)
         } else {
             false
         }
@@ -1579,7 +1582,7 @@
         ) {}
 
         /** Called whenever a previously existing Media notification was removed. */
-        fun onMediaDataRemoved(key: String) {}
+        fun onMediaDataRemoved(key: String, userInitiated: Boolean) {}
 
         /**
          * Called whenever a previously existing Smartspace media data was removed.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
index 0e2814b..043fbfa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
@@ -111,10 +111,10 @@
         }
     }
 
-    override fun onMediaDataRemoved(key: String) {
+    override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
         val token = entries.remove(key)
         token?.stop()
-        token?.let { listeners.forEach { it.onKeyRemoved(key) } }
+        token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
     }
 
     fun dump(pw: PrintWriter) {
@@ -136,7 +136,7 @@
         /** Called when the route has changed for a given notification. */
         fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)
         /** Called when the notification was removed. */
-        fun onKeyRemoved(key: String)
+        fun onKeyRemoved(key: String, userInitiated: Boolean)
     }
 
     private inner class Entry(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilter.kt
index b2a8f2e..b178d84 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilter.kt
@@ -137,7 +137,7 @@
                 // farther and dismiss the media data so that media controls for the local session
                 // don't hang around while casting.
                 if (!keyedTokens.get(key)!!.contains(TokenId(remote.sessionToken))) {
-                    dispatchMediaDataRemoved(key)
+                    dispatchMediaDataRemoved(key, userInitiated = false)
                 }
             }
         }
@@ -151,11 +151,11 @@
         backgroundExecutor.execute { dispatchSmartspaceMediaDataLoaded(key, data) }
     }
 
-    override fun onMediaDataRemoved(key: String) {
+    override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
         // Queue on background thread to ensure ordering of loaded and removed events is maintained.
         backgroundExecutor.execute {
             keyedTokens.remove(key)
-            dispatchMediaDataRemoved(key)
+            dispatchMediaDataRemoved(key, userInitiated)
         }
     }
 
@@ -174,8 +174,10 @@
         }
     }
 
-    private fun dispatchMediaDataRemoved(key: String) {
-        foregroundExecutor.execute { listeners.toSet().forEach { it.onMediaDataRemoved(key) } }
+    private fun dispatchMediaDataRemoved(key: String, userInitiated: Boolean) {
+        foregroundExecutor.execute {
+            listeners.toSet().forEach { it.onMediaDataRemoved(key, userInitiated) }
+        }
     }
 
     private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index 29f3967..fc31903 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -169,7 +169,7 @@
         mediaListeners[key] = PlaybackStateListener(key, data)
     }
 
-    override fun onMediaDataRemoved(key: String) {
+    override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
         mediaListeners.remove(key)?.destroy()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
index c888935..9e62300 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
@@ -205,12 +205,12 @@
         )
     }
 
-    override fun dismissMediaData(key: String, delay: Long): Boolean {
-        return mediaDataProcessor.dismissMediaData(key, delay)
+    override fun dismissMediaData(key: String, delay: Long, userInitiated: Boolean): Boolean {
+        return mediaDataProcessor.dismissMediaData(key, delay, userInitiated)
     }
 
     fun removeMediaControl(instanceId: InstanceId, delay: Long) {
-        mediaDataProcessor.dismissMediaData(instanceId, delay)
+        mediaDataProcessor.dismissMediaData(instanceId, delay, userInitiated = false)
     }
 
     override fun dismissSmartspaceRecommendation(key: String, delay: Long) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
index 9f2d132..d1fee90 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
@@ -90,7 +90,8 @@
         instanceId: InstanceId,
         delayMs: Long
     ): Boolean {
-        val dismissed = mediaDataProcessor.dismissMediaData(instanceId, delayMs)
+        val dismissed =
+            mediaDataProcessor.dismissMediaData(instanceId, delayMs, userInitiated = true)
         if (!dismissed) {
             Log.w(
                 TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index 45b68ca..b072534 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -193,6 +193,7 @@
     private val mediaContent: ViewGroup
     @VisibleForTesting var pageIndicator: PageIndicator
     private var needsReordering: Boolean = false
+    private var isUserInitiatedRemovalQueued: Boolean = false
     private var keysNeedRemoval = mutableSetOf<String>()
     var shouldScrollToKey: Boolean = false
     private var isRtl: Boolean = false
@@ -385,12 +386,15 @@
                 reorderAllPlayers(previousVisiblePlayerKey = null)
             }
 
-            keysNeedRemoval.forEach { removePlayer(it) }
+            keysNeedRemoval.forEach {
+                removePlayer(it, userInitiated = isUserInitiatedRemovalQueued)
+            }
             if (keysNeedRemoval.size > 0) {
                 // Carousel visibility may need to be updated after late removals
                 updateHostVisibility()
             }
             keysNeedRemoval.clear()
+            isUserInitiatedRemovalQueued = false
 
             // Update user visibility so that no extra impression will be logged when
             // activeMediaIndex resets to 0
@@ -474,18 +478,18 @@
 
                     val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
                     if (canRemove && !Utils.useMediaResumption(context)) {
-                        // This view isn't playing, let's remove this! This happens e.g. when
-                        // dismissing/timing out a view. We still have the data around because
-                        // resumption could be on, but we should save the resources and release
-                        // this.
+                        // This media control is both paused and timed out, and the resumption
+                        // setting is off - let's remove it
                         if (isReorderingAllowed) {
-                            onMediaDataRemoved(key)
+                            onMediaDataRemoved(key, userInitiated = MediaPlayerData.isSwipedAway)
                         } else {
+                            isUserInitiatedRemovalQueued = MediaPlayerData.isSwipedAway
                             keysNeedRemoval.add(key)
                         }
                     } else {
                         keysNeedRemoval.remove(key)
                     }
+                    MediaPlayerData.isSwipedAway = false
                 }
 
                 override fun onSmartspaceMediaDataLoaded(
@@ -565,11 +569,12 @@
                             addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
                         }
                     }
+                    MediaPlayerData.isSwipedAway = false
                 }
 
-                override fun onMediaDataRemoved(key: String) {
-                    debugLogger.logMediaRemoved(key)
-                    removePlayer(key)
+                override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
+                    debugLogger.logMediaRemoved(key, userInitiated)
+                    removePlayer(key, userInitiated = userInitiated)
                 }
 
                 override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
@@ -1033,7 +1038,8 @@
     fun removePlayer(
         key: String,
         dismissMediaData: Boolean = true,
-        dismissRecommendation: Boolean = true
+        dismissRecommendation: Boolean = true,
+        userInitiated: Boolean = false,
     ): MediaControlPanel? {
         if (key == MediaPlayerData.smartspaceMediaKey()) {
             MediaPlayerData.smartspaceMediaData?.let {
@@ -1052,7 +1058,7 @@
 
             if (dismissMediaData) {
                 // Inform the media manager of a potentially late dismissal
-                mediaManager.dismissMediaData(key, delay = 0L)
+                mediaManager.dismissMediaData(key, delay = 0L, userInitiated = userInitiated)
             }
             if (dismissRecommendation) {
                 // Inform the media manager of a potentially late dismissal
@@ -1512,7 +1518,8 @@
         }
     }
 
-    private fun onSwipeToDismiss() {
+    @VisibleForTesting
+    fun onSwipeToDismiss() {
         if (mediaFlags.isMediaControlsRefactorEnabled()) {
             mediaCarouselViewModel.onSwipeToDismiss()
             return
@@ -1531,6 +1538,7 @@
                 it.mIsImpressed = false
             }
         }
+        MediaPlayerData.isSwipedAway = true
         logger.logSwipeDismiss()
         mediaManager.onSwipeToDismiss()
     }
@@ -1557,6 +1565,7 @@
                 "state: ${desiredHostState?.expansion}, " +
                     "only active ${desiredHostState?.showsOnlyActiveMedia}"
             )
+            println("isSwipedAway: ${MediaPlayerData.isSwipedAway}")
         }
     }
 }
@@ -1595,7 +1604,7 @@
         val data: MediaData,
         val key: String,
         val updateTime: Long = 0,
-        val isSsReactivated: Boolean = false
+        val isSsReactivated: Boolean = false,
     )
 
     private val comparator =
@@ -1620,6 +1629,9 @@
     // A map that tracks order of visible media players before they get reordered.
     private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>()
 
+    // Whether the user swiped away the carousel since its last update
+    internal var isSwipedAway: Boolean = false
+
     fun addMediaPlayer(
         key: String,
         data: MediaData,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLogger.kt
index ebf1c6a..1be25a7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLogger.kt
@@ -53,8 +53,16 @@
             { "add player $str1, active: $bool1" }
         )
 
-    fun logMediaRemoved(key: String) =
-        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" })
+    fun logMediaRemoved(key: String, userInitiated: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = userInitiated
+            },
+            { "removing player $str1, by user $bool1" }
+        )
 
     fun logRecommendationLoaded(key: String, isActive: Boolean) =
         buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index e6c785e..0bc3c439 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -786,10 +786,11 @@
             if (mKey != null) {
                 closeGuts();
                 if (!mMediaDataManagerLazy.get().dismissMediaData(mKey,
-                        MediaViewController.GUTS_ANIMATION_DURATION + 100)) {
+                        /* delay */ MediaViewController.GUTS_ANIMATION_DURATION + 100,
+                        /* userInitiated */ true)) {
                     Log.w(TAG, "Manager failed to dismiss media " + mKey);
                     // Remove directly from carousel so user isn't stuck with defunct controls
-                    mMediaCarouselController.removePlayer(mKey, false, false);
+                    mMediaCarouselController.removePlayer(mKey, false, false, true);
                 }
             } else {
                 Log.w(TAG, "Dismiss media with null notification. Token uid="
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
index eca76b6..91050c8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
@@ -105,7 +105,7 @@
                 updateViewVisibility()
             }
 
-            override fun onMediaDataRemoved(key: String) {
+            override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
                 updateViewVisibility()
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
index 88a5f78..061e7ec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
@@ -48,7 +48,7 @@
         }
 
         @Override
-        public void onMediaDataRemoved(@NonNull String key) {
+        public void onMediaDataRemoved(@NonNull String key, boolean userInitiated) {
             final boolean hasActiveMedia = mMediaDataManager.hasActiveMedia();
             if (DEBUG) {
                 Log.d(TAG, "onMediaDataRemoved(" + key + "), mAdded=" + mAdded + ", hasActiveMedia="
diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
index 89e4760..a144dc2 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags
 import dagger.Lazy
 import javax.inject.Inject
 
@@ -48,7 +49,7 @@
      * Returns an override value for the given [flag] or `null` if the scene framework isn't enabled
      * or if the flag value doesn't need to be overridden.
      */
-    fun flagValueOverride(flag: Int): Boolean? {
+    fun flagValueOverride(@SystemUiStateFlags flag: Long): Boolean? {
         if (!SceneContainerFlag.isEnabled) {
             return null
         }
@@ -79,7 +80,7 @@
          * to be overridden by the scene framework.
          */
         val EvaluatorByFlag =
-            mapOf<Int, (SceneContainerPluginState) -> Boolean>(
+            mapOf<Long, (SceneContainerPluginState) -> Boolean>(
                 SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to { it.scene != Scenes.Gone },
                 SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to
                     {
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
index 2dd2327..481b476 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
@@ -23,6 +23,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -42,10 +43,10 @@
 
     private final DisplayTracker mDisplayTracker;
     private final SceneContainerPlugin mSceneContainerPlugin;
-    private @QuickStepContract.SystemUiStateFlags int mFlags;
+    private @SystemUiStateFlags long mFlags;
     private final List<SysUiStateCallback> mCallbacks = new ArrayList<>();
-    private int mFlagsToSet = 0;
-    private int mFlagsToClear = 0;
+    private long mFlagsToSet = 0;
+    private long mFlagsToClear = 0;
 
     public SysUiState(DisplayTracker displayTracker, SceneContainerPlugin sceneContainerPlugin) {
         mDisplayTracker = displayTracker;
@@ -67,12 +68,13 @@
     }
 
     /** Returns the current sysui state flags. */
-    public int getFlags() {
+    @SystemUiStateFlags
+    public long getFlags() {
         return mFlags;
     }
 
     /** Methods to this call can be chained together before calling {@link #commitUpdate(int)}. */
-    public SysUiState setFlag(int flag, boolean enabled) {
+    public SysUiState setFlag(@SystemUiStateFlags long flag, boolean enabled) {
         final Boolean overrideOrNull = mSceneContainerPlugin.flagValueOverride(flag);
         if (overrideOrNull != null && enabled != overrideOrNull) {
             if (DEBUG) {
@@ -91,7 +93,7 @@
         return this;
     }
 
-    /** Call to save all the flags updated from {@link #setFlag(int, boolean)}. */
+    /** Call to save all the flags updated from {@link #setFlag(long, boolean)}. */
     public void commitUpdate(int displayId) {
         updateFlags(displayId);
         mFlagsToSet = 0;
@@ -105,14 +107,14 @@
             return;
         }
 
-        int newState = mFlags;
+        long newState = mFlags;
         newState |= mFlagsToSet;
         newState &= ~mFlagsToClear;
         notifyAndSetSystemUiStateChanged(newState, mFlags);
     }
 
     /** Notify all those who are registered that the state has changed. */
-    private void notifyAndSetSystemUiStateChanged(int newFlags, int oldFlags) {
+    private void notifyAndSetSystemUiStateChanged(long newFlags, long oldFlags) {
         if (DEBUG) {
             Log.d(TAG, "SysUiState changed: old=" + oldFlags + " new=" + newFlags);
         }
@@ -137,7 +139,7 @@
     /** Callback to be notified whenever system UI state flags are changed. */
     public interface SysUiStateCallback{
         /** To be called when any SysUiStateFlag gets updated */
-        void onSystemUiStateChanged(@QuickStepContract.SystemUiStateFlags int sysUiFlags);
+        void onSystemUiStateChanged(@SystemUiStateFlags long sysUiFlags);
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
index 5c49156..1e18f24 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
@@ -40,7 +40,7 @@
  */
 fun SysUiState.updateFlags(
     @DisplayId displayId: Int,
-    vararg flagValuePairs: Pair<Int, Boolean>,
+    vararg flagValuePairs: Pair<Long, Boolean>,
 ) {
     flagValuePairs.forEach { (flag, enabled) -> setFlag(flag, enabled) }
     commitUpdate(displayId)
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index a6b6d61..80c4379 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -128,7 +128,7 @@
     private boolean mLongPressHomeEnabled;
     private boolean mAssistantTouchGestureEnabled;
     private int mNavBarMode;
-    private int mA11yButtonState;
+    private long mA11yButtonState;
     private int mRotationWatcherRotation;
     private boolean mTogglingNavbarTaskbar;
     private boolean mWallpaperVisible;
@@ -374,7 +374,7 @@
      * {@link Secure#ACCESSIBILITY_BUTTON_MODE_GESTURE}, otherwise it is reset to 0.
      */
     private void updateA11yState() {
-        final int prevState = mA11yButtonState;
+        final long prevState = mA11yButtonState;
         final boolean clickable;
         final boolean longClickable;
         if (mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()
@@ -431,7 +431,7 @@
      * 48 = the combination of {@link QuickStepContract#SYSUI_STATE_A11Y_BUTTON_CLICKABLE} and
      * {@link QuickStepContract#SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE}
      */
-    public int getA11yButtonState() {
+    public long getA11yButtonState() {
         return mA11yButtonState;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 906ebad..0e819c2 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -1602,7 +1602,7 @@
     void updateAccessibilityStateFlags() {
         mLongPressHomeEnabled = mNavBarHelper.getLongPressHomeEnabled();
         if (mView != null) {
-            int a11yFlags = mNavBarHelper.getA11yButtonState();
+            long a11yFlags = mNavBarHelper.getA11yButtonState();
             boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
             boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
             mView.setAccessibilityButtonState(clickable, longClickable);
@@ -1611,7 +1611,7 @@
     }
 
     public void updateSystemUiStateFlags() {
-        int a11yFlags = mNavBarHelper.getA11yButtonState();
+        long a11yFlags = mNavBarHelper.getA11yButtonState();
         boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
         boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index f67973b..b360af0 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -298,7 +298,7 @@
     }
 
     private void updateSysuiFlags() {
-        int a11yFlags = mNavBarHelper.getA11yButtonState();
+        long a11yFlags = mNavBarHelper.getA11yButtonState();
         boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
         boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
 
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 295b293..9487085 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -84,6 +84,7 @@
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -270,7 +271,8 @@
     private BackAnimation mBackAnimation;
     private int mLeftInset;
     private int mRightInset;
-    private int mSysUiFlags;
+    @SystemUiStateFlags
+    private long mSysUiFlags;
 
     // For Tf-Lite model.
     private BackGestureTfClassifierProvider mBackGestureTfClassifierProvider;
@@ -334,7 +336,7 @@
     private final SysUiState.SysUiStateCallback mSysUiStateCallback =
             new SysUiState.SysUiStateCallback() {
         @Override
-        public void onSystemUiStateChanged(int sysUiFlags) {
+        public void onSystemUiStateChanged(@SystemUiStateFlags long sysUiFlags) {
             mSysUiFlags = sysUiFlags;
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index faf2bbc..e4cb211 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -108,6 +108,7 @@
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -117,6 +118,8 @@
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.sysui.ShellInterface;
 
+import dagger.Lazy;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -128,8 +131,6 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 
-import dagger.Lazy;
-
 /**
  * Class to send information from overview to launcher with a binder.
  */
@@ -414,7 +415,13 @@
         @Override
         public void toggleNotificationPanel() {
             verifyCallerAndClearCallingIdentityPostMain("toggleNotificationPanel", () ->
-                    mCommandQueue.togglePanel());
+                    mCommandQueue.toggleNotificationsPanel());
+        }
+
+        @Override
+        public void toggleQuickSettingsPanel() {
+            verifyCallerAndClearCallingIdentityPostMain("toggleQuickSettingsPanel", () ->
+                    mCommandQueue.toggleQuickSettingsPanel());
         }
 
         private boolean verifyCaller(String reason) {
@@ -765,7 +772,7 @@
         }
     }
 
-    private void notifySystemUiStateFlags(int flags) {
+    private void notifySystemUiStateFlags(@SystemUiStateFlags long flags) {
         if (SysUiState.DEBUG) {
             Log.d(TAG_OPS, "Notifying sysui state change to overview service: proxy="
                     + mOverviewProxy + " flags=" + flags);
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
index eabc42b..3e2c630 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
@@ -21,6 +21,7 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.TransitionKey
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.SceneDataSource
@@ -36,6 +37,7 @@
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
 
+@SysUISingleton
 /** Source of truth for scene framework application state. */
 class SceneContainerRepository
 @Inject
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 08efe39..0d0f6e0 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -55,6 +55,18 @@
     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
 ) {
 
+    interface OnSceneAboutToChangeListener {
+
+        /**
+         * Notifies that the scene is about to change to [toScene].
+         *
+         * The implementation can choose to consume the [sceneState] to prepare the incoming scene.
+         */
+        fun onSceneAboutToChange(toScene: SceneKey, sceneState: Any?)
+    }
+
+    private val onSceneAboutToChangeListener = mutableSetOf<OnSceneAboutToChangeListener>()
+
     /**
      * The current scene.
      *
@@ -149,6 +161,10 @@
         return repository.allSceneKeys()
     }
 
+    fun registerSceneStateProcessor(processor: OnSceneAboutToChangeListener) {
+        onSceneAboutToChangeListener.add(processor)
+    }
+
     /**
      * Requests a scene change to the given scene.
      *
@@ -161,6 +177,7 @@
         toScene: SceneKey,
         loggingReason: String,
         transitionKey: TransitionKey? = null,
+        sceneState: Any? = null,
     ) {
         val currentSceneKey = currentScene.value
         if (
@@ -180,6 +197,7 @@
             isInstant = false,
         )
 
+        onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(toScene, sceneState) }
         repository.changeScene(toScene, transitionKey)
     }
 
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 4a64277..3ce12dd 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
@@ -234,7 +234,7 @@
             bouncerInteractor.onImeHiddenByUser.collectLatest {
                 if (sceneInteractor.currentScene.value == Scenes.Bouncer) {
                     sceneInteractor.changeScene(
-                        toScene = Scenes.Lockscreen,
+                        toScene = Scenes.Lockscreen, // TODO(b/336581871): add sceneState?
                         loggingReason = "IME hidden",
                     )
                 }
@@ -252,6 +252,7 @@
                     when {
                         isAnySimLocked -> {
                             switchToScene(
+                                // TODO(b/336581871): add sceneState?
                                 targetSceneKey = Scenes.Bouncer,
                                 loggingReason = "Need to authenticate locked SIM card."
                             )
@@ -259,6 +260,7 @@
                         unlockStatus.isUnlocked &&
                             deviceEntryInteractor.canSwipeToEnter.value == false -> {
                             switchToScene(
+                                // TODO(b/336581871): add sceneState?
                                 targetSceneKey = Scenes.Gone,
                                 loggingReason =
                                     "All SIM cards unlocked and device already unlocked and " +
@@ -267,6 +269,7 @@
                         }
                         else -> {
                             switchToScene(
+                                // TODO(b/336581871): add sceneState?
                                 targetSceneKey = Scenes.Lockscreen,
                                 loggingReason =
                                     "All SIM cards unlocked and device still locked" +
@@ -325,7 +328,8 @@
                                 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 ->
@@ -364,6 +368,7 @@
             powerInteractor.isAsleep.collect { isAsleep ->
                 if (isAsleep) {
                     switchToScene(
+                        // TODO(b/336581871): add sceneState?
                         targetSceneKey = Scenes.Lockscreen,
                         loggingReason = "device is starting to sleep",
                     )
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
index bd93226..969cf48 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
@@ -22,6 +22,8 @@
 import android.graphics.Rect
 import android.graphics.Region
 import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.GestureDetector.SimpleOnGestureListener
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
@@ -43,13 +45,43 @@
     private val displayMetrics = context.resources.displayMetrics
     private val tmpRect = Rect()
     private lateinit var actionsContainerBackground: View
+    private lateinit var actionsContainer: View
     private lateinit var dismissButton: View
 
+    // Prepare an internal `GestureDetector` to determine when we can initiate a touch-interception
+    // session (with the client's provided `onTouchInterceptListener`). We delegate out to their
+    // listener only for gestures that can't be handled by scrolling our `actionsContainer`.
+    private val gestureDetector =
+        GestureDetector(
+            context,
+            object : SimpleOnGestureListener() {
+                override fun onScroll(
+                    ev1: MotionEvent?,
+                    ev2: MotionEvent,
+                    distanceX: Float,
+                    distanceY: Float
+                ): Boolean {
+                    actionsContainer.getBoundsOnScreen(tmpRect)
+                    val touchedInActionsContainer =
+                        tmpRect.contains(ev2.rawX.toInt(), ev2.rawY.toInt())
+                    val canHandleInternallyByScrolling =
+                        touchedInActionsContainer
+                        && actionsContainer.canScrollHorizontally(distanceX.toInt())
+                    return !canHandleInternallyByScrolling
+                }
+            }
+        )
+
     init {
-        setOnTouchListener({ _: View, _: MotionEvent ->
+
+        // Delegate to the client-provided `onTouchInterceptListener` if we've already initiated
+        // touch-interception.
+        setOnTouchListener({ _: View, ev: MotionEvent ->
             userInteractionCallback?.invoke()
-            true
+            onTouchInterceptListener?.invoke(ev) ?: false
         })
+
+        gestureDetector.setIsLongpressEnabled(false)
     }
 
     override fun onFinishInflate() {
@@ -60,7 +92,15 @@
         blurredScreenshotPreview = requireViewById(R.id.screenshot_preview_blur)
         screenshotStatic = requireViewById(R.id.screenshot_static)
         actionsContainerBackground = requireViewById(R.id.actions_container_background)
+        actionsContainer = requireViewById(R.id.actions_container)
         dismissButton = requireViewById(R.id.screenshot_dismiss_button)
+
+        // Configure to extend the timeout during ongoing gestures (i.e. scrolls) that are already
+        // being handled by our child views.
+        actionsContainer.setOnTouchListener({ _: View, ev: MotionEvent ->
+            userInteractionCallback?.invoke()
+            false
+        })
     }
 
     fun getTouchRegion(gestureInsets: Insets): Region {
@@ -171,9 +211,16 @@
     override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
         userInteractionCallback?.invoke()
 
-        if (onTouchInterceptListener?.invoke(ev) == true) {
-            return true
+        // Let the client-provided listener see all `DOWN` events so that they'll be able to
+        // interpret the remainder of the gesture, even if interception starts partway-through.
+        // TODO: is this really necessary? And if we don't go on to start interception, should we
+        // follow up with `ACTION_CANCEL`?
+        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            onTouchInterceptListener?.invoke(ev)
         }
-        return super.onInterceptTouchEvent(ev)
+
+        // Only allow the client-provided touch interceptor to take over the gesture if our
+        // top-level `GestureDetector` decides not to scroll the action container.
+        return gestureDetector.onTouchEvent(ev)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index d2c93da..884ccef 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -140,7 +140,7 @@
 
     private fun animateCollapseShadeInternal() {
         sceneInteractor.changeScene(
-            getCollapseDestinationScene(),
+            getCollapseDestinationScene(), // TODO(b/336581871): add sceneState?
             "ShadeController.animateCollapseShade",
             SlightlyFasterShadeCollapse,
         )
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
index c9949cd..55bd8c6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
@@ -44,6 +44,7 @@
                 } else {
                     Scenes.Shade
                 }
+            // TODO(b/336581871): add sceneState?
             sceneInteractor.changeScene(key, "animateCollapseQs")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index d955349..c912616 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -134,7 +134,7 @@
     private static final int MSG_DISMISS_KEYBOARD_SHORTCUTS        = 32 << MSG_SHIFT;
     private static final int MSG_HANDLE_SYSTEM_KEY                 = 33 << MSG_SHIFT;
     private static final int MSG_SHOW_GLOBAL_ACTIONS               = 34 << MSG_SHIFT;
-    private static final int MSG_TOGGLE_PANEL                      = 35 << MSG_SHIFT;
+    private static final int MSG_TOGGLE_NOTIFICATION_PANEL         = 35 << MSG_SHIFT;
     private static final int MSG_SHOW_SHUTDOWN_UI                  = 36 << MSG_SHIFT;
     private static final int MSG_SET_TOP_APP_HIDES_STATUS_BAR      = 37 << MSG_SHIFT;
     private static final int MSG_ROTATION_PROPOSAL                 = 38 << MSG_SHIFT;
@@ -180,6 +180,7 @@
     private static final int MSG_SET_QS_TILES = 79 << MSG_SHIFT;
     private static final int MSG_ENTER_DESKTOP = 80 << MSG_SHIFT;
     private static final int MSG_SET_SPLITSCREEN_FOCUS = 81 << MSG_SHIFT;
+    private static final int MSG_TOGGLE_QUICK_SETTINGS_PANEL = 82 << MSG_SHIFT;
     public static final int FLAG_EXCLUDE_NONE = 0;
     public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
     public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1;
@@ -222,10 +223,33 @@
          */
         default void disable(int displayId, @DisableFlags int state1, @Disable2Flags int state2,
                 boolean animate) { }
+
+        /**
+         * Called to expand Notifications panel with animation.
+         */
         default void animateExpandNotificationsPanel() { }
+        /**
+         * Called to collapse Notifications panel with animation.
+         * @param flags Exclusion flags. See {@link FLAG_EXCLUDE_NONE}.
+         * @param force True to force the operation.
+         */
         default void animateCollapsePanels(int flags, boolean force) { }
-        default void togglePanel() { }
-        default void animateExpandSettingsPanel(String obj) { }
+
+        /**
+         * Called to toggle Notifications panel.
+         */
+        default void toggleNotificationsPanel() { }
+
+        /**
+         * Called to expand Quick Settings panel with animation.
+         * @param subPanel subPanel one wish to expand.
+         */
+        default void animateExpandSettingsPanel(String subPanel) { }
+
+        /**
+         * Called to toggle Quick Settings panel.
+         */
+        default void toggleQuickSettingsPanel() { }
 
         /**
          * Called to notify IME window status changes.
@@ -696,10 +720,10 @@
         }
     }
 
-    public void togglePanel() {
+    public void toggleNotificationsPanel() {
         synchronized (mLock) {
-            mHandler.removeMessages(MSG_TOGGLE_PANEL);
-            mHandler.obtainMessage(MSG_TOGGLE_PANEL, 0, 0).sendToTarget();
+            mHandler.removeMessages(MSG_TOGGLE_NOTIFICATION_PANEL);
+            mHandler.obtainMessage(MSG_TOGGLE_NOTIFICATION_PANEL, 0, 0).sendToTarget();
         }
     }
 
@@ -710,6 +734,13 @@
         }
     }
 
+    public void toggleQuickSettingsPanel() {
+        synchronized (mLock) {
+            mHandler.removeMessages(MSG_TOGGLE_QUICK_SETTINGS_PANEL);
+            mHandler.obtainMessage(MSG_TOGGLE_QUICK_SETTINGS_PANEL, 0, 0).sendToTarget();
+        }
+    }
+
     @Override
     public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
             boolean showImeSwitcher) {
@@ -1494,9 +1525,9 @@
                         mCallbacks.get(i).animateCollapsePanels(msg.arg1, msg.arg2 != 0);
                     }
                     break;
-                case MSG_TOGGLE_PANEL:
+                case MSG_TOGGLE_NOTIFICATION_PANEL:
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).togglePanel();
+                        mCallbacks.get(i).toggleNotificationsPanel();
                     }
                     break;
                 case MSG_EXPAND_SETTINGS:
@@ -1504,6 +1535,11 @@
                         mCallbacks.get(i).animateExpandSettingsPanel((String) msg.obj);
                     }
                     break;
+                case MSG_TOGGLE_QUICK_SETTINGS_PANEL:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).toggleQuickSettingsPanel();
+                    }
+                    break;
                 case MSG_SHOW_IME_BUTTON:
                     args = (SomeArgs) msg.obj;
                     handleShowImeButton(args.argi1 /* displayId */, (IBinder) args.arg1 /* token */,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index d7d3732..5bf2f41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar;
 
+import static com.android.systemui.Flags.mediaControlsUserInitiatedDismiss;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Notification;
@@ -175,14 +177,18 @@
             }
 
             @Override
-            public void onMediaDataRemoved(@NonNull String key) {
+            public void onMediaDataRemoved(@NonNull String key, boolean userInitiated) {
+                if (mediaControlsUserInitiatedDismiss() && !userInitiated) {
+                    // Dismissing the notification will send the app's deleteIntent, so ignore if
+                    // this was an automatic removal
+                    Log.d(TAG, "Not dismissing " + key + " because it was removed by the system");
+                    return;
+                }
                 mNotifPipeline.getAllNotifs()
                         .stream()
                         .filter(entry -> Objects.equals(entry.getKey(), key))
                         .findAny()
                         .ifPresent(entry -> {
-                            // TODO(b/160713608): "removing" this notification won't happen and
-                            //  won't send the 'deleteIntent' if the notification is ongoing.
                             mNotifCollection.dismissNotification(entry,
                                     getDismissedByUserStats(entry));
                         });
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 1a223c1..933eb20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -18,12 +18,10 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import android.os.SystemProperties
 import android.os.UserHandle
 import android.provider.Settings
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.Dumpable
-import com.android.systemui.Flags.notificationMinimalismPrototype
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dump.DumpManager
@@ -41,6 +39,7 @@
 import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.headsUpEvents
 import com.android.systemui.util.asIndenting
@@ -264,7 +263,7 @@
     }
 
     private fun unseenFeatureEnabled(): Flow<Boolean> {
-        if (notificationMinimalismPrototype()) {
+        if (NotificationMinimalismPrototype.V1.isEnabled) {
             return flowOf(true)
         }
         return secureSettings
@@ -342,18 +341,6 @@
             var hasFilteredAnyNotifs = false
 
             /**
-             * the [notificationMinimalismPrototype] will now show seen notifications on the locked
-             * shade by default, but this property read allows that to be quickly disabled for
-             * testing
-             */
-            private val minimalismShowOnLockedShade
-                get() =
-                    SystemProperties.getBoolean(
-                        "persist.notification_minimalism_prototype.show_on_locked_shade",
-                        true
-                    )
-
-            /**
              * Encapsulates a definition of "being on the keyguard". Note that these two definitions
              * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does
              * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing]
@@ -364,7 +351,10 @@
              * allow seen notifications to appear in the locked shade.
              */
             private fun isOnKeyguard(): Boolean =
-                if (notificationMinimalismPrototype() && minimalismShowOnLockedShade) {
+                if (
+                    NotificationMinimalismPrototype.V1.isEnabled &&
+                        NotificationMinimalismPrototype.V1.showOnLockedShade
+                ) {
                     statusBarStateController.state == StatusBarState.KEYGUARD
                 } else {
                     keyguardRepository.isKeyguardShowing()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
new file mode 100644
index 0000000..8889a10
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import android.os.SystemProperties
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the minimalism prototype flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationMinimalismPrototype {
+    object V1 {
+        /** The aconfig flag name */
+        const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+
+        /** A token used for dependency declaration */
+        val token: FlagToken
+            get() = FlagToken(FLAG_NAME, isEnabled)
+
+        /** Is the heads-up cycling animation enabled */
+        @JvmStatic
+        inline val isEnabled
+            get() = Flags.notificationMinimalismPrototype()
+
+        /**
+         * the prototype will now show seen notifications on the locked shade by default, but this
+         * property read allows that to be quickly disabled for testing
+         */
+        val showOnLockedShade: Boolean
+            get() =
+                if (isUnexpectedlyInLegacyMode()) false
+                else
+                    SystemProperties.getBoolean(
+                        "persist.notification_minimalism_prototype.show_on_locked_shade",
+                        true
+                    )
+
+        /** gets the configurable max number of notifications */
+        val maxNotifs: Int
+            get() =
+                if (isUnexpectedlyInLegacyMode()) -1
+                else
+                    SystemProperties.getInt(
+                        "persist.notification_minimalism_prototype.lock_screen_max_notifs",
+                        1
+                    )
+
+        /**
+         * Called to ensure code is only run when the flag is enabled. This protects users from the
+         * unintended behaviors caused by accidentally running new logic, while also crashing on an
+         * eng build to ensure that the refactor author catches issues in testing.
+         */
+        @JvmStatic
+        inline fun isUnexpectedlyInLegacyMode() =
+            RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+        /**
+         * Called to ensure code is only run when the flag is disabled. This will throw an exception
+         * if the flag is enabled to ensure that the refactor author catches issues in testing.
+         */
+        @JvmStatic
+        inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 5bd4c75..a446745 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -17,11 +17,9 @@
 package com.android.systemui.statusbar.notification.stack
 
 import android.content.res.Resources
-import android.os.SystemProperties
 import android.util.Log
 import android.view.View.GONE
 import androidx.annotation.VisibleForTesting
-import com.android.systemui.Flags.notificationMinimalismPrototype
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
@@ -31,6 +29,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import com.android.systemui.util.Compile
 import com.android.systemui.util.children
@@ -381,16 +380,13 @@
     fun updateResources() {
         maxKeyguardNotifications =
             infiniteIfNegative(
-                if (notificationMinimalismPrototype()) {
-                    SystemProperties.getInt(
-                        "persist.notification_minimalism_prototype.lock_screen_max_notifs",
-                        1
-                    )
+                if (NotificationMinimalismPrototype.V1.isEnabled) {
+                    NotificationMinimalismPrototype.V1.maxNotifs
                 } else {
                     resources.getInteger(R.integer.keyguard_max_notification_count)
                 }
             )
-        maxNotificationsExcludesMedia = notificationMinimalismPrototype()
+        maxNotificationsExcludesMedia = NotificationMinimalismPrototype.V1.isEnabled
 
         dividerHeight =
             max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index e93c0f6..7dac77e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -59,6 +59,7 @@
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeHeaderController;
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -82,6 +83,7 @@
     private final com.android.systemui.shade.ShadeController mShadeController;
     private final CommandQueue mCommandQueue;
     private final PanelExpansionInteractor mPanelExpansionInteractor;
+    private final Lazy<ShadeInteractor> mShadeInteractorLazy;
     private final ShadeHeaderController mShadeHeaderController;
     private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
     private final MetricsLogger mMetricsLogger;
@@ -121,6 +123,7 @@
             ShadeController shadeController,
             CommandQueue commandQueue,
             PanelExpansionInteractor panelExpansionInteractor,
+            Lazy<ShadeInteractor> shadeInteractorLazy,
             ShadeHeaderController shadeHeaderController,
             RemoteInputQuickSettingsDisabler remoteInputQuickSettingsDisabler,
             MetricsLogger metricsLogger,
@@ -148,6 +151,7 @@
         mShadeController = shadeController;
         mCommandQueue = commandQueue;
         mPanelExpansionInteractor = panelExpansionInteractor;
+        mShadeInteractorLazy = shadeInteractorLazy;
         mShadeHeaderController = shadeHeaderController;
         mRemoteInputQuickSettingsDisabler = remoteInputQuickSettingsDisabler;
         mMetricsLogger = metricsLogger;
@@ -487,14 +491,23 @@
     }
 
     @Override
-    public void togglePanel() {
-        if (mPanelExpansionInteractor.isPanelExpanded()) {
+    public void toggleNotificationsPanel() {
+        if (mShadeInteractorLazy.get().isAnyExpanded().getValue()) {
             mShadeController.animateCollapseShade();
         } else {
             mShadeController.animateExpandShade();
         }
     }
 
+    @Override
+    public void toggleQuickSettingsPanel() {
+        if (mShadeInteractorLazy.get().isQsExpanded().getValue()) {
+            mShadeController.animateCollapseShade();
+        } else {
+            mShadeController.animateExpandQs();
+        }
+    }
+
     private boolean isGoingToSleep() {
         return mWakefulnessLifecycle.getWakefulness()
                 == WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index f0dab3b..b715646 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -696,6 +696,7 @@
         Trace.beginSection("StatusBarKeyguardViewManager#show");
         mNotificationShadeWindowController.setKeyguardShowing(true);
         if (SceneContainerFlag.isEnabled()) {
+            // TODO(b/336581871): add sceneState?
             mSceneInteractorLazy.get().changeScene(
                     Scenes.Lockscreen, "StatusBarKeyguardViewManager.show");
         }
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index b86a7c9..e073f7c 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -98,7 +98,7 @@
         CoreStartable,
         CommandQueue.Callbacks {
     private static final String TAG = WMShell.class.getName();
-    private static final int INVALID_SYSUI_STATE_MASK =
+    private static final long INVALID_SYSUI_STATE_MASK =
             SYSUI_STATE_DIALOG_SHOWING
                     | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
                     | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
index 6dc5b72..bbdd805 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doAnswer;
@@ -104,7 +105,7 @@
         }).when(mAccessibilityManager).setMagnificationConnection(
                 any(IMagnificationConnection.class));
 
-        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+        when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
 
         doAnswer(invocation -> {
             mMagnification.mMagnificationSettingsControllerCallback
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
index bf6ca06..e371b39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
@@ -46,12 +46,12 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Mock
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 private const val ON: Int = 1
 private const val OFF: Int = 0
@@ -90,7 +90,7 @@
         secureSettings = FakeSettings()
         systemClock = FakeSystemClock()
         backgroundDelayableExecutor = FakeExecutor(systemClock)
-        whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState)
+        whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
 
         fontScalingDialogDelegate =
             spy(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index ebb6b48..8895a5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -118,7 +119,7 @@
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
         when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices);
         when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
-        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+        when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
         when(mCachedDevice.getAddress()).thenReturn(DEVICE_ADDRESS);
         when(mHearingDeviceItem.getCachedBluetoothDevice()).thenReturn(mCachedDevice);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 8e4c155..fd37cad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -8,6 +8,7 @@
 import android.graphics.Point
 import android.graphics.Rect
 import android.os.Looper
+import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.IRemoteAnimationFinishedCallback
@@ -17,15 +18,20 @@
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import android.widget.LinearLayout
+import android.window.RemoteTransition
+import android.window.TransitionFilter
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.shared.Flags
 import com.android.systemui.util.mockito.any
+import com.android.wm.shell.shared.ShellTransitions
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertNotNull
 import junit.framework.Assert.assertNull
 import junit.framework.Assert.assertTrue
 import junit.framework.AssertionFailedError
 import kotlin.concurrent.thread
+import kotlin.test.assertEquals
 import org.junit.After
 import org.junit.Assert.assertThrows
 import org.junit.Before
@@ -48,6 +54,7 @@
     private val transitionContainer = LinearLayout(mContext)
     private val mainExecutor = context.mainExecutor
     private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor)
+    private val testShellTransitions = FakeShellTransitions()
     @Mock lateinit var callback: ActivityTransitionAnimator.Callback
     @Mock lateinit var listener: ActivityTransitionAnimator.Listener
     @Spy private val controller = TestTransitionAnimatorController(transitionContainer)
@@ -55,12 +62,16 @@
 
     private lateinit var activityTransitionAnimator: ActivityTransitionAnimator
     @get:Rule val rule = MockitoJUnit.rule()
+    @get:Rule val setFlagsRule = SetFlagsRule()
 
     @Before
     fun setup() {
         activityTransitionAnimator =
             ActivityTransitionAnimator(
                 mainExecutor,
+                ActivityTransitionAnimator.TransitionRegister.fromShellTransitions(
+                    testShellTransitions
+                ),
                 testTransitionAnimator,
                 testTransitionAnimator,
                 disableWmTimeout = true,
@@ -164,6 +175,34 @@
     }
 
     @Test
+    fun registersReturnIffCookieIsPresent() {
+        setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY)
+        `when`(callback.isOnKeyguard()).thenReturn(false)
+
+        startIntentWithAnimation(activityTransitionAnimator, controller) { _ ->
+            ActivityManager.START_DELIVERED_TO_TOP
+        }
+
+        waitForIdleSync()
+        assertTrue(testShellTransitions.remotes.isEmpty())
+        assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
+
+        val controller =
+            object : DelegateTransitionAnimatorController(controller) {
+                override val transitionCookie
+                    get() = ActivityTransitionAnimator.TransitionCookie("testCookie")
+            }
+
+        startIntentWithAnimation(activityTransitionAnimator, controller) { _ ->
+            ActivityManager.START_DELIVERED_TO_TOP
+        }
+
+        waitForIdleSync()
+        assertEquals(1, testShellTransitions.remotes.size)
+        assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
+    }
+
+    @Test
     fun doesNotStartIfAnimationIsCancelled() {
         val runner = activityTransitionAnimator.createRunner(controller)
         runner.onAnimationCancelled()
@@ -243,6 +282,35 @@
 }
 
 /**
+ * A fake implementation of [ShellTransitions] which saves filter-transition pairs locally and
+ * allows inspection.
+ */
+private class FakeShellTransitions : ShellTransitions {
+    val remotes = mutableMapOf<TransitionFilter, RemoteTransition>()
+    val remotesForTakeover = mutableMapOf<TransitionFilter, RemoteTransition>()
+
+    override fun registerRemote(filter: TransitionFilter, remoteTransition: RemoteTransition) {
+        remotes[filter] = remoteTransition
+    }
+
+    override fun registerRemoteForTakeover(
+        filter: TransitionFilter,
+        remoteTransition: RemoteTransition
+    ) {
+        remotesForTakeover[filter] = remoteTransition
+    }
+
+    override fun unregisterRemote(remoteTransition: RemoteTransition) {
+        while (remotes.containsValue(remoteTransition)) {
+            remotes.values.remove(remoteTransition)
+        }
+        while (remotesForTakeover.containsValue(remoteTransition)) {
+            remotesForTakeover.values.remove(remoteTransition)
+        }
+    }
+}
+
+/**
  * A simple implementation of [ActivityTransitionAnimator.Controller] which throws if it is called
  * outside of the main thread.
  */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt
index b31fe21..42fcd54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt
@@ -16,12 +16,16 @@
 
 package com.android.systemui.animation
 
+import android.os.HandlerThread
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import android.view.View
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
+import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.view.LaunchableFrameLayout
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -30,6 +34,13 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() {
+    companion object {
+        private const val LAUNCH_CUJ = 0
+        private const val RETURN_CUJ = 1
+    }
+
+    private val interactionJankMonitor = FakeInteractionJankMonitor()
+
     @Test
     fun animatingOrphanViewDoesNotCrash() {
         val state = TransitionAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
@@ -47,4 +58,63 @@
             GhostedViewTransitionAnimatorController(FrameLayout(mContext))
         }
     }
+
+    @Test
+    fun cujsAreLoggedCorrectly() {
+        val parent = FrameLayout(mContext)
+
+        val launchView = LaunchableFrameLayout(mContext)
+        parent.addView((launchView))
+        val launchController =
+            GhostedViewTransitionAnimatorController(
+                    launchView,
+                launchCujType = LAUNCH_CUJ,
+                returnCujType = RETURN_CUJ,
+                interactionJankMonitor = interactionJankMonitor
+            )
+        launchController.onTransitionAnimationStart(isExpandingFullyAbove = true)
+        assertThat(interactionJankMonitor.ongoing).containsExactly(LAUNCH_CUJ)
+        launchController.onTransitionAnimationEnd(isExpandingFullyAbove = true)
+        assertThat(interactionJankMonitor.ongoing).isEmpty()
+        assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ)
+
+        val returnView = LaunchableFrameLayout(mContext)
+        parent.addView((returnView))
+        val returnController =
+            object : GhostedViewTransitionAnimatorController(
+                returnView,
+                launchCujType = LAUNCH_CUJ,
+                returnCujType = RETURN_CUJ,
+                interactionJankMonitor = interactionJankMonitor
+            ) {
+                override val isLaunching = false
+            }
+        returnController.onTransitionAnimationStart(isExpandingFullyAbove = true)
+        assertThat(interactionJankMonitor.ongoing).containsExactly(RETURN_CUJ)
+        returnController.onTransitionAnimationEnd(isExpandingFullyAbove = true)
+        assertThat(interactionJankMonitor.ongoing).isEmpty()
+        assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ, RETURN_CUJ)
+    }
+
+    /**
+     * A fake implementation of [InteractionJankMonitor] which stores ongoing and finished CUJs and
+     * allows inspection.
+     */
+    private class FakeInteractionJankMonitor : InteractionJankMonitor(
+        HandlerThread("testThread")
+    ) {
+        val ongoing: MutableSet<Int> = mutableSetOf()
+        val finished: MutableSet<Int> = mutableSetOf()
+
+        override fun begin(v: View?, cujType: Int): Boolean {
+            ongoing.add(cujType)
+            return true
+        }
+
+        override fun end(cujType: Int): Boolean {
+            ongoing.remove(cujType)
+            finished.add(cujType)
+            return true
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
index a569cee..49f2043 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
@@ -21,7 +21,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
@@ -92,7 +92,7 @@
         when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
         when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(null);
 
-        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+        when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
         when(mSystemUIDialogFactory.create(any(), any())).thenReturn(mDialog);
 
         mBroadcastDialogDelegate = new BroadcastDialogDelegate(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
index 62c98b0..7215619 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
@@ -50,7 +50,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
@@ -104,7 +104,7 @@
         dispatcher = UnconfinedTestDispatcher(scheduler)
         testScope = TestScope(dispatcher)
 
-        whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState)
+        whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
 
         mBluetoothTileDialogDelegate =
             BluetoothTileDialogDelegate(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
index bc6c459..5361cef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
@@ -35,9 +35,13 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerFake;
+import com.android.systemui.flags.DisableSceneContainer;
+import com.android.systemui.flags.EnableSceneContainer;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -50,6 +54,7 @@
 import com.android.systemui.util.sensors.ThresholdSensor;
 import com.android.systemui.util.time.FakeSystemClock;
 
+import kotlinx.coroutines.flow.MutableStateFlow;
 import kotlinx.coroutines.flow.StateFlowKt;
 
 import org.junit.Before;
@@ -89,6 +94,14 @@
     private SelectedUserInteractor mSelectedUserInteractor;
     @Mock
     private CommunalInteractor mCommunalInteractor;
+    @Mock
+    private DeviceEntryInteractor mDeviceEntryInteractor;
+    private final MutableStateFlow<Boolean> mIsDeviceEntered =
+            StateFlowKt.MutableStateFlow(false);
+    @Mock
+    private SceneContainerOcclusionInteractor mSceneContainerOcclusionInteractor;
+    private final MutableStateFlow<Boolean> mIsInvisibleDueToOcclusion =
+            StateFlowKt.MutableStateFlow(false);
     private final DockManagerFake mDockManager = new DockManagerFake();
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeExecutor mFakeExecutor = new FakeExecutor(mFakeSystemClock);
@@ -99,15 +112,21 @@
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
         when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         when(mShadeInteractor.isQsExpanded()).thenReturn(StateFlowKt.MutableStateFlow(false));
 
+        when(mDeviceEntryInteractor.isDeviceEntered()).thenReturn(mIsDeviceEntered);
+        when(mSceneContainerOcclusionInteractor.getInvisibleDueToOcclusion()).thenReturn(
+                mIsInvisibleDueToOcclusion);
+
         mFalsingCollector = new FalsingCollectorImpl(mFalsingDataProvider, mFalsingManager,
                 mKeyguardUpdateMonitor, mHistoryTracker, mProximitySensor,
                 mStatusBarStateController, mKeyguardStateController,
                 () -> mShadeInteractor, mBatteryController,
                 mDockManager, mFakeExecutor,
                 mJavaAdapter, mFakeSystemClock, () -> mSelectedUserInteractor,
-                () -> mCommunalInteractor
+                () -> mCommunalInteractor, () -> mDeviceEntryInteractor,
+                () -> mSceneContainerOcclusionInteractor
         );
         mFalsingCollector.init();
     }
@@ -189,7 +208,8 @@
     }
 
     @Test
-    public void testRegisterSensor_OccludingActivity() {
+    @DisableSceneContainer
+    public void testRegisterSensor_OccludingActivity_sceneContainerDisabled() {
         when(mKeyguardStateController.isOccluded()).thenReturn(true);
 
         ArgumentCaptor<StatusBarStateController.StateListener> stateListenerArgumentCaptor =
@@ -203,6 +223,21 @@
     }
 
     @Test
+    @EnableSceneContainer
+    public void testRegisterSensor_OccludingActivity_sceneContainerEnabled() {
+        mIsInvisibleDueToOcclusion.setValue(true);
+
+        ArgumentCaptor<StatusBarStateController.StateListener> stateListenerArgumentCaptor =
+                ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+        verify(mStatusBarStateController).addCallback(stateListenerArgumentCaptor.capture());
+
+        mFalsingCollector.onScreenTurningOn();
+        reset(mProximitySensor);
+        stateListenerArgumentCaptor.getValue().onStateChanged(StatusBarState.SHADE);
+        verify(mProximitySensor).register(any(ThresholdSensor.Listener.class));
+    }
+
+    @Test
     public void testPassThroughEnterKeyEvent() {
         KeyEvent enterDown = KeyEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER,
                 0, 0, 0, 0, 0, 0, 0, "");
@@ -280,7 +315,8 @@
     }
 
     @Test
-    public void testAvoidUnlocked() {
+    @DisableSceneContainer
+    public void testAvoidUnlocked_sceneContainerDisabled() {
         MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
         MotionEvent up = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0);
 
@@ -296,6 +332,23 @@
     }
 
     @Test
+    @EnableSceneContainer
+    public void testAvoidUnlocked_sceneContainerEnabled() {
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
+        MotionEvent up = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0);
+
+        mIsDeviceEntered.setValue(true);
+
+        // Nothing passed initially
+        mFalsingCollector.onTouchEvent(down);
+        verify(mFalsingDataProvider, never()).onMotionEvent(any(MotionEvent.class));
+
+        // Up event would normally flush the up event, but doesn't.
+        mFalsingCollector.onTouchEvent(up);
+        verify(mFalsingDataProvider, never()).onMotionEvent(any(MotionEvent.class));
+    }
+
+    @Test
     public void testGestureWhenDozing() {
         // We check the FalsingManager for taps during the transition to AoD (dozing=true,
         // pulsing=false), so the FalsingCollector needs to continue to analyze events that occur
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
new file mode 100644
index 0000000..d0d9891
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
@@ -0,0 +1,1320 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor.scenetransition
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.realKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.data.repository.sceneContainerRepository
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenSceneTransitionInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply { keyguardTransitionRepository = realKeyguardTransitionRepository }
+
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.lockscreenSceneTransitionInteractor
+
+    private val progress = MutableStateFlow(0f)
+
+    private val sceneTransitions =
+        MutableStateFlow<ObservableTransitionState>(
+            ObservableTransitionState.Idle(Scenes.Lockscreen)
+        )
+
+    private val lsToGone =
+        ObservableTransitionState.Transition(
+            Scenes.Lockscreen,
+            Scenes.Gone,
+            flowOf(Scenes.Lockscreen),
+            progress,
+            false,
+            flowOf(false)
+        )
+
+    private val goneToLs =
+        ObservableTransitionState.Transition(
+            Scenes.Gone,
+            Scenes.Lockscreen,
+            flowOf(Scenes.Lockscreen),
+            progress,
+            false,
+            flowOf(false)
+        )
+
+    @Before
+    fun setUp() {
+        underTest.start()
+        kosmos.sceneContainerRepository.setTransitionState(sceneTransitions)
+        testScope.launch {
+            kosmos.realKeyguardTransitionRepository.emitInitialStepsFromOff(
+                KeyguardState.LOCKSCREEN
+            )
+        }
+    }
+
+    /** STL: Ls -> Gone, then settle with Idle(Gone). This is the default case. */
+    @Test
+    fun transition_from_ls_scene_end_in_gone() =
+        testScope.runTest {
+            sceneTransitions.value = lsToGone
+
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            progress.value = 0.4f
+            assertTransition(
+                step = currentStep!!,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
+    /**
+     * STL: Ls -> Gone, then settle with Idle(Ls). KTF in this scenario needs to invert the
+     * transition LS -> UNDEFINED to UNDEFINED -> LS as there is no mechanism in KTF to
+     * finish/settle to progress 0.0f.
+     */
+    @Test
+    fun transition_from_ls_scene_end_in_ls() =
+        testScope.runTest {
+            sceneTransitions.value = lsToGone
+
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            progress.value = 0.4f
+            assertTransition(
+                step = currentStep!!,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.CANCELED,
+                progress = 0.4f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.STARTED,
+                progress = 0.6f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
+    /**
+     * STL: Ls -> Gone, then settle with Idle(Ls). KTF starts in AOD and needs to inverse correctly
+     * back to AOD.
+     */
+    @Test
+    fun transition_from_ls_scene_on_aod_end_in_ls() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+
+            kosmos.realKeyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = this.javaClass.simpleName,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+            sceneTransitions.value = lsToGone
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            progress.value = 0.4f
+            assertTransition(
+                step = currentStep!!,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.CANCELED,
+                progress = 0.4f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.AOD,
+                state = TransitionState.STARTED,
+                progress = 0.6f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.AOD,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then settle with Idle(Ls). This is the default case in the reverse
+     * direction.
+     */
+    @Test
+    fun transition_to_ls_scene_end_in_ls() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = goneToLs
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            progress.value = 0.4f
+            assertTransition(
+                step = currentStep!!,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
+    /** STL: Gone -> Ls (AOD), will transition to AOD once */
+    @Test
+    fun transition_to_ls_scene_with_changed_next_scene_is_respected_just_once() =
+        testScope.runTest {
+            underTest.onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD)
+            sceneTransitions.value = goneToLs
+
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.AOD,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Shade,
+                    Scenes.Lockscreen,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then settle with Idle(Gone). KTF in this scenario needs to invert the
+     * transition UNDEFINED -> LS to LS -> UNDEFINED as there is no mechanism in KTF to
+     * finish/settle to progress 0.0f.
+     */
+    @Test
+    fun transition_to_ls_scene_end_in_from_scene() =
+        testScope.runTest {
+            sceneTransitions.value = goneToLs
+
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            progress.value = 0.4f
+            assertTransition(
+                step = currentStep!!,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
+            val stepM3 = allSteps[allSteps.size - 3]
+            val stepM2 = allSteps[allSteps.size - 2]
+
+            assertTransition(
+                step = stepM3,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.CANCELED,
+                progress = 0.4f,
+            )
+
+            assertTransition(
+                step = stepM2,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.STARTED,
+                progress = 0.6f,
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupted by Shade -> Ls. KTF in this scenario needs to invert the
+     * transition UNDEFINED -> LS to LS -> UNDEFINED as there is no mechanism in KTF to
+     * finish/settle to progress 0.0f. Then restart a different transition UNDEFINED -> Ls.
+     */
+    @Test
+    fun transition_to_ls_scene_end_in_to_ls_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = goneToLs
+            progress.value = 0.4f
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Shade,
+                    Scenes.Lockscreen,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 5],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.CANCELED,
+                progress = 0.4f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 4],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.STARTED,
+                progress = 0.6f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.2f
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0.2f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupted by Ls -> Shade. This is like continuing the transition from
+     * Ls before the transition before has properly settled. This can happen in STL e.g. with an
+     * accelerated swipe (quick successive fling gestures).
+     */
+    @Test
+    fun transition_to_ls_scene_end_in_from_ls_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = goneToLs
+            progress.value = 0.4f
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Lockscreen,
+                    Scenes.Shade,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.CANCELED,
+                progress = 0.4f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.STARTED,
+                progress = 0.0f,
+            )
+
+            progress.value = 0.2f
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0.2f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupted by Gone -> Shade. This is going back to Gone but starting a
+     * transition from Gone before settling in Gone. KTF needs to make sure the transition is
+     * properly inversed and settled in UNDEFINED.
+     */
+    @Test
+    fun transition_to_ls_scene_end_in_other_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = goneToLs
+            progress.value = 0.4f
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Gone,
+                    Scenes.Shade,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.CANCELED,
+                progress = 0.4f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.STARTED,
+                progress = 0.6f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then stl still finishes in Ls. After a KTF
+     * transition is started (UNDEFINED -> LOCKSCREEN) KTF immediately considers the active scene to
+     * be LOCKSCREEN. This means that all listeners for LOCKSCREEN are active and may start a new
+     * transition LOCKSCREEN -> *. Here we test LS -> AOD.
+     *
+     * KTF is allowed to already start and play the other transition, while the STL transition may
+     * finish later (gesture completes much later). When we eventually settle the STL transition in
+     * Ls we do not want to force KTF back to its original destination (LOCKSCREEN). Instead, for
+     * this scenario the settle can be ignored.
+     */
+    @Test
+    fun transition_to_ls_scene_interrupted_by_ktf_transition_then_finish_in_lockscreen() =
+        testScope.runTest {
+            sceneTransitions.value = goneToLs
+
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            progress.value = 0.4f
+            assertTransition(
+                step = currentStep!!,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            kosmos.realKeyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = this.javaClass.simpleName,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            // Scene progress should not affect KTF transition anymore
+            progress.value = 0.7f
+            assertTransition(currentStep!!, progress = 0f)
+
+            // Scene transition still finishes but should not impact KTF transition
+            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then stl finishes in Gone.
+     *
+     * Refers to: `transition_to_ls_scene_interrupted_by_ktf_transition_then_finish_in_lockscreen`
+     *
+     * This is similar to the previous scenario but the gesture may have gone back to its origin. In
+     * this case we can not ignore the settlement, because whatever KTF has done in the meantime it
+     * needs to immediately finish in UNDEFINED (there is a jump cut).
+     */
+    @Test
+    fun transition_to_ls_scene_interrupted_by_ktf_transition_then_finish_in_gone() =
+        testScope.runTest {
+            sceneTransitions.value = goneToLs
+
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            progress.value = 0.4f
+            assertTransition(
+                step = currentStep!!,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            kosmos.realKeyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = this.javaClass.simpleName,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.7f
+            assertThat(currentStep?.value).isEqualTo(0f)
+
+            sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then STL Gone -> Shade
+     *
+     * Refers to: `transition_to_ls_scene_interrupted_by_ktf_transition_then_finish_in_lockscreen`
+     *
+     * This is similar to the previous scenario but the gesture may have been interrupted by any
+     * other transition. KTF needs to immediately finish in UNDEFINED (there is a jump cut).
+     */
+    @Test
+    fun transition_to_ls_interrupted_by_ktf_transition_then_interrupted_by_other_transition() =
+        testScope.runTest {
+            sceneTransitions.value = goneToLs
+
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            progress.value = 0.4f
+            assertTransition(
+                step = currentStep!!,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            kosmos.realKeyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = this.javaClass.simpleName,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.7f
+            assertTransition(currentStep!!, progress = 0f)
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Gone,
+                    Scenes.Shade,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then STL Ls -> Shade
+     *
+     * In this scenario it is important that the last STL transition Ls -> Shade triggers a cancel
+     * of the * -> AOD transition but then also properly starts a transition AOD (not LOCKSCREEN) ->
+     * UNDEFINED transition.
+     */
+    @Test
+    fun transition_to_ls_interrupted_by_ktf_transition_then_interrupted_by_from_ls_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = goneToLs
+            progress.value = 0.4f
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            kosmos.realKeyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = this.javaClass.simpleName,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.7f
+            assertTransition(currentStep!!, progress = 0f)
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Lockscreen,
+                    Scenes.Shade,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+            allSteps[allSteps.size - 3]
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.CANCELED,
+                progress = 0f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.2f
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0.2f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then STL Shade -> Ls
+     *
+     * In this scenario it is important KTF is brought back into a FINISHED UNDEFINED state
+     * considering the state is already on AOD from where a new UNDEFINED -> LOCKSCREEN transition
+     * can be started.
+     */
+    @Test
+    fun transition_to_ls_interrupted_by_ktf_transition_then_interrupted_by_to_ls_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = goneToLs
+            progress.value = 0.4f
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            kosmos.realKeyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = this.javaClass.simpleName,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.7f
+            assertTransition(currentStep!!, progress = 0f)
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Shade,
+                    Scenes.Lockscreen,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 5],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.CANCELED,
+                progress = 0f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 4],
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.STARTED,
+                progress = 1f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0.7f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupt multiple canceled KTF transitions, then STL Ls -> Shade
+     *
+     * Similar to
+     * `transition_to_ls_scene_interrupted_by_ktf_transition_then_interrupted_by_from_ls_transition`
+     * but here KTF is canceled multiple times such that in the end OCCLUDED -> UNDEFINED is
+     * properly started. (not from AOD or LOCKSCREEN)
+     *
+     * Note: there is no test which tests multiple cancels from the STL side, this is because all
+     * STL transitions trigger a response from LockscreenSceneTransitionInteractor which forces KTF
+     * into a specific state, so testing each pair is enough. Meanwhile KTF can move around without
+     * any reaction from LockscreenSceneTransitionInteractor.
+     */
+    @Test
+    fun transition_to_ls_interrupted_by_ktf_cancel_sequence_interrupted_by_from_ls_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = lsToGone
+            progress.value = 0.4f
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            kosmos.realKeyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = this.javaClass.simpleName,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            kosmos.realKeyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = this.javaClass.simpleName,
+                    from = KeyguardState.AOD,
+                    to = KeyguardState.DOZING,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.AOD,
+                to = KeyguardState.DOZING,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            kosmos.realKeyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = this.javaClass.simpleName,
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.OCCLUDED,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.DOZING,
+                to = KeyguardState.OCCLUDED,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.7f
+            assertTransition(currentStep!!, progress = 0f)
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Lockscreen,
+                    Scenes.Shade,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.DOZING,
+                to = KeyguardState.OCCLUDED,
+                state = TransitionState.CANCELED,
+                progress = 0f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.OCCLUDED,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.2f
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.OCCLUDED,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0.2f,
+            )
+        }
+
+    /**
+     * STL: Gone -> Ls, then interrupted by KTF LS -> AOD which is FINISHED before STL Ls -> Shade
+     *
+     * Similar to
+     * `transition_to_ls_scene_interrupted_by_ktf_transition_then_interrupted_by_from_ls_transition`
+     * but here KTF is finishing the transition and only then gets interrupted. Should correctly
+     * start AOD -> UNDEFINED.
+     */
+    @Test
+    fun transition_to_ls_scene_interrupted_and_finished_by_ktf_interrupted_by_from_ls_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = lsToGone
+            progress.value = 0.4f
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            val ktfUuid =
+                kosmos.realKeyguardTransitionRepository.startTransition(
+                    TransitionInfo(
+                        ownerName = this.javaClass.simpleName,
+                        from = KeyguardState.LOCKSCREEN,
+                        to = KeyguardState.AOD,
+                        animator = null,
+                        modeOnCanceled = TransitionModeOnCanceled.RESET
+                    )
+                )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            kosmos.realKeyguardTransitionRepository.updateTransition(
+                ktfUuid!!,
+                1f,
+                TransitionState.FINISHED
+            )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Lockscreen,
+                    Scenes.Shade,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.2f
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.AOD,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0.2f,
+            )
+        }
+
+    /**
+     * STL: Ls -> Gone, then interrupted by Ls -> Bouncer. This happens when the next transition is
+     * immediately started from Gone without settling in Idle. This specifically happens when
+     * dragging down on Ls and then changing direction. The transition will switch from -> Shade to
+     * -> Bouncer without settling or signaling any cancellation as STL considers this to be the
+     * same gesture.
+     *
+     * In STL there is no guarantee that transitions settle in Idle before continuing.
+     */
+    @Ignore("Suffers from a race condition that will be fixed in followup CL")
+    @Test
+    fun transition_from_ls_scene_interrupted_by_other_from_ls_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = lsToGone
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0f,
+            )
+
+            progress.value = 0.4f
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Lockscreen,
+                    Scenes.Bouncer,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 5],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.CANCELED,
+                progress = 0.4f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 4],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.STARTED,
+                progress = 0.6f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+        }
+
+    /**
+     * STL: Ls -> Gone, then interrupted by Gone -> Ls. This happens when the next transition is
+     * immediately started from Gone without settling in Idle. In STL there is no guarantee that
+     * transitions settle in Idle before continuing.
+     */
+    @Test
+    fun transition_from_ls_scene_interrupted_by_to_ls_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = lsToGone
+            progress.value = 0.4f
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Gone,
+                    Scenes.Lockscreen,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 3],
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+
+            assertTransition(
+                step = allSteps[allSteps.size - 2],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.STARTED,
+                progress = 0f,
+            )
+
+            progress.value = 0.2f
+            assertTransition(
+                step = allSteps[allSteps.size - 1],
+                from = KeyguardState.UNDEFINED,
+                to = KeyguardState.LOCKSCREEN,
+                state = TransitionState.RUNNING,
+                progress = 0.2f,
+            )
+        }
+
+    /**
+     * STL: Ls -> Gone, then interrupted by Gone -> Bouncer. This happens when the next transition
+     * is immediately started from Gone without settling in Idle. In STL there is no guarantee that
+     * transitions settle in Idle before continuing.
+     */
+    @Test
+    fun transition_from_ls_scene_interrupted_by_other_stl_transition() =
+        testScope.runTest {
+            val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions)
+            sceneTransitions.value = lsToGone
+            progress.value = 0.4f
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.RUNNING,
+                progress = 0.4f,
+            )
+
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    Scenes.Gone,
+                    Scenes.Bouncer,
+                    flowOf(Scenes.Lockscreen),
+                    progress,
+                    false,
+                    flowOf(false)
+                )
+
+            assertTransition(
+                step = currentStep!!,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.UNDEFINED,
+                state = TransitionState.FINISHED,
+                progress = 1f,
+            )
+        }
+
+    private fun assertTransition(
+        step: TransitionStep,
+        from: KeyguardState? = null,
+        to: KeyguardState? = null,
+        state: TransitionState? = null,
+        progress: Float? = null
+    ) {
+        if (from != null) assertThat(step.from).isEqualTo(from)
+        if (to != null) assertThat(step.to).isEqualTo(to)
+        if (state != null) assertThat(step.transitionState).isEqualTo(state)
+        if (progress != null) assertThat(step.value).isEqualTo(progress)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
index e56a253..265ade3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
@@ -172,20 +172,20 @@
     fun testOnRemovedForCurrent_callsListener() {
         // GIVEN a media was removed for main user
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
-        mediaDataFilter.onMediaDataRemoved(KEY)
+        mediaDataFilter.onMediaDataRemoved(KEY, false)
 
         // THEN we should tell the listener
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
     fun testOnRemovedForGuest_doesNotCallListener() {
         // GIVEN a media was removed for guest user
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
-        mediaDataFilter.onMediaDataRemoved(KEY)
+        mediaDataFilter.onMediaDataRemoved(KEY, false)
 
         // THEN we should NOT tell the listener
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never()).onMediaDataRemoved(eq(KEY), anyBoolean())
     }
 
     @Test
@@ -197,7 +197,7 @@
         setUser(USER_GUEST)
 
         // THEN we should remove the main user's media
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
@@ -230,7 +230,7 @@
         setPrivateProfileUnavailable()
 
         // THEN we should add the private profile media
-        verify(listener).onMediaDataRemoved(eq(KEY_ALT))
+        verify(listener).onMediaDataRemoved(eq(KEY_ALT), eq(false))
     }
 
     @Test
@@ -360,7 +360,7 @@
     @Test
     fun testOnNotificationRemoved_doesntHaveMedia() {
         mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
-        mediaDataFilter.onMediaDataRemoved(KEY)
+        mediaDataFilter.onMediaDataRemoved(KEY, false)
         assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
         assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index 5a2d22d..99bf2db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -346,7 +346,7 @@
         // THEN it is removed and listeners are informed
         foregroundExecutor.advanceClockToLast()
         foregroundExecutor.runAllReady()
-        verify(listener).onMediaDataRemoved(PACKAGE_NAME)
+        verify(listener).onMediaDataRemoved(PACKAGE_NAME, false)
     }
 
     @Test
@@ -532,7 +532,7 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         mediaDataManager.onNotificationRemoved(KEY)
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
     }
 
@@ -777,7 +777,7 @@
                 eq(false)
             )
         assertThat(mediaDataCaptor.value.resumption).isTrue()
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never()).onMediaDataRemoved(eq(KEY), eq(false))
         // WHEN the second is removed
         mediaDataManager.onNotificationRemoved(KEY_2)
         // THEN the data is for resumption and the second key is removed
@@ -791,7 +791,7 @@
                 eq(false)
             )
         assertThat(mediaDataCaptor.value.resumption).isTrue()
-        verify(listener).onMediaDataRemoved(eq(KEY_2))
+        verify(listener).onMediaDataRemoved(eq(KEY_2), eq(false))
     }
 
     @Test
@@ -816,7 +816,7 @@
         mediaDataManager.onNotificationRemoved(KEY)
 
         // THEN the media data is removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
@@ -866,7 +866,7 @@
         mediaDataManager.onNotificationRemoved(KEY)
 
         // THEN the media data is removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
@@ -905,7 +905,7 @@
         assertThat(mediaDataCaptor.value.isPlaying).isFalse()
 
         // And the oldest resume control was removed
-        verify(listener).onMediaDataRemoved(eq("0:$PACKAGE_NAME"))
+        verify(listener).onMediaDataRemoved(eq("0:$PACKAGE_NAME"), eq(false))
     }
 
     fun testOnNotificationRemoved_lockDownMode() {
@@ -915,7 +915,7 @@
         val data = mediaDataCaptor.value
         mediaDataManager.onNotificationRemoved(KEY)
 
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never()).onMediaDataRemoved(eq(KEY), anyBoolean())
         verify(logger, never())
             .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
@@ -1148,7 +1148,7 @@
         mediaDataManager.setMediaResumptionEnabled(false)
 
         // THEN the resume controls are dismissed
-        verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME))
+        verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
     }
 
@@ -1156,19 +1156,19 @@
     fun testDismissMedia_listenerCalled() {
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
-        val removed = mediaDataManager.dismissMediaData(KEY, 0L)
+        val removed = mediaDataManager.dismissMediaData(KEY, 0L, true)
         assertThat(removed).isTrue()
 
         foregroundExecutor.advanceClockToLast()
         foregroundExecutor.runAllReady()
 
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(true))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
     }
 
     @Test
     fun testDismissMedia_keyDoesNotExist_returnsFalse() {
-        val removed = mediaDataManager.dismissMediaData(KEY, 0L)
+        val removed = mediaDataManager.dismissMediaData(KEY, 0L, true)
         assertThat(removed).isFalse()
     }
 
@@ -2077,7 +2077,7 @@
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It remains as a regular player
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never()).onMediaDataRemoved(eq(KEY), anyBoolean())
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2093,7 +2093,7 @@
         mediaDataManager.onNotificationRemoved(KEY)
 
         // It is fully removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
@@ -2146,7 +2146,7 @@
         mediaDataManager.onNotificationRemoved(KEY)
 
         // It remains as a regular player
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never()).onMediaDataRemoved(eq(KEY), anyBoolean())
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2199,7 +2199,7 @@
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is fully removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
@@ -2253,7 +2253,7 @@
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is fully removed.
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(listener, never())
             .onMediaDataLoaded(
@@ -2279,7 +2279,7 @@
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is fully removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
@@ -2329,7 +2329,7 @@
         mediaDataManager.onNotificationRemoved(KEY)
 
         // We still make sure to remove it
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java
index bb5b572..dd05a0d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java
@@ -202,24 +202,24 @@
     @Test
     public void mediaDataRemoved() {
         // WHEN media data is removed without first receiving device or data
-        mManager.onMediaDataRemoved(KEY);
+        mManager.onMediaDataRemoved(KEY, false);
         // THEN a removed event isn't emitted
-        verify(mListener, never()).onMediaDataRemoved(eq(KEY));
+        verify(mListener, never()).onMediaDataRemoved(eq(KEY), anyBoolean());
     }
 
     @Test
     public void mediaDataRemovedAfterMediaEvent() {
         mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
                 0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        mManager.onMediaDataRemoved(KEY);
-        verify(mListener).onMediaDataRemoved(eq(KEY));
+        mManager.onMediaDataRemoved(KEY, false);
+        verify(mListener).onMediaDataRemoved(eq(KEY), eq(false));
     }
 
     @Test
     public void mediaDataRemovedAfterDeviceEvent() {
         mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
-        mManager.onMediaDataRemoved(KEY);
-        verify(mListener).onMediaDataRemoved(eq(KEY));
+        mManager.onMediaDataRemoved(KEY, false);
+        verify(mListener).onMediaDataRemoved(eq(KEY), eq(false));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
index 77ad263..35eefd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -205,9 +205,9 @@
 
             assertThat(currentMedia).containsExactly(mediaCommonModel)
 
-            mediaDataFilter.onMediaDataRemoved(KEY)
+            mediaDataFilter.onMediaDataRemoved(KEY, false)
 
-            verify(listener).onMediaDataRemoved(eq(KEY))
+            verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
             assertThat(currentMedia).doesNotContain(mediaCommonModel)
         }
 
@@ -218,9 +218,9 @@
 
             // GIVEN a media was removed for guest user
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
-            mediaDataFilter.onMediaDataRemoved(KEY)
+            mediaDataFilter.onMediaDataRemoved(KEY, false)
 
-            verify(listener, never()).onMediaDataRemoved(eq(KEY))
+            verify(listener, never()).onMediaDataRemoved(eq(KEY), eq(false))
             assertThat(currentMedia).isEmpty()
         }
 
@@ -239,7 +239,7 @@
             setUser(USER_GUEST)
 
             // THEN we should remove the main user's media
-            verify(listener).onMediaDataRemoved(eq(KEY))
+            verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
             assertThat(currentMedia).isEmpty()
         }
 
@@ -291,7 +291,7 @@
 
             val mediaLoadedStatesModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
             // THEN we should remove the private profile media
-            verify(listener).onMediaDataRemoved(eq(KEY_ALT))
+            verify(listener).onMediaDataRemoved(eq(KEY_ALT), eq(false))
             assertThat(currentMedia)
                 .containsExactly(MediaCommonModel.MediaControl(mediaLoadedStatesModel))
         }
@@ -502,7 +502,7 @@
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
 
             mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
-            mediaDataFilter.onMediaDataRemoved(KEY)
+            mediaDataFilter.onMediaDataRemoved(KEY, false)
             assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
                 .isFalse()
             assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 1de7ee3..5791826 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -384,7 +384,7 @@
         // THEN it is removed and listeners are informed
         foregroundExecutor.advanceClockToLast()
         foregroundExecutor.runAllReady()
-        verify(listener).onMediaDataRemoved(PACKAGE_NAME)
+        verify(listener).onMediaDataRemoved(PACKAGE_NAME, false)
     }
 
     @Test
@@ -567,7 +567,7 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         mediaDataProcessor.onNotificationRemoved(KEY)
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
     }
 
@@ -812,7 +812,7 @@
                 eq(false)
             )
         assertThat(mediaDataCaptor.value.resumption).isTrue()
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never()).onMediaDataRemoved(eq(KEY), anyBoolean())
         // WHEN the second is removed
         mediaDataProcessor.onNotificationRemoved(KEY_2)
         // THEN the data is for resumption and the second key is removed
@@ -826,7 +826,7 @@
                 eq(false)
             )
         assertThat(mediaDataCaptor.value.resumption).isTrue()
-        verify(listener).onMediaDataRemoved(eq(KEY_2))
+        verify(listener).onMediaDataRemoved(eq(KEY_2), eq(false))
     }
 
     @Test
@@ -851,7 +851,7 @@
         mediaDataProcessor.onNotificationRemoved(KEY)
 
         // THEN the media data is removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
@@ -901,7 +901,7 @@
         mediaDataProcessor.onNotificationRemoved(KEY)
 
         // THEN the media data is removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
@@ -940,7 +940,7 @@
         assertThat(mediaDataCaptor.value.isPlaying).isFalse()
 
         // And the oldest resume control was removed
-        verify(listener).onMediaDataRemoved(eq("0:$PACKAGE_NAME"))
+        verify(listener).onMediaDataRemoved(eq("0:$PACKAGE_NAME"), eq(false))
     }
 
     fun testOnNotificationRemoved_lockDownMode() {
@@ -950,7 +950,7 @@
         val data = mediaDataCaptor.value
         mediaDataProcessor.onNotificationRemoved(KEY)
 
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never()).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger, never())
             .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
@@ -1183,7 +1183,7 @@
         mediaDataProcessor.setMediaResumptionEnabled(false)
 
         // THEN the resume controls are dismissed
-        verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME))
+        verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
     }
 
@@ -1191,19 +1191,19 @@
     fun testDismissMedia_listenerCalled() {
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
-        val removed = mediaDataProcessor.dismissMediaData(KEY, 0L)
+        val removed = mediaDataProcessor.dismissMediaData(KEY, 0L, true)
         assertThat(removed).isTrue()
 
         foregroundExecutor.advanceClockToLast()
         foregroundExecutor.runAllReady()
 
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(true))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
     }
 
     @Test
     fun testDismissMedia_keyDoesNotExist_returnsFalse() {
-        val removed = mediaDataProcessor.dismissMediaData(KEY, 0L)
+        val removed = mediaDataProcessor.dismissMediaData(KEY, 0L, true)
         assertThat(removed).isFalse()
     }
 
@@ -2102,7 +2102,7 @@
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It remains as a regular player
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never()).onMediaDataRemoved(eq(KEY), anyBoolean())
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2118,7 +2118,7 @@
         mediaDataProcessor.onNotificationRemoved(KEY)
 
         // It is fully removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
@@ -2171,7 +2171,7 @@
         mediaDataProcessor.onNotificationRemoved(KEY)
 
         // It remains as a regular player
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(listener, never()).onMediaDataRemoved(eq(KEY), anyBoolean())
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2224,7 +2224,7 @@
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is fully removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
@@ -2278,7 +2278,7 @@
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is fully removed.
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(listener, never())
             .onMediaDataLoaded(
@@ -2304,7 +2304,7 @@
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is fully removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
         verify(listener, never())
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
@@ -2354,7 +2354,7 @@
         mediaDataProcessor.onNotificationRemoved(KEY)
 
         // We still make sure to remove it
-        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
index a447e44..befe64c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
@@ -60,6 +60,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.any
@@ -158,7 +159,8 @@
 
     @Test
     fun removeUnknown() {
-        manager.onMediaDataRemoved("unknown")
+        manager.onMediaDataRemoved("unknown", false)
+        verify(listener, never()).onKeyRemoved(eq(KEY), anyBoolean())
     }
 
     @Test
@@ -170,7 +172,7 @@
     @Test
     fun loadAndRemoveMediaData() {
         manager.onMediaDataLoaded(KEY, null, mediaData)
-        manager.onMediaDataRemoved(KEY)
+        manager.onMediaDataRemoved(KEY, false)
         fakeBgExecutor.runAllReady()
         verify(lmm).unregisterCallback(any())
         verify(muteAwaitManager).stopListening()
@@ -386,9 +388,9 @@
     fun listenerReceivesKeyRemoved() {
         manager.onMediaDataLoaded(KEY, null, mediaData)
         // WHEN the notification is removed
-        manager.onMediaDataRemoved(KEY)
+        manager.onMediaDataRemoved(KEY, true)
         // THEN the listener receives key removed event
-        verify(listener).onKeyRemoved(eq(KEY))
+        verify(listener).onKeyRemoved(eq(KEY), eq(true))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterTest.kt
index 5a3c220..030bca2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterTest.kt
@@ -165,10 +165,10 @@
 
     @Test
     fun noMediaSession_removedEventNotFiltered() {
-        filter.onMediaDataRemoved(KEY)
+        filter.onMediaDataRemoved(KEY, false)
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
-        verify(mediaListener).onMediaDataRemoved(eq(KEY))
+        verify(mediaListener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
@@ -193,11 +193,11 @@
         whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
         sessionListener.onActiveSessionsChanged(controllers)
         // WHEN a removed event is received
-        filter.onMediaDataRemoved(KEY)
+        filter.onMediaDataRemoved(KEY, false)
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataRemoved(eq(KEY))
+        verify(mediaListener).onMediaDataRemoved(eq(KEY), eq(false))
     }
 
     @Test
@@ -294,7 +294,7 @@
                 anyBoolean()
             )
         // AND there should be a removed event for key2
-        verify(mediaListener).onMediaDataRemoved(eq(key2))
+        verify(mediaListener).onMediaDataRemoved(eq(key2), eq(false))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
index 3cc65c9..cdbf9d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
@@ -166,12 +166,12 @@
     @Test
     fun testOnMediaDataRemoved_unregistersPlaybackListener() {
         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        mediaTimeoutListener.onMediaDataRemoved(KEY)
+        mediaTimeoutListener.onMediaDataRemoved(KEY, false)
         verify(mediaController).unregisterCallback(anyObject())
 
         // Ignores duplicate requests
         clearInvocations(mediaController)
-        mediaTimeoutListener.onMediaDataRemoved(KEY)
+        mediaTimeoutListener.onMediaDataRemoved(KEY, false)
         verify(mediaController, never()).unregisterCallback(anyObject())
     }
 
@@ -181,7 +181,7 @@
         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
         assertThat(executor.numPending()).isEqualTo(1)
         // WHEN the media is removed
-        mediaTimeoutListener.onMediaDataRemoved(KEY)
+        mediaTimeoutListener.onMediaDataRemoved(KEY, false)
         // THEN the timeout runnable is cancelled
         assertThat(executor.numPending()).isEqualTo(0)
     }
@@ -398,7 +398,7 @@
         // WHEN we have a resume control
         testOnMediaDataLoaded_resumption_registersTimeout()
         // AND the media is removed
-        mediaTimeoutListener.onMediaDataRemoved(PACKAGE)
+        mediaTimeoutListener.onMediaDataRemoved(PACKAGE, false)
 
         // THEN the timeout runnable is cancelled
         assertThat(executor.numPending()).isEqualTo(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index 0a5aace..3bb8b8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -33,6 +33,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -74,12 +76,14 @@
 import kotlinx.coroutines.test.TestDispatcher
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
+import org.junit.After
 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.anyLong
 import org.mockito.Mockito.floatThat
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
@@ -136,6 +140,9 @@
     private lateinit var testDispatcher: TestDispatcher
     private lateinit var mediaCarouselController: MediaCarouselController
 
+    private var originalResumeSetting =
+        Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -186,6 +193,15 @@
             )
     }
 
+    @After
+    fun tearDown() {
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RESUME,
+            originalResumeSetting
+        )
+    }
+
     @Test
     fun testPlayerOrdering() {
         // Test values: key, data, last active time
@@ -822,6 +838,7 @@
     @Test
     fun testKeyguardGone_showMediaCarousel() =
         kosmos.testScope.runTest {
+            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
             var updatedVisibility = false
             mediaCarouselController.updateHostVisibility = { updatedVisibility = true }
             mediaCarouselController.mediaCarousel = mediaCarousel
@@ -844,6 +861,7 @@
     @Test
     fun keyguardShowing_notAllowedOnLockscreen_updateVisibility() {
         kosmos.testScope.runTest {
+            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
             var updatedVisibility = false
             mediaCarouselController.updateHostVisibility = { updatedVisibility = true }
             mediaCarouselController.mediaCarousel = mediaCarousel
@@ -870,6 +888,7 @@
     @Test
     fun keyguardShowing_allowedOnLockscreen_updateVisibility() {
         kosmos.testScope.runTest {
+            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
             var updatedVisibility = false
             mediaCarouselController.updateHostVisibility = { updatedVisibility = true }
             mediaCarouselController.mediaCarousel = mediaCarousel
@@ -968,6 +987,45 @@
         verify(panel).updateAnimatorDurationScale()
     }
 
+    @Test
+    fun swipeToDismiss_pausedAndResumeOff_userInitiated() {
+        // When resumption is disabled, paused media should be dismissed after being swiped away
+        Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+
+        val pausedMedia = DATA.copy(isPlaying = false)
+        listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
+        mediaCarouselController.onSwipeToDismiss()
+
+        // When it can be removed immediately on update
+        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
+        val inactiveMedia = pausedMedia.copy(active = false)
+        listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
+
+        // This is processed as a user-initiated dismissal
+        verify(debugLogger).logMediaRemoved(eq(PAUSED_LOCAL), eq(true))
+        verify(mediaDataManager).dismissMediaData(eq(PAUSED_LOCAL), anyLong(), eq(true))
+    }
+
+    @Test
+    fun swipeToDismiss_pausedAndResumeOff_delayed_userInitiated() {
+        // When resumption is disabled, paused media should be dismissed after being swiped away
+        Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+        mediaCarouselController.updateHostVisibility = {}
+
+        val pausedMedia = DATA.copy(isPlaying = false)
+        listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
+        mediaCarouselController.onSwipeToDismiss()
+
+        // When it can't be removed immediately on update
+        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
+        val inactiveMedia = pausedMedia.copy(active = false)
+        listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
+        visualStabilityCallback.value.onReorderingAllowed()
+
+        // This is processed as a user-initiated dismissal
+        verify(mediaDataManager).dismissMediaData(eq(PAUSED_LOCAL), anyLong(), eq(true))
+    }
+
     /**
      * Helper method when a configuration change occurs.
      *
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index 83e4d31..0c9fee9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -1344,7 +1344,7 @@
         assertThat(dismiss.isEnabled).isEqualTo(true)
         dismiss.callOnClick()
         verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
-        verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong())
+        verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong(), eq(true))
     }
 
     @Test
@@ -1360,7 +1360,8 @@
     @Test
     fun player_dismissButtonClick_notInManager() {
         val mediaKey = "key for dismissal"
-        whenever(mediaDataManager.dismissMediaData(eq(mediaKey), anyLong())).thenReturn(false)
+        whenever(mediaDataManager.dismissMediaData(eq(mediaKey), anyLong(), eq(true)))
+            .thenReturn(false)
 
         player.attachPlayer(viewHolder)
         val state = mediaData.copy(notificationKey = KEY)
@@ -1369,8 +1370,8 @@
         assertThat(dismiss.isEnabled).isEqualTo(true)
         dismiss.callOnClick()
 
-        verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong())
-        verify(mediaCarouselController).removePlayer(eq(mediaKey), eq(false), eq(false))
+        verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong(), eq(true))
+        verify(mediaCarouselController).removePlayer(eq(mediaKey), eq(false), eq(false), eq(true))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
index ff7c970..8f8630e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
@@ -104,11 +104,11 @@
         listener.onMediaDataLoaded(mKey, mOldKey, mData, /* immediately= */true,
                 /* receivedSmartspaceCardLatency= */0, /* isSsReactived= */ false);
 
-        listener.onMediaDataRemoved(mKey);
+        listener.onMediaDataRemoved(mKey, false);
         verify(mDreamOverlayStateController, never()).removeComplication(any());
 
         when(mMediaDataManager.hasActiveMedia()).thenReturn(false);
-        listener.onMediaDataRemoved(mKey);
+        listener.onMediaDataRemoved(mKey, false);
         verify(mDreamOverlayStateController).removeComplication(eq(mMediaEntryComplication));
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
index 8e05410..c06a28e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
@@ -42,13 +42,13 @@
     fun updateFlags() {
         underTest.updateFlags(
             Display.DEFAULT_DISPLAY,
-            1 to true,
-            2 to false,
-            3 to true,
+            1L to true,
+            2L to false,
+            3L to true,
         )
 
-        assertThat(underTest.flags and 1).isNotEqualTo(0)
-        assertThat(underTest.flags and 2).isEqualTo(0)
-        assertThat(underTest.flags and 3).isNotEqualTo(0)
+        assertThat(underTest.flags and 1L).isNotEqualTo(0L)
+        assertThat(underTest.flags and 2L).isEqualTo(0L)
+        assertThat(underTest.flags and 3L).isNotEqualTo(0L)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
index 224e755..2ff660f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
@@ -125,7 +125,7 @@
     private AccessibilityManager.AccessibilityServicesStateChangeListener
             mAccessibilityServicesStateChangeListener;
 
-    private static final int ACCESSIBILITY_BUTTON_CLICKABLE_STATE =
+    private static final long ACCESSIBILITY_BUTTON_CLICKABLE_STATE =
             SYSUI_STATE_A11Y_BUTTON_CLICKABLE | SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
     private NavBarHelper mNavBarHelper;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 0e7a215..6cea1e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -38,7 +38,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -300,7 +300,7 @@
         doNothing().when(mWindowManager).addView(any(), any());
         doNothing().when(mWindowManager).removeViewImmediate(any());
         mMockSysUiState = mock(SysUiState.class);
-        when(mMockSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mMockSysUiState);
+        when(mMockSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mMockSysUiState);
 
         mContext.addMockSystemService(WindowManager.class, mWindowManager);
         mSysuiTestableContextExternal.addMockSystemService(WindowManager.class, mWindowManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
index 8d01e80d..bba275e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
@@ -16,18 +16,18 @@
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.wm.shell.back.BackAnimation
 import com.android.wm.shell.pip.Pip
+import java.util.Optional
 import org.junit.Before
 import org.junit.Test
 import org.mockito.ArgumentMatchers
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyLong
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
-import java.util.Optional
 
 @SmallTest
 class TaskbarDelegateTest : SysuiTestCase() {
@@ -74,7 +74,7 @@
         `when`(mNavBarHelper.edgeBackGestureHandler).thenReturn(mEdgeBackGestureHandler)
         `when`(mLightBarControllerFactory.create(any())).thenReturn(mLightBarTransitionController)
         `when`(mNavBarHelper.currentSysuiState).thenReturn(mCurrentSysUiState)
-        `when`(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState)
+        `when`(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState)
         mTaskStackChangeListeners = TaskStackChangeListeners.getTestInstance()
         mTaskbarDelegate = TaskbarDelegate(context, mLightBarControllerFactory,
             mStatusBarKeyguardViewManager)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
index f88a5a0..b75b318 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
@@ -20,7 +20,7 @@
 import static junit.framework.Assert.assertNotSame;
 
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -81,7 +81,7 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
-        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+        when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
         when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
         when(mSystemUIDialog.getContext()).thenReturn(mContext);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index effae5f..74deae3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -72,7 +72,7 @@
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.atLeast
 import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.intThat
+import org.mockito.Mockito.longThat
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.times
@@ -162,7 +162,7 @@
 
         verify(overviewProxy)
             .onSystemUiStateChanged(
-                intThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_AWAKE }
+                longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_AWAKE }
             )
     }
 
@@ -172,7 +172,7 @@
 
         verify(overviewProxy)
             .onSystemUiStateChanged(
-                intThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_WAKING }
+                longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_WAKING }
             )
     }
 
@@ -182,7 +182,7 @@
 
         verify(overviewProxy)
             .onSystemUiStateChanged(
-                intThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_ASLEEP }
+                longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_ASLEEP }
             )
     }
 
@@ -194,7 +194,7 @@
 
         verify(overviewProxy)
             .onSystemUiStateChanged(
-                intThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_GOING_TO_SLEEP }
+                longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_GOING_TO_SLEEP }
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index 6846c72..fcc6b4f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -55,6 +55,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
@@ -94,7 +95,7 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
         whenever(dprLazy.get()).thenReturn(devicePolicyResolver)
-        whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState)
+        whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
         whenever(screenCaptureDisabledDialogDelegate.createSysUIDialog())
             .thenReturn(screenCaptureDisabledDialog)
         whenever(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 5b47c94..766113f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -655,7 +655,7 @@
         when(mView.getParent()).thenReturn(mViewParent);
         when(mQs.getHeader()).thenReturn(mQsHeader);
         when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
-        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+        when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
 
         mMainHandler = new Handler(Looper.getMainLooper());
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index 04fa590..845744a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -42,13 +42,10 @@
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FakeFeatureFlagsClassic;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -59,9 +56,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.qs.QSFragmentLegacy;
 import com.android.systemui.res.R;
-import com.android.systemui.scene.data.repository.SceneContainerRepository;
 import com.android.systemui.scene.domain.interactor.SceneInteractor;
-import com.android.systemui.scene.shared.logger.SceneLogger;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
@@ -176,12 +171,7 @@
     protected Handler mMainHandler;
     protected LockscreenShadeTransitionController.Callback mLockscreenShadeTransitionCallback;
 
-    protected final ShadeExpansionStateManager mShadeExpansionStateManager =
-            new ShadeExpansionStateManager();
-
     protected FragmentHostManager.FragmentListener mFragmentListener;
-    private FromLockscreenTransitionInteractor mFromLockscreenTransitionInteractor;
-    private FromPrimaryBouncerTransitionInteractor mFromPrimaryBouncerTransitionInteractor;
 
     @Before
     public void setup() {
@@ -190,19 +180,11 @@
         mStatusBarStateController = mKosmos.getStatusBarStateController();
 
         mKosmos.getFakeDeviceProvisioningRepository().setDeviceProvisioned(true);
-        FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
         FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository();
 
         PowerInteractor powerInteractor = mKosmos.getPowerInteractor();
 
-        SceneInteractor sceneInteractor = new SceneInteractor(
-                mTestScope.getBackgroundScope(),
-                new SceneContainerRepository(
-                        mTestScope.getBackgroundScope(),
-                        mKosmos.getFakeSceneContainerConfig(),
-                        mKosmos.getSceneDataSource()),
-                mock(SceneLogger.class),
-                mKosmos.getDeviceUnlockedInteractor());
+        SceneInteractor sceneInteractor = mKosmos.getSceneInteractor();
 
         KeyguardTransitionInteractor keyguardTransitionInteractor =
                 mKosmos.getKeyguardTransitionInteractor();
@@ -220,10 +202,6 @@
                 () -> mKosmos.getSharedNotificationContainerInteractor(),
                 mTestScope);
 
-        mFromLockscreenTransitionInteractor = mKosmos.getFromLockscreenTransitionInteractor();
-        mFromPrimaryBouncerTransitionInteractor =
-                mKosmos.getFromPrimaryBouncerTransitionInteractor();
-
         ResourcesSplitShadeStateController splitShadeStateController =
                 new ResourcesSplitShadeStateController();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index c088609..fe6a88d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -51,6 +51,7 @@
 import com.android.systemui.shade.ShadeHeaderController;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -80,6 +81,7 @@
     @Mock private QuickSettingsController mQuickSettingsController;
     @Mock private ShadeViewController mShadeViewController;
     @Mock private PanelExpansionInteractor mPanelExpansionInteractor;
+    @Mock private Lazy<ShadeInteractor> mShadeInteractorLazy;
     @Mock private ShadeHeaderController mShadeHeaderController;
     @Mock private RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
     private final MetricsLogger mMetricsLogger = new FakeMetricsLogger();
@@ -115,6 +117,7 @@
                 mShadeController,
                 mCommandQueue,
                 mPanelExpansionInteractor,
+                mShadeInteractorLazy,
                 mShadeHeaderController,
                 mRemoteInputQuickSettingsDisabler,
                 mMetricsLogger,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt
index 408157b..3e69e87 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt
@@ -17,7 +17,10 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
 
 var Kosmos.keyguardTransitionRepository: KeyguardTransitionRepository by
     Kosmos.Fixture { fakeKeyguardTransitionRepository }
 var Kosmos.fakeKeyguardTransitionRepository by Kosmos.Fixture { FakeKeyguardTransitionRepository() }
+var Kosmos.realKeyguardTransitionRepository: KeyguardTransitionRepository by
+    Kosmos.Fixture { KeyguardTransitionRepositoryImpl(testDispatcher) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
new file mode 100644
index 0000000..7d0e8b1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.lockscreenSceneTransitionRepository by
+    Kosmos.Fixture { LockscreenSceneTransitionRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
new file mode 100644
index 0000000..3c1f7b1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor.scenetransition
+
+import com.android.systemui.keyguard.data.repository.lockscreenSceneTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+
+var Kosmos.lockscreenSceneTransitionInteractor by
+    Kosmos.Fixture {
+        LockscreenSceneTransitionInteractor(
+            transitionInteractor = keyguardTransitionInteractor,
+            applicationScope = applicationCoroutineScope,
+            sceneInteractor = sceneInteractor,
+            repository = lockscreenSceneTransitionRepository,
+        )
+    }
diff --git a/services/backup/java/com/android/server/backup/transport/TransportConnection.java b/services/backup/java/com/android/server/backup/transport/TransportConnection.java
index 1009787..67ebb3e 100644
--- a/services/backup/java/com/android/server/backup/transport/TransportConnection.java
+++ b/services/backup/java/com/android/server/backup/transport/TransportConnection.java
@@ -658,11 +658,13 @@
      * This class is a proxy to TransportClient methods that doesn't hold a strong reference to the
      * TransportClient, allowing it to be GC'ed. If the reference was lost it logs a message.
      */
-    private static class TransportConnectionMonitor implements ServiceConnection {
+    @VisibleForTesting
+    static class TransportConnectionMonitor implements ServiceConnection {
         private final Context mContext;
         private final WeakReference<TransportConnection> mTransportClientRef;
 
-        private TransportConnectionMonitor(Context context,
+        @VisibleForTesting
+        TransportConnectionMonitor(Context context,
                 TransportConnection transportConnection) {
             mContext = context;
             mTransportClientRef = new WeakReference<>(transportConnection);
@@ -704,7 +706,13 @@
 
         /** @see TransportConnection#finalize() */
         private void referenceLost(String caller) {
-            mContext.unbindService(this);
+            try {
+                mContext.unbindService(this);
+            } catch (IllegalArgumentException e) {
+                TransportUtils.log(Priority.WARN, TAG,
+                        caller + " called but unbindService failed: " + e.getMessage());
+                return;
+            }
             TransportUtils.log(
                     Priority.INFO,
                     TAG,
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index 9069689..026d29c 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -135,7 +135,7 @@
      */
     public PendingIntent buildPermissionTransferUserConsentIntent(String packageName,
             @UserIdInt int userId, int associationId) {
-        if (PackageUtils.isPackageAllowlisted(mContext, mPackageManager, packageName)) {
+        if (PackageUtils.isPermSyncAutoEnabled(mContext, mPackageManager, packageName)) {
             Slog.i(LOG_TAG, "User consent Intent should be skipped. Returning null.");
             // Auto enable perm sync for the allowlisted packages, but don't override user decision
             PermissionSyncRequest request = getPermissionSyncRequest(associationId);
diff --git a/services/companion/java/com/android/server/companion/utils/PackageUtils.java b/services/companion/java/com/android/server/companion/utils/PackageUtils.java
index 254d28b..94ab9dd 100644
--- a/services/companion/java/com/android/server/companion/utils/PackageUtils.java
+++ b/services/companion/java/com/android/server/companion/utils/PackageUtils.java
@@ -21,6 +21,11 @@
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.os.Binder.getCallingUid;
 
+import static com.android.internal.R.array.config_companionDeviceCerts;
+import static com.android.internal.R.array.config_companionDevicePackages;
+import static com.android.internal.R.array.config_companionPermSyncEnabledCerts;
+import static com.android.internal.R.array.config_companionPermSyncEnabledPackages;
+
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -185,15 +190,30 @@
      */
     public static boolean isPackageAllowlisted(Context context,
             PackageManagerInternal packageManagerInternal, @NonNull String packageName) {
-        final String[] allowlistedPackages = context.getResources()
-                .getStringArray(com.android.internal.R.array.config_companionDevicePackages);
+        return isPackageAllowlisted(context, packageManagerInternal, packageName,
+                config_companionDevicePackages, config_companionDeviceCerts);
+    }
+
+    /**
+     * Check if perm sync is allowlisted and auto-enabled for the package.
+     */
+    public static boolean isPermSyncAutoEnabled(Context context,
+            PackageManagerInternal packageManagerInternal, String packageName) {
+        return isPackageAllowlisted(context, packageManagerInternal, packageName,
+                config_companionPermSyncEnabledPackages, config_companionPermSyncEnabledCerts);
+    }
+
+    private static boolean isPackageAllowlisted(Context context,
+            PackageManagerInternal packageManagerInternal, String packageName,
+            int packagesConfig, int certsConfig) {
+        final String[] allowlistedPackages = context.getResources().getStringArray(packagesConfig);
         if (!ArrayUtils.contains(allowlistedPackages, packageName)) {
             Slog.d(TAG, packageName + " is not allowlisted.");
             return false;
         }
 
         final String[] allowlistedPackagesSignatureDigests = context.getResources()
-                .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
+                .getStringArray(certsConfig);
         final Set<String> allowlistedSignatureDigestsForRequestingPackage = new HashSet<>();
         for (int i = 0; i < allowlistedPackages.length; i++) {
             if (allowlistedPackages[i].equals(packageName)) {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index d153c18..dc1155a 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -232,6 +232,7 @@
         "android.hardware.rebootescrow-V1-java",
         "android.hardware.power.stats-V2-java",
         "android.hidl.manager-V1.2-java",
+        "audio-permission-aidl-java",
         "cbor-java",
         "com.android.media.audio-aconfig-java",
         "icu4j_calendar_astronomer",
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 1db3483..ad93f6f 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -55,6 +55,7 @@
 import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM;
 import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM_OPS;
 import static android.app.AppOpsManager.SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE;
+import static android.app.AppOpsManager.UID_STATE_NONEXISTENT;
 import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES;
 import static android.app.AppOpsManager._NUM_OP;
 import static android.app.AppOpsManager.extractFlagsFromKey;
@@ -70,7 +71,6 @@
 import static android.content.Intent.EXTRA_REPLACING;
 import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
 import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
-import static android.permission.flags.Flags.runtimePermissionAppopsMappingEnabled;
 
 import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
 
@@ -130,6 +130,7 @@
 import android.os.UserHandle;
 import android.os.storage.StorageManagerInternal;
 import android.permission.PermissionManager;
+import android.permission.flags.Flags;
 import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -140,6 +141,7 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
+import android.util.SparseLongArray;
 import android.util.TimeUtils;
 import android.util.Xml;
 
@@ -153,7 +155,6 @@
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.app.IAppOpsStartedCallback;
 import com.android.internal.app.MessageSamplingConfig;
-import com.android.internal.camera.flags.Flags;
 import com.android.internal.compat.IPlatformCompat;
 import com.android.internal.os.Clock;
 import com.android.internal.pm.pkg.component.ParsedAttribution;
@@ -1421,6 +1422,9 @@
     // The callback method from AppOpsUidStateTracker
     private void onUidStateChanged(int uid, int state, boolean foregroundModeMayChange) {
         synchronized (this) {
+            if (state == UID_STATE_NONEXISTENT) {
+                onUidProcessDeathLocked(uid);
+            }
             UidState uidState = getUidStateLocked(uid, false);
 
             boolean hasForegroundWatchers = false;
@@ -1508,6 +1512,11 @@
                 }
             }
 
+            if (state == UID_STATE_NONEXISTENT) {
+                // For UID_STATE_NONEXISTENT, we don't call onUidStateChanged for AttributedOps
+                return;
+            }
+
             if (uidState != null) {
                 int numPkgs = uidState.pkgOps.size();
                 for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) {
@@ -1532,6 +1541,81 @@
         }
     }
 
+    @GuardedBy("this")
+    private void onUidProcessDeathLocked(int uid) {
+        if (!mUidStates.contains(uid) || !Flags.finishRunningOpsForKilledPackages()) {
+            return;
+        }
+        final SparseLongArray chainsToFinish = new SparseLongArray();
+        doForAllAttributedOpsInUidLocked(uid, (attributedOp) -> {
+            attributedOp.doForAllInProgressStartOpEvents((event) -> {
+                int chainId = event.getAttributionChainId();
+                if (chainId != ATTRIBUTION_CHAIN_ID_NONE) {
+                    long currentEarliestStartTime =
+                            chainsToFinish.get(chainId, Long.MAX_VALUE);
+                    if (event.getStartTime() < currentEarliestStartTime) {
+                        // Store the earliest chain link we're finishing, so that we can go back
+                        // and finish any links in the chain that started after this one
+                        chainsToFinish.put(chainId, event.getStartTime());
+                    }
+                }
+                attributedOp.finished(event.getClientId());
+            });
+        });
+        finishChainsLocked(chainsToFinish);
+    }
+
+    @GuardedBy("this")
+    private void finishChainsLocked(SparseLongArray chainsToFinish) {
+        doForAllAttributedOpsLocked((attributedOp) -> {
+            attributedOp.doForAllInProgressStartOpEvents((event) -> {
+                int chainId = event.getAttributionChainId();
+                // If this event is part of a chain, and this event started after the event in the
+                // chain we already finished, then finish this event, too
+                long earliestEventStart = chainsToFinish.get(chainId, Long.MAX_VALUE);
+                if (chainId != ATTRIBUTION_CHAIN_ID_NONE
+                        && event.getStartTime() >= earliestEventStart) {
+                    attributedOp.finished(event.getClientId());
+                }
+            });
+        });
+    }
+
+    @GuardedBy("this")
+    private void doForAllAttributedOpsLocked(Consumer<AttributedOp> action) {
+        int numUids = mUidStates.size();
+        for (int uidNum = 0; uidNum < numUids; uidNum++) {
+            int uid = mUidStates.keyAt(uidNum);
+            doForAllAttributedOpsInUidLocked(uid, action);
+        }
+    }
+
+    @GuardedBy("this")
+    private void doForAllAttributedOpsInUidLocked(int uid, Consumer<AttributedOp> action) {
+        UidState uidState = mUidStates.get(uid);
+        if (uidState == null) {
+            return;
+        }
+
+        int numPkgs = uidState.pkgOps.size();
+        for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) {
+            Ops ops = uidState.pkgOps.valueAt(pkgNum);
+            int numOps = ops.size();
+            for (int opNum = 0; opNum < numOps; opNum++) {
+                Op op = ops.valueAt(opNum);
+                int numDevices = op.mDeviceAttributedOps.size();
+                for (int deviceNum = 0; deviceNum < numDevices; deviceNum++) {
+                    ArrayMap<String, AttributedOp> attrOps =
+                            op.mDeviceAttributedOps.valueAt(deviceNum);
+                    int numAttributions = attrOps.size();
+                    for (int attrNum = 0; attrNum < numAttributions; attrNum++) {
+                        action.accept(attrOps.valueAt(attrNum));
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Notify the proc state or capability has changed for a certain UID.
      */
@@ -2702,7 +2786,7 @@
      * have information on them.
      */
     private static boolean isOpAllowedForUid(int uid) {
-        return runtimePermissionAppopsMappingEnabled()
+        return Flags.runtimePermissionAppopsMappingEnabled()
                 && (uid == Process.ROOT_UID || uid == Process.SYSTEM_UID);
     }
 
@@ -4775,8 +4859,8 @@
         if ((code == OP_CAMERA) && isAutomotive()) {
             final long identity = Binder.clearCallingIdentity();
             try {
-                if ((Flags.cameraPrivacyAllowlist())
-                        && (mSensorPrivacyManager.isCameraPrivacyEnabled(packageName))) {
+                if (com.android.internal.camera.flags.Flags.cameraPrivacyAllowlist()
+                        && mSensorPrivacyManager.isCameraPrivacyEnabled(packageName)) {
                     return true;
                 }
             } finally {
diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java b/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java
index 18ea8cf..268b286 100644
--- a/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java
+++ b/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java
@@ -68,6 +68,7 @@
             return UID_STATE_BACKGROUND;
         }
 
+        // UID_STATE_NONEXISTENT is deliberately excluded here
         return UID_STATE_CACHED;
     }
 
diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
index bc6ef20..03c8156 100644
--- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
@@ -34,7 +34,9 @@
 import static android.app.AppOpsManager.OP_TAKE_AUDIO_FOCUS;
 import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
 import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED;
+import static android.app.AppOpsManager.UID_STATE_NONEXISTENT;
 import static android.app.AppOpsManager.UID_STATE_TOP;
+import static android.permission.flags.Flags.finishRunningOpsForKilledPackages;
 
 import static com.android.server.appop.AppOpsUidStateTracker.processStateToUidState;
 
@@ -343,13 +345,14 @@
         int capability = mCapability.get(uid, PROCESS_CAPABILITY_NONE);
         boolean appWidgetVisible = mAppWidgetVisible.get(uid, false);
 
+        boolean foregroundChange = uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED
+                != pendingUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED
+                || capability != pendingCapability
+                || appWidgetVisible != pendingAppWidgetVisible;
+
         if (uidState != pendingUidState
                 || capability != pendingCapability
                 || appWidgetVisible != pendingAppWidgetVisible) {
-            boolean foregroundChange = uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED
-                    != pendingUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED
-                    || capability != pendingCapability
-                    || appWidgetVisible != pendingAppWidgetVisible;
 
             if (foregroundChange) {
                 // To save on memory usage, log only interesting changes.
@@ -372,6 +375,16 @@
             mCapability.delete(uid);
             mAppWidgetVisible.delete(uid);
             mPendingGone.delete(uid);
+            if (finishRunningOpsForKilledPackages()) {
+                for (int i = 0; i < mUidStateChangedCallbacks.size(); i++) {
+                    UidStateChangedCallback cb = mUidStateChangedCallbacks.keyAt(i);
+                    Executor executor = mUidStateChangedCallbacks.valueAt(i);
+
+                    executor.execute(PooledLambda.obtainRunnable(
+                            UidStateChangedCallback::onUidStateChanged, cb, uid,
+                            UID_STATE_NONEXISTENT, foregroundChange));
+                }
+            }
         } else {
             mUidStates.put(uid, pendingUidState);
             mCapability.put(uid, pendingCapability);
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
index 2760ccf..02fc993 100644
--- a/services/core/java/com/android/server/appop/AttributedOp.java
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
@@ -38,6 +38,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 
 final class AttributedOp {
     private final @NonNull AppOpsService mAppOpsService;
@@ -256,6 +257,19 @@
         }
     }
 
+    public void doForAllInProgressStartOpEvents(Consumer<InProgressStartOpEvent> action) {
+        ArrayMap<IBinder, AttributedOp.InProgressStartOpEvent> events = isPaused()
+                ? mPausedInProgressEvents : mInProgressEvents;
+        if (events == null) {
+            return;
+        }
+
+        int numStartedOps = events.size();
+        for (int i = 0; i < numStartedOps; i++) {
+            action.accept(events.valueAt(i));
+        }
+    }
+
     /**
      * Update state when finishOp was called. Will finish started ops, and delete paused ops.
      *
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index c310822..15c5c10 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -4440,7 +4440,8 @@
                     || usage == AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) {
                 voiceActive = true;
             }
-            if (usage == AudioAttributes.USAGE_MEDIA || usage == AudioAttributes.USAGE_GAME) {
+            if (usage == AudioAttributes.USAGE_MEDIA || usage == AudioAttributes.USAGE_GAME
+                    || usage == AudioAttributes.USAGE_UNKNOWN) {
                 mediaActive = true;
             }
         }
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index e2c4b46..cae1695 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -347,9 +347,6 @@
     //------------------------------------------------------
     // routing monitoring
     synchronized void onRoutingUpdated() {
-        if (!mFeatureEnabled) {
-            return;
-        }
         switch (mState) {
             case STATE_UNINITIALIZED:
             case STATE_NOT_SUPPORTED:
@@ -393,7 +390,7 @@
             setDispatchAvailableState(false);
         }
 
-        boolean enabled = able && enabledAvailable.first;
+        boolean enabled = mFeatureEnabled && able && enabledAvailable.first;
         if (enabled) {
             loglogi("Enabling Spatial Audio since enabled for media device:"
                     + currentDevice);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 848f74e..67df992 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -485,16 +485,6 @@
         return userData.mBindingController.getSelectedMethodId();
     }
 
-    /**
-     * The current binding sequence number, incremented every time there is
-     * a new bind performed.
-     */
-    @GuardedBy("ImfLock.class")
-    private int getSequenceNumberLocked() {
-        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
-        return userData.mBindingController.getSequenceNumber();
-    }
-
     @GuardedBy("ImfLock.class")
     @Nullable
     InputMethodInfo queryInputMethodForCurrentUserLocked(@NonNull String imeId) {
@@ -1915,7 +1905,6 @@
         return mClientController.getClient(client.asBinder());
     }
 
-    // TODO(b/314150112): Move this to ClientController.
     @GuardedBy("ImfLock.class")
     void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) {
         if (mCurClient != null) {
@@ -1935,7 +1924,13 @@
             // all accessibility too. That means, when input method get disconnected (including
             // switching ime), we also unbind accessibility
             mCurClient.mClient.setActive(false /* active */, false /* fullscreen */);
-            mCurClient.mClient.onUnbindMethod(getSequenceNumberLocked(), unbindClientReason);
+
+            // TODO(b/325515685): make binding controller user independent. Before this change, the
+            //  following dependencies also need to be user independent: mCurClient, mBoundToMethod,
+            //  getCurMethodLocked(), and mMenuController.
+            final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+            mCurClient.mClient.onUnbindMethod(userData.mBindingController.getSequenceNumber(),
+                    unbindClientReason);
             mCurClient.mSessionRequested = false;
             mCurClient.mSessionRequestedForAccessibility = false;
             mCurClient = null;
@@ -2013,12 +2008,14 @@
 
         final boolean restarting = !initial;
         final Binder startInputToken = new Binder();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
         final StartInputInfo info = new StartInputInfo(mCurrentUserId,
                 getCurTokenLocked(),
                 mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting,
                 UserHandle.getUserId(mCurClient.mUid),
                 mCurClient.mSelfReportedDisplayId, mImeBindingState.mFocusedWindow, mCurEditorInfo,
-                mImeBindingState.mFocusedWindowSoftInputMode, getSequenceNumberLocked());
+                mImeBindingState.mFocusedWindowSoftInputMode,
+                userData.mBindingController.getSequenceNumber());
         mImeTargetWindowMap.put(startInputToken, mImeBindingState.mFocusedWindow);
         mStartInputHistory.addEntry(info);
 
@@ -2051,21 +2048,20 @@
                     null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
         }
 
-        String curId = getCurIdLocked();
+        final var curId = getCurIdLocked();
         final InputMethodInfo curInputMethodInfo = InputMethodSettingsRepository.get(mCurrentUserId)
                 .getMethodMap().get(curId);
         final boolean suppressesSpellChecker =
                 curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker();
         final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions =
                 createAccessibilityInputMethodSessions(mCurClient.mAccessibilitySessions);
-        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
         if (userData.mBindingController.supportsStylusHandwriting() && hasSupportedStylusLocked()) {
             mHwController.setInkWindowInitializer(new InkWindowInitializer());
         }
         return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
                 session.mSession, accessibilityInputMethodSessions,
                 (session.mChannel != null ? session.mChannel.dup() : null),
-                curId, getSequenceNumberLocked(), suppressesSpellChecker);
+                curId, userData.mBindingController.getSequenceNumber(), suppressesSpellChecker);
     }
 
     @GuardedBy("ImfLock.class")
@@ -2341,7 +2337,8 @@
                 requestClientSessionForAccessibilityLocked(cs);
                 return new InputBindResult(
                         InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
-                        null, null, null, getCurIdLocked(), getSequenceNumberLocked(), false);
+                        null, null, null, getCurIdLocked(),
+                        userData.mBindingController.getSequenceNumber(), false);
             } else {
                 final long lastBindTime = userData.mBindingController.getLastBindTime();
                 long bindingDuration = SystemClock.uptimeMillis() - lastBindTime;
@@ -2355,7 +2352,8 @@
                     // to see if we can get back in touch with the service.
                     return new InputBindResult(
                             InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
-                            null, null, null, getCurIdLocked(), getSequenceNumberLocked(), false);
+                            null, null, null, getCurIdLocked(),
+                            userData.mBindingController.getSequenceNumber(), false);
                 } else {
                     EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME,
                             getSelectedMethodIdLocked(), bindingDuration, 0);
@@ -3567,12 +3565,14 @@
                     "InputMethodManagerService#startInputOrWindowGainedFocus", mDumper);
             final InputBindResult result;
             synchronized (ImfLock.class) {
+                final var userData = mUserDataRepository.getOrCreate(userId);
                 // If the system is not yet ready, we shouldn't be running third party code.
                 if (!mSystemReady) {
                     return new InputBindResult(
                             InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY,
                             null /* method */, null /* accessibilitySessions */, null /* channel */,
-                            getSelectedMethodIdLocked(), getSequenceNumberLocked(),
+                            getSelectedMethodIdLocked(),
+                            userData.mBindingController.getSequenceNumber(),
                             false /* isInputMethodSuppressingSpellChecker */);
                 }
                 final ClientState cs = mClientController.getClient(client.asBinder());
@@ -3664,7 +3664,7 @@
                     result = startInputOrWindowGainedFocusInternalLocked(startInputReason,
                             client, windowToken, startInputFlags, softInputMode, windowFlags,
                             editorInfo, inputConnection, remoteAccessibilityInputConnection,
-                            unverifiedTargetSdkVersion, userId, imeDispatcher, cs);
+                            unverifiedTargetSdkVersion, userData, imeDispatcher, cs);
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
@@ -3692,7 +3692,7 @@
             @SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo editorInfo,
             IRemoteInputConnection inputContext,
             @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
-            int unverifiedTargetSdkVersion, @UserIdInt int userId,
+            int unverifiedTargetSdkVersion, @NonNull UserDataRepository.UserData userData,
             @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs) {
         if (DEBUG) {
             Slog.v(TAG, "startInputOrWindowGainedFocusInternalLocked: reason="
@@ -3705,7 +3705,7 @@
                     + " softInputMode=" + InputMethodDebug.softInputModeToString(softInputMode)
                     + " windowFlags=#" + Integer.toHexString(windowFlags)
                     + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion
-                    + " userId=" + userId
+                    + " userData=" + userData
                     + " imeDispatcher=" + imeDispatcher
                     + " cs=" + cs);
         }
@@ -3724,7 +3724,6 @@
                 startInputByWinGainedFocus, toolType);
         mVisibilityStateComputer.setWindowState(windowToken, windowState);
 
-        final var userData = mUserDataRepository.getOrCreate(userId);
         if (sameWindowFocused && isTextEditor) {
             if (DEBUG) {
                 Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client
@@ -4429,7 +4428,7 @@
             final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
             final long token = proto.start(fieldId);
             proto.write(CUR_METHOD_ID, getSelectedMethodIdLocked());
-            proto.write(CUR_SEQ, getSequenceNumberLocked());
+            proto.write(CUR_SEQ, userData.mBindingController.getSequenceNumber());
             proto.write(CUR_CLIENT, Objects.toString(mCurClient));
             mImeBindingState.dumpDebug(proto, mWindowManagerInternal);
             proto.write(LAST_IME_TARGET_WINDOW_NAME,
@@ -5626,6 +5625,7 @@
         public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
                 IAccessibilityInputMethodSession session, @UserIdInt int userId) {
             synchronized (ImfLock.class) {
+                final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
                 // TODO(b/305829876): Implement user ID verification
                 if (mCurClient != null) {
                     clearClientSessionForAccessibilityLocked(mCurClient, accessibilityConnectionId);
@@ -5647,8 +5647,10 @@
                                     mCurClient.mAccessibilitySessions);
                     final InputBindResult res = new InputBindResult(
                             InputBindResult.ResultCode.SUCCESS_WITH_ACCESSIBILITY_SESSION,
-                            imeSession, accessibilityInputMethodSessions, null, getCurIdLocked(),
-                            getSequenceNumberLocked(), false);
+                            imeSession, accessibilityInputMethodSessions, /* channel= */ null,
+                            getCurIdLocked(),
+                            userData.mBindingController.getSequenceNumber(),
+                            /* isInputMethodSuppressingSpellChecker= */ false);
                     mCurClient.mClient.onBindAccessibilityService(res, accessibilityConnectionId);
                 }
             }
@@ -5658,6 +5660,7 @@
         public void unbindAccessibilityFromCurrentClient(int accessibilityConnectionId,
                 @UserIdInt int userId) {
             synchronized (ImfLock.class) {
+                final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
                 // TODO(b/305829876): Implement user ID verification
                 if (mCurClient != null) {
                     if (DEBUG) {
@@ -5667,7 +5670,7 @@
                     // A11yManagerService unbinds the disabled accessibility service. We don't need
                     // to do it here.
                     mCurClient.mClient.onUnbindAccessibilityService(
-                            getSequenceNumberLocked(),
+                            userData.mBindingController.getSequenceNumber(),
                             accessibilityConnectionId);
                 }
                 // We only have sessions when we bound to an input method. Remove this session
@@ -5877,13 +5880,11 @@
             @SuppressWarnings("GuardedBy") Consumer<ClientState> clientControllerDump = c -> {
                 p.println("  " + c + ":");
                 p.println("    client=" + c.mClient);
-
                 p.println("    fallbackInputConnection="
                         + c.mFallbackInputConnection);
                 p.println("    sessionRequested="
                         + c.mSessionRequested);
-                p.println(
-                        "    sessionRequestedForAccessibility="
+                p.println("    sessionRequestedForAccessibility="
                                 + c.mSessionRequestedForAccessibility);
                 p.println("    curSession=" + c.mCurSession);
                 p.println("    selfReportedDisplayId=" + c.mSelfReportedDisplayId);
@@ -5891,13 +5892,15 @@
                 p.println("    pid=" + c.mPid);
             };
             mClientController.forAllClients(clientControllerDump);
+            final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
             p.println("  mCurrentUserId=" + mCurrentUserId);
             p.println("  mCurMethodId=" + getSelectedMethodIdLocked());
             client = mCurClient;
-            p.println("  mCurClient=" + client + " mCurSeq=" + getSequenceNumberLocked());
+            p.println("  mCurClient=" + client + " mCurSeq="
+                    + userData.mBindingController.getSequenceNumber());
             p.println("  mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
             mImeBindingState.dump(/* prefix= */ "  ", p);
-            final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+
             p.println("  mCurId=" + getCurIdLocked()
                     + " mHaveConnection=" + userData.mBindingController.hasMainConnection()
                     + " mBoundToMethod=" + mBoundToMethod + " mVisibleBound="
diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
index 825cfcb..2b19d3e 100644
--- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java
+++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
@@ -96,5 +96,10 @@
             mUserId = userId;
             mBindingController = bindingController;
         }
+
+        @Override
+        public String toString() {
+            return "UserData{" + "mUserId=" + mUserId + '}';
+        }
     }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 61054a9..4f87c83 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -593,6 +593,8 @@
 
     static final long NOTIFICATION_TTL = Duration.ofDays(3).toMillis();
 
+    static final long NOTIFICATION_MAX_AGE_AT_POST = Duration.ofDays(14).toMillis();
+
     private IActivityManager mAm;
     private ActivityTaskManagerInternal mAtm;
     private ActivityManager mActivityManager;
@@ -2637,27 +2639,48 @@
      * Cleanup broadcast receivers change listeners.
      */
     public void onDestroy() {
-        getContext().unregisterReceiver(mIntentReceiver);
-        getContext().unregisterReceiver(mPackageIntentReceiver);
-        if (Flags.allNotifsNeedTtl()) {
-            mTtlHelper.destroy();
-        } else {
-            getContext().unregisterReceiver(mNotificationTimeoutReceiver);
+        if (mIntentReceiver != null) {
+            getContext().unregisterReceiver(mIntentReceiver);
         }
-        getContext().unregisterReceiver(mRestoreReceiver);
-        getContext().unregisterReceiver(mLocaleChangeReceiver);
-
-        mSettingsObserver.destroy();
-        mRoleObserver.destroy();
+        if (mPackageIntentReceiver != null) {
+            getContext().unregisterReceiver(mPackageIntentReceiver);
+        }
+        if (Flags.allNotifsNeedTtl()) {
+            if (mTtlHelper != null) {
+                mTtlHelper.destroy();
+            }
+        } else {
+            if (mNotificationTimeoutReceiver != null) {
+                getContext().unregisterReceiver(mNotificationTimeoutReceiver);
+            }
+        }
+        if (mRestoreReceiver != null) {
+            getContext().unregisterReceiver(mRestoreReceiver);
+        }
+        if (mLocaleChangeReceiver != null) {
+            getContext().unregisterReceiver(mLocaleChangeReceiver);
+        }
+        if (mSettingsObserver != null) {
+            mSettingsObserver.destroy();
+        }
+        if (mRoleObserver != null) {
+            mRoleObserver.destroy();
+        }
         if (mShortcutHelper != null) {
             mShortcutHelper.destroy();
         }
-        mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_PREFERENCES);
-        mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES);
-        mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES);
-        mStatsManager.clearPullAtomCallback(DND_MODE_RULE);
-        mAppOps.stopWatchingMode(mAppOpsListener);
-        mAlarmManager.cancelAll();
+        if (mStatsManager != null) {
+            mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_PREFERENCES);
+            mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES);
+            mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES);
+            mStatsManager.clearPullAtomCallback(DND_MODE_RULE);
+        }
+        if (mAppOps != null) {
+            mAppOps.stopWatchingMode(mAppOpsListener);
+        }
+        if (mAlarmManager != null) {
+            mAlarmManager.cancelAll();
+        }
     }
 
     protected String[] getStringArrayResource(int key) {
@@ -7722,6 +7745,9 @@
             return true;
         }
         // Check if an app has been given system exemption
+        if (ai.uid == Process.SYSTEM_UID) {
+            return false;
+        }
         return mAppOps.checkOpNoThrow(
                 AppOpsManager.OP_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS, ai.uid,
                 ai.packageName) == MODE_ALLOWED;
@@ -8016,6 +8042,13 @@
             return false;
         }
 
+        if (Flags.rejectOldNotifications() && n.hasAppProvidedWhen() && n.getWhen() > 0
+                && (System.currentTimeMillis() - n.getWhen()) > NOTIFICATION_MAX_AGE_AT_POST) {
+            Slog.d(TAG, "Ignored enqueue for old " + n.getWhen() + " notification " + r.getKey());
+            mUsageStats.registerTooOldBlocked(r);
+            return false;
+        }
+
         return true;
     }
 
diff --git a/services/core/java/com/android/server/notification/NotificationUsageStats.java b/services/core/java/com/android/server/notification/NotificationUsageStats.java
index e960f4b..c09077e 100644
--- a/services/core/java/com/android/server/notification/NotificationUsageStats.java
+++ b/services/core/java/com/android/server/notification/NotificationUsageStats.java
@@ -257,6 +257,14 @@
         }
     }
 
+    public synchronized void registerTooOldBlocked(NotificationRecord notification) {
+        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
+        for (AggregatedStats stats : aggregatedStatsArray) {
+            stats.numTooOld++;
+        }
+        releaseAggregatedStatsLocked(aggregatedStatsArray);
+    }
+
     @GuardedBy("this")
     private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
         return getAggregatedStatsLocked(record.getSbn().getPackageName());
@@ -405,6 +413,7 @@
         public int numUndecoratedRemoteViews;
         public long mLastAccessTime;
         public int numImagesRemoved;
+        public int numTooOld;
 
         public AggregatedStats(Context context, String key) {
             this.key = key;
@@ -535,6 +544,7 @@
             maybeCount("note_over_alert_rate", (numAlertViolations - previous.numAlertViolations));
             maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
             maybeCount("note_images_removed", (numImagesRemoved - previous.numImagesRemoved));
+            maybeCount("not_too_old", (numTooOld - previous.numTooOld));
             noisyImportance.maybeCount(previous.noisyImportance);
             quietImportance.maybeCount(previous.quietImportance);
             finalImportance.maybeCount(previous.finalImportance);
@@ -570,6 +580,7 @@
             previous.numAlertViolations = numAlertViolations;
             previous.numQuotaViolations = numQuotaViolations;
             previous.numImagesRemoved = numImagesRemoved;
+            previous.numTooOld = numTooOld;
             noisyImportance.update(previous.noisyImportance);
             quietImportance.update(previous.quietImportance);
             finalImportance.update(previous.finalImportance);
@@ -679,6 +690,8 @@
             output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
             output.append(indentPlusTwo);
             output.append("numImagesRemoved=").append(numImagesRemoved).append("\n");
+            output.append(indentPlusTwo);
+            output.append("numTooOld=").append(numTooOld).append("\n");
             output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
             output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
             output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
@@ -725,6 +738,7 @@
             maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
             maybePut(dump, "numAlertViolations", numAlertViolations);
             maybePut(dump, "numImagesRemoved", numImagesRemoved);
+            maybePut(dump, "numTooOld", numTooOld);
             noisyImportance.maybePut(dump, previous.noisyImportance);
             quietImportance.maybePut(dump, previous.quietImportance);
             finalImportance.maybePut(dump, previous.finalImportance);
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 9dcca49..bf6b652 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -135,3 +135,10 @@
   description: "This flag controls which signal is used to handle a user switch system event"
   bug: "337077643"
 }
+
+flag {
+  name: "reject_old_notifications"
+  namespace: "systemui"
+  description: "This flag does not allow notifications older than 2 weeks old to be posted"
+  bug: "339833083"
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 99401a1..235e3cd 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -16,6 +16,10 @@
 
 package com.android.server.ondeviceintelligence;
 
+import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_LOADED_BUNDLE_KEY;
+import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_UNLOADED_BUNDLE_KEY;
+import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY;
+
 import static com.android.server.ondeviceintelligence.BundleUtil.sanitizeInferenceParams;
 import static com.android.server.ondeviceintelligence.BundleUtil.validatePfdReadOnly;
 import static com.android.server.ondeviceintelligence.BundleUtil.sanitizeStateParams;
@@ -41,6 +45,7 @@
 import android.app.ondeviceintelligence.OnDeviceIntelligenceException;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
@@ -105,12 +110,20 @@
     /** Handler message to {@link #resetTemporaryServices()} */
     private static final int MSG_RESET_TEMPORARY_SERVICE = 0;
 
+    /** Handler message to clean up temporary broadcast keys. */
+    private static final int MSG_RESET_BROADCAST_KEYS = 1;
+
     /** Default value in absence of {@link DeviceConfig} override. */
     private static final boolean DEFAULT_SERVICE_ENABLED = true;
     private static final String NAMESPACE_ON_DEVICE_INTELLIGENCE = "ondeviceintelligence";
 
+    private static final String SYSTEM_PACKAGE = "android";
+
+
     private final Executor resourceClosingExecutor = Executors.newCachedThreadPool();
     private final Executor callbackExecutor = Executors.newCachedThreadPool();
+    private final Executor broadcastExecutor = Executors.newCachedThreadPool();
+
 
     private final Context mContext;
     protected final Object mLock = new Object();
@@ -123,10 +136,14 @@
     @GuardedBy("mLock")
     private String[] mTemporaryServiceNames;
 
+    @GuardedBy("mLock")
+    private String[] mTemporaryBroadcastKeys;
+    @GuardedBy("mLock")
+    private String mBroadcastPackageName;
+
     /**
      * Handler used to reset the temporary service names.
      */
-    @GuardedBy("mLock")
     private Handler mTemporaryHandler;
 
     public OnDeviceIntelligenceManagerService(Context context) {
@@ -482,6 +499,8 @@
                                     ensureRemoteIntelligenceServiceInitialized();
                                     mRemoteOnDeviceIntelligenceService.run(
                                             IOnDeviceIntelligenceService::notifyInferenceServiceConnected);
+                                    broadcastExecutor.execute(
+                                            () -> registerModelLoadingBroadcasts(service));
                                     service.registerRemoteStorageService(
                                             getIRemoteStorageService());
                                 } catch (RemoteException ex) {
@@ -493,6 +512,56 @@
         }
     }
 
+    private void registerModelLoadingBroadcasts(IOnDeviceSandboxedInferenceService service) {
+        String[] modelBroadcastKeys;
+        try {
+            modelBroadcastKeys = getBroadcastKeys();
+        } catch (Resources.NotFoundException e) {
+            Slog.d(TAG, "Skipping model broadcasts as broadcast intents configured.");
+            return;
+        }
+
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY, true);
+        try {
+            service.updateProcessingState(bundle, new IProcessingUpdateStatusCallback.Stub() {
+                @Override
+                public void onSuccess(PersistableBundle statusParams) {
+                    Binder.clearCallingIdentity();
+                    synchronized (mLock) {
+                        if (statusParams.containsKey(MODEL_LOADED_BUNDLE_KEY)) {
+                            String modelLoadedBroadcastKey = modelBroadcastKeys[0];
+                            if (modelLoadedBroadcastKey != null
+                                    && !modelLoadedBroadcastKey.isEmpty()) {
+                                final Intent intent = new Intent(modelLoadedBroadcastKey);
+                                intent.setPackage(mBroadcastPackageName);
+                                mContext.sendBroadcast(intent,
+                                        Manifest.permission.USE_ON_DEVICE_INTELLIGENCE);
+                            }
+                        } else if (statusParams.containsKey(MODEL_UNLOADED_BUNDLE_KEY)) {
+                            String modelUnloadedBroadcastKey = modelBroadcastKeys[1];
+                            if (modelUnloadedBroadcastKey != null
+                                    && !modelUnloadedBroadcastKey.isEmpty()) {
+                                final Intent intent = new Intent(modelUnloadedBroadcastKey);
+                                intent.setPackage(mBroadcastPackageName);
+                                mContext.sendBroadcast(intent,
+                                        Manifest.permission.USE_ON_DEVICE_INTELLIGENCE);
+                            }
+                        }
+                    }
+                }
+
+                @Override
+                public void onFailure(int errorCode, String errorMessage) {
+                    Slog.e(TAG, "Failed to register model loading callback with status code",
+                            new OnDeviceIntelligenceException(errorCode, errorMessage));
+                }
+            });
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to register model loading callback with status code", e);
+        }
+    }
+
     @NonNull
     private IRemoteStorageService.Stub getIRemoteStorageService() {
         return new IRemoteStorageService.Stub() {
@@ -629,6 +698,20 @@
                         R.string.config_defaultOnDeviceSandboxedInferenceService)};
     }
 
+    protected String[] getBroadcastKeys() throws Resources.NotFoundException {
+        // TODO 329240495 : Consider a small class with explicit field names for the two services
+        synchronized (mLock) {
+            if (mTemporaryBroadcastKeys != null && mTemporaryBroadcastKeys.length == 2) {
+                return mTemporaryBroadcastKeys;
+            }
+        }
+
+        return new String[]{mContext.getResources().getString(
+                R.string.config_onDeviceIntelligenceModelLoadedBroadcastKey),
+                mContext.getResources().getString(
+                        R.string.config_onDeviceIntelligenceModelUnloadedBroadcastKey)};
+    }
+
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
     public void setTemporaryServices(@NonNull String[] componentNames, int durationMs) {
         Objects.requireNonNull(componentNames);
@@ -645,25 +728,26 @@
                 mRemoteOnDeviceIntelligenceService.unbind();
                 mRemoteOnDeviceIntelligenceService = null;
             }
-            if (mTemporaryHandler == null) {
-                mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
-                    @Override
-                    public void handleMessage(Message msg) {
-                        if (msg.what == MSG_RESET_TEMPORARY_SERVICE) {
-                            synchronized (mLock) {
-                                resetTemporaryServices();
-                            }
-                        } else {
-                            Slog.wtf(TAG, "invalid handler msg: " + msg);
-                        }
-                    }
-                };
-            } else {
-                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
-            }
 
             if (durationMs != -1) {
-                mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs);
+                getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE,
+                        durationMs);
+            }
+        }
+    }
+
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void setModelBroadcastKeys(@NonNull String[] broadcastKeys, String receiverPackageName,
+            int durationMs) {
+        Objects.requireNonNull(broadcastKeys);
+        enforceShellOnly(Binder.getCallingUid(), "setModelBroadcastKeys");
+        mContext.enforceCallingPermission(
+                Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+        synchronized (mLock) {
+            mTemporaryBroadcastKeys = broadcastKeys;
+            mBroadcastPackageName = receiverPackageName;
+            if (durationMs != -1) {
+                getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_BROADCAST_KEYS, durationMs);
             }
         }
     }
@@ -751,4 +835,28 @@
             }
         }
     }
+
+    private synchronized Handler getTemporaryHandler() {
+        if (mTemporaryHandler == null) {
+            mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
+                @Override
+                public void handleMessage(Message msg) {
+                    if (msg.what == MSG_RESET_TEMPORARY_SERVICE) {
+                        synchronized (mLock) {
+                            resetTemporaryServices();
+                        }
+                    } else if (msg.what == MSG_RESET_BROADCAST_KEYS) {
+                        synchronized (mLock) {
+                            mTemporaryBroadcastKeys = null;
+                            mBroadcastPackageName = SYSTEM_PACKAGE;
+                        }
+                    } else {
+                        Slog.wtf(TAG, "invalid handler msg: " + msg);
+                    }
+                }
+            };
+        }
+
+        return mTemporaryHandler;
+    }
 }
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java
index a76d8a3..5744b5c 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java
@@ -43,6 +43,8 @@
                 return setTemporaryServices();
             case "get-services":
                 return getConfiguredServices();
+            case "set-model-broadcasts":
+                return setBroadcastKeys();
             default:
                 return handleDefaultCommands(cmd);
         }
@@ -62,12 +64,18 @@
         pw.println("    To reset, call without any arguments.");
 
         pw.println("  get-services To get the names of services that are currently being used.");
+        pw.println(
+                "  set-model-broadcasts [ModelLoadedBroadcastKey] [ModelUnloadedBroadcastKey] "
+                        + "[ReceiverPackageName] "
+                        + "[DURATION] To set the names of broadcast intent keys that are to be "
+                        + "emitted for cts tests.");
     }
 
     private int setTemporaryServices() {
         final PrintWriter out = getOutPrintWriter();
         final String intelligenceServiceName = getNextArg();
         final String inferenceServiceName = getNextArg();
+
         if (getRemainingArgsCount() == 0 && intelligenceServiceName == null
                 && inferenceServiceName == null) {
             mService.resetTemporaryServices();
@@ -79,7 +87,8 @@
         Objects.requireNonNull(inferenceServiceName);
         final int duration = Integer.parseInt(getNextArgRequired());
         mService.setTemporaryServices(
-                new String[]{intelligenceServiceName, inferenceServiceName}, duration);
+                new String[]{intelligenceServiceName, inferenceServiceName},
+                duration);
         out.println("OnDeviceIntelligenceService temporarily set to " + intelligenceServiceName
                 + " \n and \n OnDeviceTrustedInferenceService set to " + inferenceServiceName
                 + " for " + duration + "ms");
@@ -93,4 +102,22 @@
                 + " \n and \n OnDeviceTrustedInferenceService set to : " + services[1]);
         return 0;
     }
+
+    private int setBroadcastKeys() {
+        final PrintWriter out = getOutPrintWriter();
+        final String modelLoadedKey = getNextArgRequired();
+        final String modelUnloadedKey = getNextArgRequired();
+        final String receiverPackageName = getNextArg();
+
+        final int duration = Integer.parseInt(getNextArgRequired());
+        mService.setModelBroadcastKeys(
+                new String[]{modelLoadedKey, modelUnloadedKey}, receiverPackageName, duration);
+        out.println("OnDeviceIntelligence Model Loading broadcast keys temporarily set to "
+                + modelLoadedKey
+                + " \n and \n OnDeviceTrustedInferenceService set to " + modelUnloadedKey
+                + "\n and Package name set to : " + receiverPackageName
+                + " for " + duration + "ms");
+        return 0;
+    }
+
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java
index 9ba88aa..fe774aa 100644
--- a/services/core/java/com/android/server/pm/AppDataHelper.java
+++ b/services/core/java/com/android/server/pm/AppDataHelper.java
@@ -504,9 +504,12 @@
         } else {
             storageFlags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
         }
-        List<String> deferPackages = reconcileAppsDataLI(StorageManager.UUID_PRIVATE_INTERNAL,
-                UserHandle.USER_SYSTEM, storageFlags, true /* migrateAppData */,
-                true /* onlyCoreApps */);
+        final List<String> deferPackages;
+        synchronized (mPm.mInstallLock) {
+           deferPackages = reconcileAppsDataLI(StorageManager.UUID_PRIVATE_INTERNAL,
+                    UserHandle.USER_SYSTEM, storageFlags, true /* migrateAppData */,
+                    true /* onlyCoreApps */);
+        }
         Future<?> prepareAppDataFuture = SystemServerInitThreadPool.submit(() -> {
             TimingsTraceLog traceLog = new TimingsTraceLog("SystemServerTimingAsync",
                     Trace.TRACE_TAG_PACKAGE_MANAGER);
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 19a0ba7..908b47d 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -985,13 +985,13 @@
     }
 
     void installPackagesTraced(List<InstallRequest> requests) {
-        synchronized (mPm.mInstallLock) {
-            try {
-                Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackages");
-                installPackagesLI(requests);
-            } finally {
-                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
-            }
+        mPm.mInstallLock.lock();
+        try {
+            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackages");
+            installPackagesLI(requests);
+        } finally {
+            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+            mPm.mInstallLock.unlock();
         }
     }
 
@@ -2590,22 +2590,30 @@
             final boolean performDexopt = DexOptHelper.shouldPerformDexopt(installRequest,
                     dexoptOptions, mContext);
             if (performDexopt) {
-                Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
+                // dexopt can take long, and ArtService doesn't require installd, so we release
+                // the lock here and re-acquire the lock after dexopt is finished.
+                mPm.mInstallLock.unlock();
+                try {
+                    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
 
-                // This mirrors logic from commitReconciledScanResultLocked, where the library files
-                // needed for dexopt are assigned.
-                PackageSetting realPkgSetting = installRequest.getRealPackageSetting();
+                    // This mirrors logic from commitReconciledScanResultLocked, where the library
+                    // files needed for dexopt are assigned.
+                    PackageSetting realPkgSetting = installRequest.getRealPackageSetting();
 
-                // Unfortunately, the updated system app flag is only tracked on this PackageSetting
-                boolean isUpdatedSystemApp =
-                        installRequest.getScannedPackageSetting().isUpdatedSystemApp();
+                    // Unfortunately, the updated system app flag is only tracked on this
+                    // PackageSetting
+                    boolean isUpdatedSystemApp =
+                            installRequest.getScannedPackageSetting().isUpdatedSystemApp();
 
-                realPkgSetting.getPkgState().setUpdatedSystemApp(isUpdatedSystemApp);
+                    realPkgSetting.getPkgState().setUpdatedSystemApp(isUpdatedSystemApp);
 
-                DexoptResult dexOptResult =
-                        DexOptHelper.dexoptPackageUsingArtService(installRequest, dexoptOptions);
-                installRequest.onDexoptFinished(dexOptResult);
-                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+                    DexoptResult dexOptResult = DexOptHelper.dexoptPackageUsingArtService(
+                            installRequest, dexoptOptions);
+                    installRequest.onDexoptFinished(dexOptResult);
+                    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+                } finally {
+                    mPm.mInstallLock.lock();
+                }
             }
         }
         PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental(
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index ae485ed..121cf3f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -626,7 +626,7 @@
     // Lock for state used when installing and doing other long running
     // operations.  Methods that must be called with this lock held have
     // the suffix "LI".
-    final Object mInstallLock;
+    final PackageManagerTracedLock mInstallLock;
 
     // ----------------------------------------------------------------
 
@@ -1692,8 +1692,8 @@
         final TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG + "Timing",
                 Trace.TRACE_TAG_PACKAGE_MANAGER);
         t.traceBegin("create package manager");
-        final PackageManagerTracedLock lock = new PackageManagerTracedLock();
-        final Object installLock = new Object();
+        final PackageManagerTracedLock lock = new PackageManagerTracedLock("mLock");
+        final PackageManagerTracedLock installLock = new PackageManagerTracedLock("mInstallLock");
 
         HandlerThread backgroundThread = new ServiceThread("PackageManagerBg",
                 Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
index 83f3b16..ae2eaeb 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
@@ -86,7 +86,7 @@
     private final Context mContext;
     private final PackageManagerTracedLock mLock;
     private final Installer mInstaller;
-    private final Object mInstallLock;
+    private final PackageManagerTracedLock mInstallLock;
     private final Handler mBackgroundHandler;
     private final Executor mBackgroundExecutor;
     private final List<ScanPartition> mSystemPartitions;
@@ -144,7 +144,7 @@
     private final Singleton<PackageMonitorCallbackHelper> mPackageMonitorCallbackHelper;
 
     PackageManagerServiceInjector(Context context, PackageManagerTracedLock lock,
-            Installer installer, Object installLock, PackageAbiHelper abiHelper,
+            Installer installer, PackageManagerTracedLock installLock, PackageAbiHelper abiHelper,
             Handler backgroundHandler,
             List<ScanPartition> systemPartitions,
             Producer<ComponentResolver> componentResolverProducer,
@@ -254,7 +254,7 @@
         return mAbiHelper;
     }
 
-    public Object getInstallLock() {
+    public PackageManagerTracedLock getInstallLock() {
         return mInstallLock;
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerTracedLock.java b/services/core/java/com/android/server/pm/PackageManagerTracedLock.java
index 75e1803f..303b8b9 100644
--- a/services/core/java/com/android/server/pm/PackageManagerTracedLock.java
+++ b/services/core/java/com/android/server/pm/PackageManagerTracedLock.java
@@ -16,6 +16,9 @@
 
 package com.android.server.pm;
 
+import android.annotation.Nullable;
+import android.util.Slog;
+
 import java.util.concurrent.locks.ReentrantLock;
 
 /**
@@ -23,4 +26,31 @@
  * injection, similar to {@link ActivityManagerGlobalLock}.
  */
 public class PackageManagerTracedLock extends ReentrantLock {
+    private static final String TAG = "PackageManagerTracedLock";
+    private static final boolean DEBUG = false;
+    @Nullable private final String mLockName;
+
+    public PackageManagerTracedLock(@Nullable String lockName) {
+        mLockName = lockName;
+    }
+
+    public PackageManagerTracedLock() {
+        this(null);
+    }
+
+    @Override
+    public void lock() {
+        super.lock();
+        if (DEBUG && mLockName != null) {
+            Slog.i(TAG, "locked " + mLockName);
+        }
+    }
+
+    @Override
+    public void unlock() {
+        super.unlock();
+        if (DEBUG && mLockName != null) {
+            Slog.i(TAG, "unlocked " + mLockName);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java
index 1d41401..ef32485 100644
--- a/services/core/java/com/android/server/pm/UserDataPreparer.java
+++ b/services/core/java/com/android/server/pm/UserDataPreparer.java
@@ -52,11 +52,11 @@
     private static final String TAG = "UserDataPreparer";
     private static final String XATTR_SERIAL = "user.serial";
 
-    private final Object mInstallLock;
+    private final PackageManagerTracedLock mInstallLock;
     private final Context mContext;
     private final Installer mInstaller;
 
-    UserDataPreparer(Installer installer, Object installLock, Context context) {
+    UserDataPreparer(Installer installer, PackageManagerTracedLock installLock, Context context) {
         mInstallLock = installLock;
         mContext = context;
         mInstaller = installer;
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index cdd456c..4264e91 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -950,7 +950,7 @@
 
         if (mBar != null) {
             try {
-                mBar.togglePanel();
+                mBar.toggleNotificationsPanel();
             } catch (RemoteException ex) {
             }
         }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 9e16b8a..57827c5 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -989,6 +989,10 @@
         }
     }
 
+    boolean isLetterboxEducationEnabled() {
+        return mLetterboxConfiguration.getIsEducationEnabled();
+    }
+
     /**
      * Whether we use split screen aspect ratio for the activity when camera compat treatment
      * is active because the corresponding config is enabled and activity supports resizing.
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 6dec712..72f592b 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -64,7 +64,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.ArraySet;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 8bd7b5f..8defec3 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3448,6 +3448,8 @@
         // Whether the direct top activity is eligible for letterbox education.
         appCompatTaskInfo.topActivityEligibleForLetterboxEducation = isTopActivityResumed
                 && top.isEligibleForLetterboxEducation();
+        appCompatTaskInfo.isLetterboxEducationEnabled = top != null
+                && top.mLetterboxUiController.isLetterboxEducationEnabled();
         // Whether the direct top activity requested showing camera compat control.
         appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = isTopActivityResumed
                 ? top.getCameraCompatControlState()
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index f75803f..2b93d21 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -339,6 +339,7 @@
 import android.app.admin.ManagedSubscriptionsPolicy;
 import android.app.admin.NetworkEvent;
 import android.app.admin.PackagePolicy;
+import android.app.admin.PackageSetPolicyValue;
 import android.app.admin.ParcelableGranteeMap;
 import android.app.admin.ParcelableResource;
 import android.app.admin.PasswordMetrics;
@@ -349,7 +350,6 @@
 import android.app.admin.PreferentialNetworkServiceConfig;
 import android.app.admin.SecurityLog;
 import android.app.admin.SecurityLog.SecurityEvent;
-import android.app.admin.PackageSetPolicyValue;
 import android.app.admin.StartInstallingUpdateCallback;
 import android.app.admin.SystemUpdateInfo;
 import android.app.admin.SystemUpdatePolicy;
@@ -2718,6 +2718,7 @@
                     mDevicePolicyEngine.getResolvedPolicy(
                             PolicyDefinition.SECURITY_LOGGING, UserHandle.USER_ALL));
             setLoggingConfiguration(securityLoggingEnabled, auditLoggingEnabled);
+            mInjector.runCryptoSelfTest();
         } else {
             synchronized (getLockObject()) {
                 mSecurityLogMonitor.start(getSecurityLoggingEnabledUser());
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index 9e4f821..d307200 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -1276,7 +1276,23 @@
                     packageName,
                     permissionName
                 )
-            else -> permissionAllowlist.getSignatureAppAllowlistState(packageName, permissionName)
+            else ->
+                permissionAllowlist.getProductSignatureAppAllowlistState(
+                    packageName,
+                    permissionName
+                )
+                    ?: permissionAllowlist.getVendorSignatureAppAllowlistState(
+                        packageName,
+                        permissionName
+                    )
+                    ?: permissionAllowlist.getSystemExtSignatureAppAllowlistState(
+                        packageName,
+                        permissionName
+                    )
+                    ?: permissionAllowlist.getSignatureAppAllowlistState(
+                        packageName,
+                        permissionName
+                    )
         }
     }
 
diff --git a/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java b/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java
index 6a82f16..3e87c6f 100644
--- a/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -543,6 +544,18 @@
         return future.get();
     }
 
+    @Test
+    public void onBindingDied_referenceLost_doesNotThrow() {
+        TransportConnection.TransportConnectionMonitor transportConnectionMonitor =
+                new TransportConnection.TransportConnectionMonitor(
+                        mContext, /* transportConnection= */ null);
+        doThrow(new IllegalArgumentException("Service not registered")).when(
+                mContext).unbindService(any());
+
+        // Test no exception is thrown
+        transportConnectionMonitor.onBindingDied(mTransportComponent);
+    }
+
     private ServiceConnection verifyBindServiceAsUserAndCaptureServiceConnection(Context context) {
         ArgumentCaptor<ServiceConnection> connectionCaptor =
                 ArgumentCaptor.forClass(ServiceConnection.class);
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
index 9e11fa2..e545a49 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
@@ -71,7 +71,7 @@
     @Mock
     private Installer mInstaller;
 
-    private Object mInstallLock;
+    private PackageManagerTracedLock mInstallLock;
 
     @Before
     public void setup() {
@@ -79,7 +79,7 @@
         TEST_USER.serialNumber = TEST_USER_SERIAL;
         Context ctx = InstrumentationRegistry.getContext();
         FileUtils.deleteContents(ctx.getCacheDir());
-        mInstallLock = new Object();
+        mInstallLock = new PackageManagerTracedLock();
         MockitoAnnotations.initMocks(this);
         mUserDataPreparer = new TestUserDataPreparer(mInstaller, mInstallLock, mContextMock,
                 ctx.getCacheDir());
@@ -238,8 +238,8 @@
     private static class TestUserDataPreparer extends UserDataPreparer {
         File testDir;
 
-        TestUserDataPreparer(Installer installer, Object installLock, Context context,
-                File testDir) {
+        TestUserDataPreparer(Installer installer, PackageManagerTracedLock installLock,
+                Context context, File testDir) {
             super(installer, installLock, context);
             this.testDir = testDir;
         }
diff --git a/services/tests/apexsystemservices/OWNERS b/services/tests/apexsystemservices/OWNERS
index 0295b9e..8b6675a 100644
--- a/services/tests/apexsystemservices/OWNERS
+++ b/services/tests/apexsystemservices/OWNERS
@@ -1,4 +1 @@
-omakoto@google.com
-satayev@google.com
-
 include platform/packages/modules/common:/OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
index c9aab53..396edae 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
@@ -186,7 +186,7 @@
 
     class Mocks {
         val lock = PackageManagerTracedLock()
-        val installLock = Any()
+        val installLock = PackageManagerTracedLock()
         val injector: PackageManagerServiceInjector = mock()
         val systemWrapper: PackageManagerServiceInjector.SystemWrapper = mock()
         val context: Context = mock()
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 2d672b8..200952c 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -112,6 +112,7 @@
 import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER;
 import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER;
 import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL;
+import static com.android.server.notification.Flags.FLAG_REJECT_OLD_NOTIFICATIONS;
 import static com.android.server.notification.NotificationManagerService.BITMAP_DURATION;
 import static com.android.server.notification.NotificationManagerService.DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE;
 import static com.android.server.notification.NotificationManagerService.NOTIFICATION_TTL;
@@ -339,6 +340,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -909,7 +911,9 @@
         }
 
         mService.clearNotifications();
-        TestableLooper.get(this).processAllMessages();
+        if (mTestableLooper != null) {
+            mTestableLooper.processAllMessages();
+        }
 
         try {
             mService.onDestroy();
@@ -920,14 +924,16 @@
 
         InstrumentationRegistry.getInstrumentation()
                 .getUiAutomation().dropShellPermissionIdentity();
-        // Remove scheduled messages that would be processed when the test is already done, and
-        // could cause issues, for example, messages that remove/cancel shown toasts (this causes
-        // problematic interactions with mocks when they're no longer working as expected).
-        mWorkerHandler.removeCallbacksAndMessages(null);
+        if (mWorkerHandler != null) {
+            // Remove scheduled messages that would be processed when the test is already done, and
+            // could cause issues, for example, messages that remove/cancel shown toasts (this causes
+            // problematic interactions with mocks when they're no longer working as expected).
+            mWorkerHandler.removeCallbacksAndMessages(null);
+        }
 
-        if (TestableLooper.get(this) != null) {
+        if (mTestableLooper != null) {
             // Must remove static reference to this test object to prevent leak (b/261039202)
-            TestableLooper.remove(this);
+            mTestableLooper.remove(this);
         }
     }
 
@@ -1009,7 +1015,9 @@
     }
 
     public void waitForIdle() {
-        mTestableLooper.processAllMessages();
+        if (mTestableLooper != null) {
+            mTestableLooper.processAllMessages();
+        }
     }
 
     private void setUpPrefsForBubbles(String pkg, int uid, boolean globalEnabled,
@@ -1302,6 +1310,106 @@
         return nrSummary;
     }
 
+    private NotificationRecord createAndPostCallStyleNotification(String packageName,
+            UserHandle userHandle, String testName) throws Exception {
+        Person person = new Person.Builder().setName("caller").build();
+        Notification.Builder nb = new Notification.Builder(mContext,
+                mTestNotificationChannel.getId())
+                .setFlag(FLAG_USER_INITIATED_JOB, true)
+                .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent))
+                .setSmallIcon(android.R.drawable.sym_def_app_icon);
+        StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, 1,
+                testName, mUid, 0, nb.build(), userHandle, null, 0);
+        NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+
+        mService.addEnqueuedNotification(r);
+        mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(),
+                r.getUid(), mPostNotificationTrackerFactory.newTracker(null)).run();
+        waitForIdle();
+
+        return mService.findNotificationLocked(
+                packageName, r.getSbn().getTag(), r.getSbn().getId(), r.getSbn().getUserId());
+    }
+
+    private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName)
+            throws RemoteException {
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, testName, mUid, 0,
+                nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+
+        mBinderService.enqueueNotificationWithTag(mPkg, mPkg, sbn.getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+
+        return mService.findNotificationLocked(
+                mPkg, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getUserId());
+    }
+
+    private static <T extends Parcelable> T parcelAndUnparcel(T source,
+            Parcelable.Creator<T> creator) {
+        Parcel parcel = Parcel.obtain();
+        source.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        return creator.createFromParcel(parcel);
+    }
+
+    private PendingIntent createPendingIntent(String action) {
+        return PendingIntent.getActivity(mContext, 0,
+                new Intent(action).setPackage(mContext.getPackageName()),
+                PendingIntent.FLAG_MUTABLE);
+    }
+
+    private void allowTestPackageToToast() throws Exception {
+        assertWithMessage("toast queue").that(mService.mToastQueue).isEmpty();
+        mService.isSystemUid = false;
+        mService.isSystemAppId = false;
+        setToastRateIsWithinQuota(true);
+        setIfPackageHasPermissionToAvoidToastRateLimiting(TEST_PACKAGE, false);
+        // package is not suspended
+        when(mPackageManager.isPackageSuspendedForUser(TEST_PACKAGE, mUserId))
+                .thenReturn(false);
+    }
+
+    private boolean enqueueToast(String testPackage, ITransientNotification callback)
+            throws RemoteException {
+        return enqueueToast((INotificationManager) mService.mService, testPackage, new Binder(),
+                callback);
+    }
+
+    private boolean enqueueToast(INotificationManager service, String testPackage,
+            IBinder token, ITransientNotification callback) throws RemoteException {
+        return service.enqueueToast(testPackage, token, callback, TOAST_DURATION, /* isUiContext= */
+                true, DEFAULT_DISPLAY);
+    }
+
+    private boolean enqueueTextToast(String testPackage, CharSequence text) throws RemoteException {
+        return enqueueTextToast(testPackage, text, /* isUiContext= */ true, DEFAULT_DISPLAY);
+    }
+
+    private boolean enqueueTextToast(String testPackage, CharSequence text, boolean isUiContext,
+            int displayId) throws RemoteException {
+        return ((INotificationManager) mService.mService).enqueueTextToast(testPackage,
+                new Binder(), text, TOAST_DURATION, isUiContext, displayId,
+                /* textCallback= */ null);
+    }
+
+    private void mockIsVisibleBackgroundUsersSupported(boolean supported) {
+        when(mUm.isVisibleBackgroundUsersSupported()).thenReturn(supported);
+    }
+
+    private void mockIsUserVisible(int displayId, boolean visible) {
+        when(mUmInternal.isUserVisible(mUserId, displayId)).thenReturn(visible);
+    }
+
+    private void mockDisplayAssignedToUser(int displayId) {
+        when(mUmInternal.getMainDisplayAssignedToUser(mUserId)).thenReturn(displayId);
+    }
+
+    private void verifyToastShownForTestPackage(String text, int displayId) {
+        verify(mStatusBar).showToast(eq(mUid), eq(TEST_PACKAGE), any(), eq(text), any(),
+                eq(TOAST_DURATION), any(), eq(displayId));
+    }
+
     @Test
     @DisableFlags(FLAG_ALL_NOTIFS_NEED_TTL)
     public void testLimitTimeOutBroadcast() {
@@ -14069,11 +14177,12 @@
     public void enqueueUpdate_whenBelowMaxEnqueueRate_accepts() throws Exception {
         // Post the first version.
         Notification original = generateNotificationRecord(null).getNotification();
-        original.when = 111;
+        original.when = System.currentTimeMillis();
         mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 0, original, mUserId);
         waitForIdle();
         assertThat(mService.mNotificationList).hasSize(1);
-        assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(111);
+        assertThat(mService.mNotificationList.get(0).getNotification().when)
+                .isEqualTo(original.when);
 
         reset(mUsageStats);
         when(mUsageStats.getAppEnqueueRate(eq(mPkg)))
@@ -14081,7 +14190,7 @@
 
         // Post the update.
         Notification update = generateNotificationRecord(null).getNotification();
-        update.when = 222;
+        update.when = System.currentTimeMillis() + 111;
         mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 0, update, mUserId);
         waitForIdle();
 
@@ -14090,18 +14199,19 @@
         verify(mUsageStats, never()).registerPostedByApp(any());
         verify(mUsageStats).registerUpdatedByApp(any(), any());
         assertThat(mService.mNotificationList).hasSize(1);
-        assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(222);
+        assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(update.when);
     }
 
     @Test
     public void enqueueUpdate_whenAboveMaxEnqueueRate_rejects() throws Exception {
         // Post the first version.
         Notification original = generateNotificationRecord(null).getNotification();
-        original.when = 111;
+        original.when = System.currentTimeMillis();
         mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 0, original, mUserId);
         waitForIdle();
         assertThat(mService.mNotificationList).hasSize(1);
-        assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(111);
+        assertThat(mService.mNotificationList.get(0).getNotification().when)
+                .isEqualTo(original.when);
 
         reset(mUsageStats);
         when(mUsageStats.getAppEnqueueRate(eq(mPkg)))
@@ -14109,7 +14219,7 @@
 
         // Post the update.
         Notification update = generateNotificationRecord(null).getNotification();
-        update.when = 222;
+        update.when = System.currentTimeMillis() + 111;
         mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 0, update, mUserId);
         waitForIdle();
 
@@ -14118,7 +14228,8 @@
         verify(mUsageStats, never()).registerPostedByApp(any());
         verify(mUsageStats, never()).registerUpdatedByApp(any(), any());
         assertThat(mService.mNotificationList).hasSize(1);
-        assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(111); // old
+        assertThat(mService.mNotificationList.get(0).getNotification().when)
+                .isEqualTo(original.when); // old
     }
 
     @Test
@@ -15483,103 +15594,48 @@
         assertThat(n.getTimeoutAfter()).isEqualTo(20);
     }
 
-    private NotificationRecord createAndPostCallStyleNotification(String packageName,
-            UserHandle userHandle, String testName) throws Exception {
-        Person person = new Person.Builder().setName("caller").build();
-        Notification.Builder nb = new Notification.Builder(mContext,
-                mTestNotificationChannel.getId())
-                .setFlag(FLAG_USER_INITIATED_JOB, true)
-                .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent))
-                .setSmallIcon(android.R.drawable.sym_def_app_icon);
-        StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, 1,
-                testName, mUid, 0, nb.build(), userHandle, null, 0);
+    @Test
+    @EnableFlags(FLAG_REJECT_OLD_NOTIFICATIONS)
+    public void testRejectOldNotification_oldWhen() throws Exception {
+        Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setWhen(System.currentTimeMillis() - Duration.ofDays(15).toMillis())
+                .build();
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, null, mUid, 0,
+                n, UserHandle.getUserHandleForUid(mUid), null, 0);
         NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
 
-        mService.addEnqueuedNotification(r);
-        mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(),
-                r.getUid(), mPostNotificationTrackerFactory.newTracker(null)).run();
-        waitForIdle();
-
-        return mService.findNotificationLocked(
-                packageName, r.getSbn().getTag(), r.getSbn().getId(), r.getSbn().getUserId());
+        assertThat(mService.checkDisqualifyingFeatures(mUserId, mUid, 0, null, r, false, false))
+                .isFalse();
     }
 
-    private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName)
-            throws RemoteException {
-        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, testName, mUid, 0,
-                nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
-        NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+    @Test
+    @EnableFlags(FLAG_REJECT_OLD_NOTIFICATIONS)
+    public void testRejectOldNotification_mediumOldWhen() throws Exception {
+        Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setWhen(System.currentTimeMillis() - Duration.ofDays(13).toMillis())
+                .build();
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, null, mUid, 0,
+                n, UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
 
-        mBinderService.enqueueNotificationWithTag(mPkg, mPkg, sbn.getTag(),
-                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
-        waitForIdle();
-
-        return mService.findNotificationLocked(
-                mPkg, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getUserId());
+        assertThat(mService.checkDisqualifyingFeatures(mUserId, mUid, 0, null, r, false, false))
+                .isTrue();
     }
 
-    private static <T extends Parcelable> T parcelAndUnparcel(T source,
-            Parcelable.Creator<T> creator) {
-        Parcel parcel = Parcel.obtain();
-        source.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        return creator.createFromParcel(parcel);
-    }
+    @Test
+    @EnableFlags(FLAG_REJECT_OLD_NOTIFICATIONS)
+    public void testRejectOldNotification_zeroWhen() throws Exception {
+        Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setWhen(0)
+                .build();
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, null, mUid, 0,
+                n, UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
 
-    private PendingIntent createPendingIntent(String action) {
-        return PendingIntent.getActivity(mContext, 0,
-                new Intent(action).setPackage(mContext.getPackageName()),
-                PendingIntent.FLAG_MUTABLE);
-    }
-
-    private void allowTestPackageToToast() throws Exception {
-        assertWithMessage("toast queue").that(mService.mToastQueue).isEmpty();
-        mService.isSystemUid = false;
-        mService.isSystemAppId = false;
-        setToastRateIsWithinQuota(true);
-        setIfPackageHasPermissionToAvoidToastRateLimiting(TEST_PACKAGE, false);
-        // package is not suspended
-        when(mPackageManager.isPackageSuspendedForUser(TEST_PACKAGE, mUserId))
-                .thenReturn(false);
-    }
-
-    private boolean enqueueToast(String testPackage, ITransientNotification callback)
-            throws RemoteException {
-        return enqueueToast((INotificationManager) mService.mService, testPackage, new Binder(),
-                callback);
-    }
-
-    private boolean enqueueToast(INotificationManager service, String testPackage,
-            IBinder token, ITransientNotification callback) throws RemoteException {
-        return service.enqueueToast(testPackage, token, callback, TOAST_DURATION, /* isUiContext= */
-                true, DEFAULT_DISPLAY);
-    }
-
-    private boolean enqueueTextToast(String testPackage, CharSequence text) throws RemoteException {
-        return enqueueTextToast(testPackage, text, /* isUiContext= */ true, DEFAULT_DISPLAY);
-    }
-
-    private boolean enqueueTextToast(String testPackage, CharSequence text, boolean isUiContext,
-            int displayId) throws RemoteException {
-        return ((INotificationManager) mService.mService).enqueueTextToast(testPackage,
-                new Binder(), text, TOAST_DURATION, isUiContext, displayId,
-                /* textCallback= */ null);
-    }
-
-    private void mockIsVisibleBackgroundUsersSupported(boolean supported) {
-        when(mUm.isVisibleBackgroundUsersSupported()).thenReturn(supported);
-    }
-
-    private void mockIsUserVisible(int displayId, boolean visible) {
-        when(mUmInternal.isUserVisible(mUserId, displayId)).thenReturn(visible);
-    }
-
-    private void mockDisplayAssignedToUser(int displayId) {
-        when(mUmInternal.getMainDisplayAssignedToUser(mUserId)).thenReturn(displayId);
-    }
-
-    private void verifyToastShownForTestPackage(String text, int displayId) {
-        verify(mStatusBar).showToast(eq(mUid), eq(TEST_PACKAGE), any(), eq(text), any(),
-                eq(TOAST_DURATION), any(), eq(displayId));
+        assertThat(mService.checkDisqualifyingFeatures(mUserId, mUid, 0, null, r, false, false))
+                .isTrue();
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index a60d243..1195c93 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -1623,6 +1623,12 @@
         assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
     }
 
+    @Test
+    public void testIsLetterboxEducationEnabled() {
+        mController.isLetterboxEducationEnabled();
+        verify(mLetterboxConfiguration).getIsEducationEnabled();
+    }
+
     private void mockThatProperty(String propertyName, boolean value) throws Exception {
         Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
                 /* className */ "");
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 03ba8fa..dbe4f27 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -13114,39 +13114,41 @@
     })
     @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS)
     public @Nullable ServiceState getServiceState(@IncludeLocationData int includeLocationData) {
-        return getServiceStateForSubscriber(getSubId(),
+        return getServiceStateForSlot(SubscriptionManager.getSlotIndex(getSubId()),
                 includeLocationData != INCLUDE_LOCATION_DATA_FINE,
                 includeLocationData == INCLUDE_LOCATION_DATA_NONE);
     }
 
     /**
-     * Returns the service state information on specified subscription. Callers require
-     * either READ_PRIVILEGED_PHONE_STATE or READ_PHONE_STATE to retrieve the information.
+     * Returns the service state information on specified SIM slot.
      *
-     * May return {@code null} when the subscription is inactive or when there was an error
+     * May return {@code null} when the {@code slotIndex} is invalid or when there was an error
      * communicating with the phone process.
+     *
+     * @param slotIndex of phone whose service state is returned
      * @param renounceFineLocationAccess Set this to true if the caller would not like to receive
      * location related information which will be sent if the caller already possess
      * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and do not renounce the permission
      * @param renounceCoarseLocationAccess Set this to true if the caller would not like to
      * receive location related information which will be sent if the caller already possess
      * {@link Manifest.permission#ACCESS_COARSE_LOCATION} and do not renounce the permissions.
+     * @return Service state on specified SIM slot.
      */
-    private ServiceState getServiceStateForSubscriber(int subId,
-            boolean renounceFineLocationAccess,
+    private ServiceState getServiceStateForSlot(int slotIndex, boolean renounceFineLocationAccess,
             boolean renounceCoarseLocationAccess) {
         try {
             ITelephony service = getITelephony();
             if (service != null) {
-                return service.getServiceStateForSubscriber(subId, renounceFineLocationAccess,
-                        renounceCoarseLocationAccess, getOpPackageName(), getAttributionTag());
+                return service.getServiceStateForSlot(slotIndex,
+                        renounceFineLocationAccess, renounceCoarseLocationAccess,
+                        getOpPackageName(), getAttributionTag());
             }
         } catch (RemoteException e) {
-            Log.e(TAG, "Error calling ITelephony#getServiceStateForSubscriber", e);
+            Log.e(TAG, "Error calling ITelephony#getServiceStateForSlot", e);
         } catch (NullPointerException e) {
             AnomalyReporter.reportAnomaly(
                     UUID.fromString("e2bed88e-def9-476e-bd71-3e572a8de6d1"),
-                    "getServiceStateForSubscriber " + subId + " NPE");
+                    "getServiceStateForSlot " + slotIndex + " NPE");
         }
         return null;
     }
@@ -13161,7 +13163,35 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
     public ServiceState getServiceStateForSubscriber(int subId) {
-        return getServiceStateForSubscriber(subId, false, false);
+        return getServiceStateForSlot(
+                SubscriptionManager.getSlotIndex(subId), false, false);
+    }
+
+    /**
+     * Returns the service state information on specified SIM slot.
+     *
+     * If you want continuous updates of service state info, register a {@link TelephonyCallback}
+     * that implements {@link TelephonyCallback.ServiceStateListener} through
+     * {@link #registerTelephonyCallback}.
+     *
+     * May return {@code null} when the {@code slotIndex} is invalid or when there was an error
+     * communicating with the phone process
+     *
+     * See {@link #getActiveModemCount()} to get the total number of slots
+     * that are active on the device.
+     *
+     * @param slotIndex of phone whose service state is returned
+     * @return ServiceState on specified SIM slot.
+     *
+     * @hide
+     */
+    @RequiresPermission(allOf = {
+            Manifest.permission.READ_PHONE_STATE,
+            Manifest.permission.ACCESS_COARSE_LOCATION
+    })
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS)
+    public @Nullable ServiceState getServiceStateForSlot(int slotIndex) {
+        return getServiceStateForSlot(slotIndex, false, false);
     }
 
     /**
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 0bb5fd5..47f53f3 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1015,13 +1015,27 @@
      * @hide
      */
     public static final int DATAGRAM_TYPE_KEEP_ALIVE = 3;
+    /**
+     * Datagram type indicating that the datagram to be sent or received is of type SOS message and
+     * is the last message to emergency service provider indicating still needs help.
+     * @hide
+     */
+    public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP = 4;
+    /**
+     * Datagram type indicating that the datagram to be sent or received is of type SOS message and
+     * is the last message to emergency service provider indicating no more help is needed.
+     * @hide
+     */
+    public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED = 5;
 
     /** @hide */
     @IntDef(prefix = "DATAGRAM_TYPE_", value = {
             DATAGRAM_TYPE_UNKNOWN,
             DATAGRAM_TYPE_SOS_MESSAGE,
             DATAGRAM_TYPE_LOCATION_SHARING,
-            DATAGRAM_TYPE_KEEP_ALIVE
+            DATAGRAM_TYPE_KEEP_ALIVE,
+            DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP,
+            DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface DatagramType {}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index d4da736..65de7e4 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -1399,19 +1399,18 @@
     oneway void requestModemActivityInfo(in ResultReceiver result);
 
     /**
-     * Get the service state on specified subscription
-     * @param subId Subscription id
+     * Get the service state on specified SIM slot.
+     * @param slotIndex of phone whose service state is returned
      * @param renounceFineLocationAccess Set this to true if the caller would not like to
      * receive fine location related information
      * @param renounceCoarseLocationAccess Set this to true if the caller would not like to
      * receive coarse location related information
      * @param callingPackage The package making the call
      * @param callingFeatureId The feature in the package
-     * @return Service state on specified subscription.
+     * @return Service state on specified SIM slot.
      */
-    ServiceState getServiceStateForSubscriber(int subId, boolean renounceFineLocationAccess,
-            boolean renounceCoarseLocationAccess,
-            String callingPackage, String callingFeatureId);
+    ServiceState getServiceStateForSlot(int slotIndex, boolean renounceFineLocationAccess,
+            boolean renounceCoarseLocationAccess, String callingPackage, String callingFeatureId);
 
     /**
      * Returns the URI for the per-account voicemail ringtone set in Phone settings.