Merge "Allows an app to cancel a device state request they made at any point." into main
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 857154f..9a178e5 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4495,8 +4495,9 @@
                             final int[] userIds =
                                     mUserWakeupStore.getUserIdsToWakeup(nowELAPSED);
                             for (int i = 0; i < userIds.length; i++) {
-                                if (!mActivityManagerInternal.startUserInBackground(
-                                        userIds[i])) {
+                                if (mActivityManagerInternal.isUserRunning(userIds[i], 0)
+                                        || !mActivityManagerInternal.startUserInBackground(
+                                                userIds[i])) {
                                     mUserWakeupStore.removeUserWakeup(userIds[i]);
                                 }
                             }
diff --git a/core/api/current.txt b/core/api/current.txt
index d610f4c..c5a70df 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -34161,6 +34161,7 @@
     field public static final int USAGE_CLASS_UNKNOWN = 0; // 0x0
     field public static final int USAGE_COMMUNICATION_REQUEST = 65; // 0x41
     field public static final int USAGE_HARDWARE_FEEDBACK = 50; // 0x32
+    field @FlaggedApi("android.os.vibrator.vibration_attribute_ime_usage_api") public static final int USAGE_IME_FEEDBACK = 82; // 0x52
     field public static final int USAGE_MEDIA = 19; // 0x13
     field public static final int USAGE_NOTIFICATION = 49; // 0x31
     field public static final int USAGE_PHYSICAL_EMULATION = 34; // 0x22
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index 10dc3c6..62aa5e0 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -25,7 +25,6 @@
 import android.content.ContentProvider;
 import android.content.Intent;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -660,28 +659,6 @@
         }
     }
 
-    /**
-     * Shows or hides a Camera app compat toggle for stretched issues with the requested state.
-     *
-     * @param token The token for the window that needs a control.
-     * @param showControl Whether the control should be shown or hidden.
-     * @param transformationApplied Whether the treatment is already applied.
-     * @param callback The callback executed when the user clicks on a control.
-     */
-    void requestCompatCameraControl(Resources res, IBinder token, boolean showControl,
-            boolean transformationApplied, ICompatCameraControlCallback callback) {
-        if (!res.getBoolean(com.android.internal.R.bool
-                .config_isCameraCompatControlForStretchedIssuesEnabled)) {
-            return;
-        }
-        try {
-            getActivityClientController().requestCompatCameraControl(
-                    token, showControl, transformationApplied, callback);
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
-        }
-    }
-
     public static ActivityClient getInstance() {
         return sInstance.get();
     }
diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java
index c8ab260..799df1f 100644
--- a/core/java/android/app/ActivityTaskManager.java
+++ b/core/java/android/app/ActivityTaskManager.java
@@ -172,7 +172,7 @@
         }
     }
 
-    /** Removes root tasks of the activity types from the system. */
+    /** Removes root tasks of the activity types from the Default TDA of all displays. */
     @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
     public void removeRootTasksWithActivityTypes(@NonNull int[] activityTypes) {
         try {
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 75aab7d..07e4f22 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -734,19 +734,6 @@
                             activityWindowInfo,
                             false /* alwaysReportChange */);
                 }
-
-                @Override
-                public void requestCompatCameraControl(boolean showControl,
-                        boolean transformationApplied, ICompatCameraControlCallback callback) {
-                    if (activity == null) {
-                        throw new IllegalStateException(
-                                "Received camera compat control update for non-existing activity");
-                    }
-                    ActivityClient.getInstance().requestCompatCameraControl(
-                            activity.getResources(), token, showControl, transformationApplied,
-                            callback);
-                }
-
             };
         }
 
diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java
index 92543b1..b03011f 100644
--- a/core/java/android/app/AppCompatTaskInfo.java
+++ b/core/java/android/app/AppCompatTaskInfo.java
@@ -136,8 +136,7 @@
      * @return {@value true} if the task has some compat ui.
      */
     public boolean hasCompatUI() {
-        return cameraCompatTaskInfo.hasCameraCompatUI() || topActivityInSizeCompat
-                || topActivityEligibleForLetterboxEducation
+        return topActivityInSizeCompat || topActivityEligibleForLetterboxEducation
                 || isLetterboxDoubleTapEnabled
                 || topActivityEligibleForUserAspectRatioButton;
     }
diff --git a/core/java/android/app/CameraCompatTaskInfo.java b/core/java/android/app/CameraCompatTaskInfo.java
index 1e116b7..53eddbe 100644
--- a/core/java/android/app/CameraCompatTaskInfo.java
+++ b/core/java/android/app/CameraCompatTaskInfo.java
@@ -31,45 +31,6 @@
  */
 public class CameraCompatTaskInfo implements Parcelable {
     /**
-     * Camera compat control isn't shown because it's not requested by heuristics.
-     */
-    public static final int CAMERA_COMPAT_CONTROL_HIDDEN = 0;
-
-    /**
-     * Camera compat control is shown with the treatment suggested.
-     */
-    public static final int CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED = 1;
-
-    /**
-     * Camera compat control is shown to allow reverting the applied treatment.
-     */
-    public static final int CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED = 2;
-
-    /**
-     * Camera compat control is dismissed by user.
-     */
-    public static final int CAMERA_COMPAT_CONTROL_DISMISSED = 3;
-
-    /**
-     * Enum for the Camera app compat control states.
-     */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = { "CAMERA_COMPAT_CONTROL_" }, value = {
-            CAMERA_COMPAT_CONTROL_HIDDEN,
-            CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED,
-            CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED,
-            CAMERA_COMPAT_CONTROL_DISMISSED,
-    })
-    public @interface CameraCompatControlState {}
-
-    /**
-     * State of the Camera app compat control which is used to correct stretched viewfinder
-     * in apps that don't handle all possible configurations and changes between them correctly.
-     */
-    @CameraCompatControlState
-    public int cameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN;
-
-    /**
      * The value to use when no camera compat treatment should be applied to a windowed task.
      */
     public static final int CAMERA_COMPAT_FREEFORM_NONE = 0;
@@ -137,7 +98,6 @@
      * Reads the CameraCompatTaskInfo from a parcel.
      */
     void readFromParcel(Parcel source) {
-        cameraCompatControlState = source.readInt();
         freeformCameraCompatMode = source.readInt();
     }
 
@@ -146,26 +106,10 @@
      */
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeInt(cameraCompatControlState);
         dest.writeInt(freeformCameraCompatMode);
     }
 
     /**
-     * @return {@value true} if the task has camera compat controls.
-     */
-    public boolean hasCameraCompatControl() {
-        return cameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN
-                && cameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED;
-    }
-
-    /**
-     * @return {@value true} if the task has some compat ui.
-     */
-    public boolean hasCameraCompatUI() {
-        return hasCameraCompatControl();
-    }
-
-    /**
      * @return  {@code true} if the camera compat parameters that are important for task organizers
      * are equal.
      */
@@ -183,33 +127,16 @@
         if (that == null) {
             return false;
         }
-        return cameraCompatControlState == that.cameraCompatControlState
-                && freeformCameraCompatMode == that.freeformCameraCompatMode;
+        return freeformCameraCompatMode == that.freeformCameraCompatMode;
     }
 
     @Override
     public String toString() {
-        return "CameraCompatTaskInfo { cameraCompatControlState="
-                + cameraCompatControlStateToString(cameraCompatControlState)
-                + " freeformCameraCompatMode="
+        return "CameraCompatTaskInfo { freeformCameraCompatMode="
                 + freeformCameraCompatModeToString(freeformCameraCompatMode)
                 + "}";
     }
 
-    /** Human readable version of the camera control state. */
-    @NonNull
-    public static String cameraCompatControlStateToString(
-            @CameraCompatControlState int cameraCompatControlState) {
-        return switch (cameraCompatControlState) {
-            case CAMERA_COMPAT_CONTROL_HIDDEN -> "hidden";
-            case CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED -> "treatment-suggested";
-            case CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED -> "treatment-applied";
-            case CAMERA_COMPAT_CONTROL_DISMISSED -> "dismissed";
-            default -> throw new AssertionError(
-                    "Unexpected camera compat control state: " + cameraCompatControlState);
-        };
-    }
-
     /** Human readable version of the freeform camera compat mode. */
     @NonNull
     public static String freeformCameraCompatModeToString(
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 9c8fea1..9615015 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -17,7 +17,6 @@
 package android.app;
 
 import android.app.ActivityManager;
-import android.app.ICompatCameraControlCallback;
 import android.app.IRequestFinishCallback;
 import android.app.PictureInPictureParams;
 import android.content.ComponentName;
@@ -172,17 +171,6 @@
     oneway void splashScreenAttached(in IBinder token);
 
     /**
-     * Shows or hides a Camera app compat toggle for stretched issues with the requested state.
-     *
-     * @param token The token for the window that needs a control.
-     * @param showControl Whether the control should be shown or hidden.
-     * @param transformationApplied Whether the treatment is already applied.
-     * @param callback The callback executed when the user clicks on a control.
-     */
-    oneway void requestCompatCameraControl(in IBinder token, boolean showControl,
-            boolean transformationApplied, in ICompatCameraControlCallback callback);
-
-    /**
      * If set, any activity launch in the same task will be overridden to the locale of activity
      * that started the task.
      */
diff --git a/core/java/android/app/ICompatCameraControlCallback.aidl b/core/java/android/app/ICompatCameraControlCallback.aidl
deleted file mode 100644
index 1a7f210..0000000
--- a/core/java/android/app/ICompatCameraControlCallback.aidl
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app;
-
-/**
- * This callback allows ActivityRecord to ask the calling View to apply the treatment for stretched
- * issues affecting camera viewfinders when the user clicks on the camera compat control.
- *
- * {@hide}
- */
-oneway interface ICompatCameraControlCallback {
-
-    void applyCameraCompatTreatment();
-
-    void revertCameraCompatTreatment();
-}
diff --git a/core/java/android/app/Person.java b/core/java/android/app/Person.java
index 96f6f4e..c7432c5 100644
--- a/core/java/android/app/Person.java
+++ b/core/java/android/app/Person.java
@@ -189,10 +189,8 @@
      */
     public void visitUris(@NonNull Consumer<Uri> visitor) {
         visitor.accept(getIconUri());
-        if (Flags.visitPersonUri()) {
-            if (mUri != null && !mUri.isEmpty()) {
-                visitor.accept(Uri.parse(mUri));
-            }
+        if (mUri != null && !mUri.isEmpty()) {
+            visitor.accept(Uri.parse(mUri));
         }
     }
 
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 19de793..a1ae9da 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -84,6 +84,13 @@
 }
 
 flag {
+     namespace: "virtual_devices"
+     name: "enforce_remote_device_opt_out_on_all_virtual_displays"
+     description: "Respect canDisplayOnRemoteDevices on all virtual displays"
+     bug: "338973239"
+}
+
+flag {
     namespace: "virtual_devices"
     name: "virtual_display_multi_window_mode_support"
     description: "Add support for WINDOWING_MODE_MULTI_WINDOW to virtual displays by default"
diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java
index 32d1964..ca6d86a 100644
--- a/core/java/android/content/IntentSender.java
+++ b/core/java/android/content/IntentSender.java
@@ -16,6 +16,8 @@
 
 package android.content;
 
+import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM;
+
 import android.annotation.FlaggedApi;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -23,6 +25,9 @@
 import android.app.ActivityOptions;
 import android.app.ActivityThread;
 import android.app.IApplicationThread;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Bundle;
 import android.os.Handler;
@@ -65,6 +70,11 @@
  * {@link android.app.PendingIntent#getIntentSender() PendingIntent.getIntentSender()}.
  */
 public class IntentSender implements Parcelable {
+    /** If enabled consider the deprecated @hide method as removed. */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = VANILLA_ICE_CREAM)
+    private static final long REMOVE_HIDDEN_SEND_INTENT_METHOD = 356174596;
+
     private static final Bundle SEND_INTENT_DEFAULT_OPTIONS =
             ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode(
                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT).toBundle();
@@ -220,6 +230,44 @@
      * original Intent.  Use {@code null} to not modify the original Intent.
      * @param onFinished The object to call back on when the send has
      * completed, or {@code null} for no callback.
+     * @param handler Handler identifying the thread on which the callback
+     * should happen.  If {@code null}, the callback will happen from the thread
+     * pool of the process.
+     * @param options Additional options the caller would like to provide to modify the sending
+     * behavior.  Typically built from using {@link ActivityOptions} to apply to an activity start.
+     *
+     * @throws SendIntentException Throws CanceledIntentException if the IntentSender
+     * is no longer allowing more intents to be sent through it.
+     *
+     * @deprecated use {@link #sendIntent(Context, int, Intent, String, Bundle, Executor,
+     *         OnFinished)}
+     *
+     * @hide
+     */
+    @Deprecated public void sendIntent(Context context, int code, Intent intent,
+            OnFinished onFinished, Handler handler, String requiredPermission,
+            @Nullable Bundle options)
+            throws SendIntentException {
+        if (CompatChanges.isChangeEnabled(REMOVE_HIDDEN_SEND_INTENT_METHOD)) {
+            throw new NoSuchMethodError("This overload of sendIntent was removed.");
+        }
+        sendIntent(context, code, intent, requiredPermission, options,
+                handler == null ? null : handler::post, onFinished);
+    }
+
+    /**
+     * Perform the operation associated with this IntentSender, allowing the
+     * caller to specify information about the Intent to use and be notified
+     * when the send has completed.
+     *
+     * @param context The Context of the caller.  This may be {@code null} if
+     * <var>intent</var> is also {@code null}.
+     * @param code Result code to supply back to the IntentSender's target.
+     * @param intent Additional Intent data.  See {@link Intent#fillIn
+     * Intent.fillIn()} for information on how this is applied to the
+     * original Intent.  Use {@code null} to not modify the original Intent.
+     * @param onFinished The object to call back on when the send has
+     * completed, or {@code null} for no callback.
      * @param executor Executor identifying the thread on which the callback
      * should happen.  If {@code null}, the callback will happen from the thread
      * pool of the process.
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 6952a09..481e6b5 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -617,7 +617,7 @@
      */
     public static final int FLAG_ENABLE_VR_MODE = 0x8000;
     /**
-     * Bit in {@link #flags} indicating if the activity can be displayed on a remote device.
+     * Bit in {@link #flags} indicating if the activity can be displayed on a virtual display.
      * Corresponds to {@link android.R.attr#canDisplayOnRemoteDevices}
      * @hide
      */
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 88fbbdd..6882d5c 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -359,3 +359,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "show_different_creation_error_for_unsupported_devices"
+  namespace: "profile_experiences"
+  description: "On private space create error due to child account added/fully managed user show message with link to the Help Center to find out more."
+  bug: "340130375"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index fe14d457..00ce949 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -1171,12 +1171,14 @@
             }
             switch (motionEvent.getAction()) {
                 case MotionEvent.ACTION_DOWN:
+                case MotionEvent.ACTION_HOVER_ENTER:
                     // Consume and ignore all touches while stylus is down to prevent
                     // accidental touches from going to the app while writing.
                     mPrivOps.setHandwritingSurfaceNotTouchable(false);
                     break;
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_HOVER_EXIT:
                     // Go back to only consuming stylus events so that the user
                     // can continue to interact with the app using touch
                     // when the stylus is not down.
diff --git a/core/java/android/os/BatteryConsumer.java b/core/java/android/os/BatteryConsumer.java
index 000a537..623196b 100644
--- a/core/java/android/os/BatteryConsumer.java
+++ b/core/java/android/os/BatteryConsumer.java
@@ -209,7 +209,8 @@
                 POWER_COMPONENT_VIDEO,
                 POWER_COMPONENT_FLASHLIGHT,
                 POWER_COMPONENT_CAMERA,
-                POWER_COMPONENT_GNSS};
+                POWER_COMPONENT_GNSS,
+                POWER_COMPONENT_SENSORS};
         Arrays.sort(supportedPowerComponents);
         SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE = IntArray.wrap(supportedPowerComponents);
     };
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index cdffea4..c7751e3 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -3077,7 +3077,7 @@
     public static final String[] HISTORY_EVENT_NAMES = new String[] {
             "null", "proc", "fg", "top", "sync", "wake_lock_in", "job", "user", "userfg", "conn",
             "active", "pkginst", "pkgunin", "alarm", "stats", "pkginactive", "pkgactive",
-            "tmpwhitelist", "screenwake", "wakeupap", "longwake", "est_capacity", "state"
+            "tmpwhitelist", "screenwake", "wakeupap", "longwake", "state"
     };
 
     public static final String[] HISTORY_EVENT_CHECKIN_NAMES = new String[] {
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 136c45d..47096db 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -434,7 +434,6 @@
     @RavenwoodThrow
     private static native void nativeWriteStrongBinder(long nativePtr, IBinder val);
     @FastNative
-    @RavenwoodThrow
     private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val);
 
     private static native byte[] nativeCreateByteArray(long nativePtr);
@@ -456,7 +455,6 @@
     @RavenwoodThrow
     private static native IBinder nativeReadStrongBinder(long nativePtr);
     @FastNative
-    @RavenwoodThrow
     private static native FileDescriptor nativeReadFileDescriptor(long nativePtr);
 
     private static native long nativeCreate();
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 71957ee..464df23 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -381,6 +381,8 @@
     }
 
     private static void closeInternal$ravenwood(FileDescriptor fd) {
+        // Desktop JVM doesn't have FileDescriptor.close(), so we'll need to go to the ravenwood
+        // side to close it.
         native_close$ravenwood(fd);
     }
 
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index ce3156e..1fb7937 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -139,11 +139,16 @@
      * @param screenState The overridden screen state, or {@link Display#STATE_UNKNOWN}
      * to disable the override.
      * @param reason The reason for overriding the screen state.
-     * @param screenBrightness The overridden screen brightness, or
-     * {@link PowerManager#BRIGHTNESS_DEFAULT} to disable the override.
+     * @param screenBrightnessFloat The overridden screen brightness between
+     * {@link PowerManager#BRIGHTNESS_MIN} and {@link PowerManager#BRIGHTNESS_MAX}, or
+     * {@link PowerManager#BRIGHTNESS_INVALID_FLOAT} if screenBrightnessInt should be used instead.
+     * @param screenBrightnessInt The overridden screen brightness between 1 and 255, or
+     * {@link PowerManager#BRIGHTNESS_DEFAULT} to disable the override. Not used if
+     *                            screenBrightnessFloat is provided (is not NaN).
      */
     public abstract void setDozeOverrideFromDreamManager(
-            int screenState, @Display.StateReason int reason, int screenBrightness);
+            int screenState, @Display.StateReason int reason, float screenBrightnessFloat,
+            int screenBrightnessInt);
 
     /**
      * Used by sidekick manager to tell the power manager if it shouldn't change the display state
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index e95c6a4..8aec7eb 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -425,7 +425,7 @@
     private static IBinder rawGetService(String name) throws RemoteException {
         final long start = sStatLogger.getTime();
 
-        final IBinder binder = getIServiceManager().getService(name).getBinder();
+        final IBinder binder = getIServiceManager().getService2(name).getBinder();
 
         final int time = (int) sStatLogger.logDurationStat(Stats.GET_SERVICE, start);
 
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
index 6c9a5c7..5a9c878 100644
--- a/core/java/android/os/ServiceManagerNative.java
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -57,8 +57,14 @@
         return mRemote;
     }
 
+    // TODO(b/355394904): This function has been deprecated, please use getService2 instead.
     @UnsupportedAppUsage
-    public Service getService(String name) throws RemoteException {
+    public IBinder getService(String name) throws RemoteException {
+        // Same as checkService (old versions of servicemanager had both methods).
+        return checkService(name).getBinder();
+    }
+
+    public Service getService2(String name) throws RemoteException {
         // Same as checkService (old versions of servicemanager had both methods).
         return checkService(name);
     }
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 3aa42c6..392b6eb 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -6442,7 +6442,11 @@
      */
     @UnsupportedAppUsage
     public int getUserSerialNumber(@UserIdInt int userId) {
-        if (android.multiuser.Flags.cacheUserSerialNumber()) {
+        // Read only flag should is to fix early access to this API
+        // cacheUserSerialNumber to be removed after the
+        // cacheUserSerialNumberReadOnly is fully rolled out
+        if (android.multiuser.Flags.cacheUserSerialNumberReadOnly()
+                || android.multiuser.Flags.cacheUserSerialNumber()) {
             // System user serial number is always 0, and it always exists.
             // There is no need to call binder for that.
             if (userId == UserHandle.USER_SYSTEM) {
diff --git a/core/java/android/os/VibrationAttributes.java b/core/java/android/os/VibrationAttributes.java
index 9df5b85..da863e5 100644
--- a/core/java/android/os/VibrationAttributes.java
+++ b/core/java/android/os/VibrationAttributes.java
@@ -16,6 +16,9 @@
 
 package android.os;
 
+import static android.os.vibrator.Flags.FLAG_VIBRATION_ATTRIBUTE_IME_USAGE_API;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -55,6 +58,7 @@
             USAGE_PHYSICAL_EMULATION,
             USAGE_RINGTONE,
             USAGE_TOUCH,
+            USAGE_IME_FEEDBACK,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Usage {}
@@ -136,6 +140,12 @@
      */
     public static final int USAGE_ACCESSIBILITY = 0x40 | USAGE_CLASS_FEEDBACK;
     /**
+     * Usage value to use for input method editor (IME) haptic feedback.
+     */
+    @FlaggedApi(FLAG_VIBRATION_ATTRIBUTE_IME_USAGE_API)
+    public static final int USAGE_IME_FEEDBACK = 0x50 | USAGE_CLASS_FEEDBACK;
+
+    /**
      * Usage value to use for media vibrations, such as music, movie, soundtrack, animations, games,
      * or any interactive media that isn't for touch feedback specifically.
      */
@@ -174,7 +184,6 @@
             FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF,
             FLAG_INVALIDATE_SETTINGS_CACHE,
             FLAG_PIPELINED_EFFECT,
-            FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Flag{}
@@ -228,31 +237,12 @@
     public static final int FLAG_PIPELINED_EFFECT = 1 << 3;
 
     /**
-     * Flag requesting that this vibration effect to be played without applying the user
-     * intensity setting to scale the vibration.
-     *
-     * <p>The user setting is still applied to enable/disable the vibration, but the vibration
-     * effect strength will not be scaled based on the enabled setting value.
-     *
-     * <p>This is intended to be used on scenarios where the system needs to enforce a specific
-     * strength for the vibration effect, regardless of the user preference. Only privileged apps
-     * can ignore user settings, and this flag will be ignored otherwise.
-     *
-     * <p>If you need to bypass the user setting when it's disabling vibrations then this also
-     * needs the flag {@link #FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF} to be set.
-     *
-     * @hide
-     */
-    public static final int FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE = 1 << 4;
-
-    /**
      * All flags supported by vibrator service, update it when adding new flag.
      * @hide
      */
     public static final int FLAG_ALL_SUPPORTED =
             FLAG_BYPASS_INTERRUPTION_POLICY | FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF
-                    | FLAG_INVALIDATE_SETTINGS_CACHE | FLAG_PIPELINED_EFFECT
-                    | FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+                    | FLAG_INVALIDATE_SETTINGS_CACHE | FLAG_PIPELINED_EFFECT;
 
     /** Creates a new {@link VibrationAttributes} instance with given usage. */
     public static @NonNull VibrationAttributes createForUsage(@Usage int usage) {
@@ -349,6 +339,7 @@
             case USAGE_RINGTONE:
                 return AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
             case USAGE_TOUCH:
+            case USAGE_IME_FEEDBACK:
                 return AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
             case USAGE_ALARM:
                 return AudioAttributes.USAGE_ALARM;
@@ -447,6 +438,8 @@
                 return "PHYSICAL_EMULATION";
             case USAGE_HARDWARE_FEEDBACK:
                 return "HARDWARE_FEEDBACK";
+            case USAGE_IME_FEEDBACK:
+                return "IME";
             default:
                 return "unknown usage " + usage;
         }
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index f6e73b3..a4164e9 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -20,6 +20,7 @@
 import static android.os.VibrationAttributes.USAGE_ALARM;
 import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
 import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
 import static android.os.VibrationAttributes.USAGE_MEDIA;
 import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
 import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
@@ -67,6 +68,8 @@
     private final int mDefaultNotificationVibrationIntensity;
     @VibrationIntensity
     private final int mDefaultRingVibrationIntensity;
+    @VibrationIntensity
+    private final int mDefaultKeyboardVibrationIntensity;
 
     private final boolean mKeyboardVibrationSettingsSupported;
 
@@ -98,6 +101,8 @@
                 com.android.internal.R.integer.config_defaultNotificationVibrationIntensity);
         mDefaultRingVibrationIntensity = loadDefaultIntensity(resources,
                 com.android.internal.R.integer.config_defaultRingVibrationIntensity);
+        mDefaultKeyboardVibrationIntensity = loadDefaultIntensity(resources,
+                com.android.internal.R.integer.config_defaultKeyboardVibrationIntensity);
     }
 
     @VibrationIntensity
@@ -213,6 +218,9 @@
             case USAGE_PHYSICAL_EMULATION:
             case USAGE_ACCESSIBILITY:
                 return mDefaultHapticFeedbackIntensity;
+            case USAGE_IME_FEEDBACK:
+                return isKeyboardVibrationSettingsSupported()
+                        ? mDefaultKeyboardVibrationIntensity : mDefaultHapticFeedbackIntensity;
             case USAGE_MEDIA:
             case USAGE_UNKNOWN:
                 // fall through
@@ -236,6 +244,7 @@
                 + ", mDefaultMediaIntensity=" + mDefaultMediaVibrationIntensity
                 + ", mDefaultNotificationIntensity=" + mDefaultNotificationVibrationIntensity
                 + ", mDefaultRingIntensity=" + mDefaultRingVibrationIntensity
+                + ", mDefaultKeyboardIntensity=" + mDefaultKeyboardVibrationIntensity
                 + ", mKeyboardVibrationSettingsSupported=" + mKeyboardVibrationSettingsSupported
                 + "}";
     }
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 62b3682..67c3464 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -74,3 +74,14 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "haptics"
+    name: "vibration_attribute_ime_usage_api"
+    is_exported: true
+    description: "A public API for IME usage vibration attribute"
+    bug: "332661766"
+    metadata {
+        purpose: PURPOSE_FEATURE
+    }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 850b979..0ee6f43 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12560,6 +12560,14 @@
                 "contextual_screen_timeout_enabled";
 
         /**
+         * Whether hinge angle lidevent is enabled.
+         *
+         * @hide
+         */
+        public static final String HINGE_ANGLE_LIDEVENT_ENABLED =
+                "hinge_angle_lidevent_enabled";
+
+        /**
          * Whether lockscreen weather is enabled.
          *
          * @hide
@@ -15812,7 +15820,7 @@
          * The following keys are supported:
          *
          * <pre>
-         * screen_brightness_array         (int[])
+         * screen_brightness_array         (int[], values in range [1, 255])
          * dimming_scrim_array             (int[])
          * prox_screen_off_delay           (long)
          * prox_cooldown_trigger           (long)
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index d454716..06e53ac 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -74,6 +74,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.util.DumpUtils;
 
 import java.io.FileDescriptor;
@@ -269,6 +270,7 @@
     private volatile int mDozeScreenState = Display.STATE_UNKNOWN;
     private volatile @Display.StateReason int mDozeScreenStateReason = Display.STATE_REASON_UNKNOWN;
     private volatile int mDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
+    private volatile float mDozeScreenBrightnessFloat = PowerManager.BRIGHTNESS_INVALID_FLOAT;
 
     private boolean mDebug = false;
 
@@ -927,12 +929,12 @@
             try {
                 if (startAndStopDozingInBackground()) {
                     mDreamManager.startDozingOneway(
-                        mDreamToken, mDozeScreenState, mDozeScreenStateReason,
-                        mDozeScreenBrightness);
+                            mDreamToken, mDozeScreenState, mDozeScreenStateReason,
+                            mDozeScreenBrightnessFloat, mDozeScreenBrightness);
                 } else {
                     mDreamManager.startDozing(
                             mDreamToken, mDozeScreenState, mDozeScreenStateReason,
-                            mDozeScreenBrightness);
+                            mDozeScreenBrightnessFloat, mDozeScreenBrightness);
                 }
 
             } catch (RemoteException ex) {
@@ -1057,7 +1059,7 @@
      * Gets the screen brightness to use while dozing.
      *
      * @return The screen brightness while dozing as a value between
-     * {@link PowerManager#BRIGHTNESS_OFF} (0) and {@link PowerManager#BRIGHTNESS_ON} (255),
+     * {@link PowerManager#BRIGHTNESS_OFF + 1} (1) and {@link PowerManager#BRIGHTNESS_ON} (255),
      * or {@link PowerManager#BRIGHTNESS_DEFAULT} (-1) to ask the system to apply
      * its default policy based on the screen state.
      *
@@ -1078,11 +1080,11 @@
      * The dream may set a different brightness before starting to doze and may adjust
      * the brightness while dozing to conserve power and achieve various effects.
      * </p><p>
-     * Note that dream may specify any brightness in the full 0-255 range, including
+     * Note that dream may specify any brightness in the full 1-255 range, including
      * values that are less than the minimum value for manual screen brightness
-     * adjustments by the user. In particular, the value may be set to 0 which may
-     * turn off the backlight entirely while still leaving the screen on although
-     * this behavior is device dependent and not guaranteed.
+     * adjustments by the user. In particular, the value may be set to
+     * {@link PowerManager.BRIGHTNESS_OFF} which may turn off the backlight entirely while still
+     * leaving the screen on although this behavior is device dependent and not guaranteed.
      * </p><p>
      * The available range of display brightness values and their behavior while dozing is
      * hardware dependent and may vary across devices. The dream may therefore
@@ -1090,7 +1092,7 @@
      * </p>
      *
      * @param brightness The screen brightness while dozing as a value between
-     * {@link PowerManager#BRIGHTNESS_OFF} (0) and {@link PowerManager#BRIGHTNESS_ON} (255),
+     * {@link PowerManager#BRIGHTNESS_OFF + 1} (1) and {@link PowerManager#BRIGHTNESS_ON} (255),
      * or {@link PowerManager#BRIGHTNESS_DEFAULT} (-1) to ask the system to apply
      * its default policy based on the screen state.
      *
@@ -1108,6 +1110,44 @@
     }
 
     /**
+     * Sets the screen brightness to use while dozing.
+     * <p>
+     * The value of this property determines the power state of the primary display
+     * once {@link #startDozing} has been called. The default value is
+     * {@link PowerManager#BRIGHTNESS_INVALID_FLOAT} which lets the system decide.
+     * The dream may set a different brightness before starting to doze and may adjust
+     * the brightness while dozing to conserve power and achieve various effects.
+     * </p><p>
+     * Note that dream may specify any brightness in the full 0-1 range, including
+     * values that are less than the minimum value for manual screen brightness
+     * adjustments by the user. In particular, the value may be set to
+     * {@link PowerManager#BRIGHTNESS_OFF_FLOAT} which may turn off the backlight entirely while
+     * still leaving the screen on although this behavior is device dependent and not guaranteed.
+     * </p><p>
+     * The available range of display brightness values and their behavior while dozing is
+     * hardware dependent and may vary across devices. The dream may therefore
+     * need to be modified or configured to correctly support the hardware.
+     * </p>
+     *
+     * @param brightness The screen brightness while dozing as a value between
+     * {@link PowerManager#BRIGHTNESS_MIN} (0) and {@link PowerManager#BRIGHTNESS_MAX} (1),
+     * or {@link PowerManager#BRIGHTNESS_INVALID_FLOAT} (Float.NaN) to ask the system to apply
+     * its default policy based on the screen state.
+     *
+     * @hide For use by system UI components only.
+     */
+    @UnsupportedAppUsage
+    public void setDozeScreenBrightnessFloat(float brightness) {
+        if (!Float.isNaN(brightness)) {
+            brightness = clampAbsoluteBrightnessFloat(brightness);
+        }
+        if (!BrightnessSynchronizer.floatEquals(mDozeScreenBrightnessFloat, brightness)) {
+            mDozeScreenBrightnessFloat = brightness;
+            updateDoze();
+        }
+    }
+
+    /**
      * Called when this Dream is constructed.
      */
     @Override
@@ -1346,7 +1386,11 @@
                     Slog.w(mTag, "WakeUp was called before the dream was attached.");
                 } else {
                     try {
-                        mDreamManager.finishSelf(mDreamToken, false /*immediate*/);
+                        if (startAndStopDozingInBackground()) {
+                            mDreamManager.finishSelfOneway(mDreamToken, false /*immediate*/);
+                        } else {
+                            mDreamManager.finishSelf(mDreamToken, false /*immediate*/);
+                        }
                     } catch (RemoteException ex) {
                         // system server died
                     }
@@ -1497,7 +1541,11 @@
         if (mFinished || mWaking) {
             Slog.w(mTag, "attach() called after dream already finished");
             try {
-                mDreamManager.finishSelf(dreamToken, true /*immediate*/);
+                if (startAndStopDozingInBackground()) {
+                    mDreamManager.finishSelfOneway(dreamToken, true /*immediate*/);
+                } else {
+                    mDreamManager.finishSelf(dreamToken, true /*immediate*/);
+                }
             } catch (RemoteException ex) {
                 // system server died
             }
@@ -1743,6 +1791,13 @@
         return MathUtils.constrain(value, PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON);
     }
 
+    private static float clampAbsoluteBrightnessFloat(float value) {
+        if (value == PowerManager.BRIGHTNESS_OFF_FLOAT) {
+            return value;
+        }
+        return MathUtils.constrain(value, PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX);
+    }
+
     /**
      * The DreamServiceWrapper is used as a gateway to the system_server, where DreamController
      * uses it to control the DreamService. It is also used to receive callbacks from the
diff --git a/core/java/android/service/dreams/IDreamManager.aidl b/core/java/android/service/dreams/IDreamManager.aidl
index 76f6363..611e791 100644
--- a/core/java/android/service/dreams/IDreamManager.aidl
+++ b/core/java/android/service/dreams/IDreamManager.aidl
@@ -42,7 +42,8 @@
     /** @deprecated Please use finishSelfOneway instead. */
     void finishSelf(in IBinder token, boolean immediate);
     /** @deprecated Please use startDozingOneway instead. */
-    void startDozing(in IBinder token, int screenState, int reason, int screenBrightness);
+    void startDozing(in IBinder token, int screenState, int reason, float screenBrightnessFloat,
+            int screenBrightnessInt);
     void stopDozing(in IBinder token);
     void forceAmbientDisplayEnabled(boolean enabled);
     ComponentName[] getDreamComponentsForUser(int userId);
@@ -52,6 +53,7 @@
     void startDreamActivity(in Intent intent);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE)")
     oneway void setDreamIsObscured(in boolean isObscured);
-    oneway void startDozingOneway(in IBinder token, int screenState, int reason, int screenBrightness);
+    oneway void startDozingOneway(in IBinder token, int screenState, int reason,
+            float screenBrightnessFloat, int screenBrightnessInt);
     oneway void finishSelfOneway(in IBinder token, boolean immediate);
 }
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index a78a417..6b1aef7 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -1320,7 +1320,11 @@
                         // It's possible that a Span is removed when the text covering it is
                         // deleted, in this case, the original start and end of the span might be
                         // OOB. So it'll reflow the entire string instead.
-                        reflow(s, 0, 0, s.length());
+                        if (Flags.insertModeCrashUpdateLayoutSpan()) {
+                            transformAndReflow(s, 0, s.length());
+                        } else {
+                            reflow(s, 0, 0, s.length());
+                        }
                     } else {
                         reflow(s, start, end - start, end - start);
                     }
@@ -1343,7 +1347,11 @@
                         // When text is changed, it'll also trigger onSpanChanged. In this case we
                         // can't determine the updated range in the transformed text. So it'll
                         // reflow the entire range instead.
-                        reflow(s, 0, 0, s.length());
+                        if (Flags.insertModeCrashUpdateLayoutSpan()) {
+                            transformAndReflow(s, 0, s.length());
+                        } else {
+                            reflow(s, 0, 0, s.length());
+                        }
                     } else {
                         reflow(s, start, end - start, end - start);
                         reflow(s, nstart, nend - nstart, nend - nstart);
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 155a3e4..88a1b9c 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -125,6 +125,16 @@
 }
 
 flag {
+  name: "insert_mode_crash_update_layout_span"
+  namespace: "text"
+  description: "Fix insert mode crash when the text has UpdateLayout span attached."
+  bug: "355137282"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "icu_bidi_migration"
   namespace: "text"
   description: "A flag for replacing AndroidBidi with android.icu.text.Bidi."
diff --git a/core/java/android/view/InputEventAssigner.java b/core/java/android/view/InputEventAssigner.java
index 7fac6c5..30d9aaa 100644
--- a/core/java/android/view/InputEventAssigner.java
+++ b/core/java/android/view/InputEventAssigner.java
@@ -17,7 +17,8 @@
 package android.view;
 
 import static android.os.IInputConstants.INVALID_INPUT_EVENT_ID;
-import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.InputDevice.SOURCE_CLASS_POINTER;
+import static android.view.InputDevice.SOURCE_CLASS_POSITION;
 
 /**
  * Process input events and assign input event id to a specific frame.
@@ -64,18 +65,19 @@
     public int processEvent(InputEvent event) {
         if (event instanceof MotionEvent) {
             MotionEvent motionEvent = (MotionEvent) event;
-            if (motionEvent.isFromSource(SOURCE_TOUCHSCREEN)) {
+            if (motionEvent.isFromSource(SOURCE_CLASS_POINTER) || motionEvent.isFromSource(
+                    SOURCE_CLASS_POSITION)) {
                 final int action = motionEvent.getActionMasked();
                 if (action == MotionEvent.ACTION_DOWN) {
                     mHasUnprocessedDown = true;
                     mDownEventId = event.getId();
                 }
-                if (mHasUnprocessedDown && action == MotionEvent.ACTION_MOVE) {
-                    return mDownEventId;
-                }
                 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                     mHasUnprocessedDown = false;
                 }
+                if (mHasUnprocessedDown) {
+                    return mDownEventId;
+                }
             }
         }
         return event.getId();
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 64c7766..5f8bea1 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -34071,14 +34071,21 @@
     }
 
     private float convertVelocityToFrameRate(float velocityPps) {
-        // From UXR study, premium experience is:
-        // 1500+    dp/s: 120fps
-        // 0 - 1500 dp/s:  80fps
-        // OEMs are likely to modify this to balance battery and user experience for their
-        // specific device.
+        // Internal testing has shown that this gives a premium experience:
+        // above 300dp/s => 120fps
+        // between 300dp/s and 125fps => 80fps
+        // below 125dp/s => 60fps
         float density = mAttachInfo.mDensity;
         float velocityDps = velocityPps / density;
-        return (velocityDps >= 1500f) ? MAX_FRAME_RATE : 80f;
+        float frameRate;
+        if (velocityDps > 300f) {
+            frameRate = MAX_FRAME_RATE; // Use maximum at fast motion
+        } else if (velocityDps > 125f) {
+            frameRate = 80f; // Use medium frame rate when motion is slower
+        } else {
+            frameRate = 60f; // Use minimum frame rate when motion is very slow
+        }
+        return frameRate;
     }
 
     /**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9e52a14..9518abf 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -145,7 +145,6 @@
 import android.annotation.UiContext;
 import android.app.ActivityManager;
 import android.app.ActivityThread;
-import android.app.ICompatCameraControlCallback;
 import android.app.ResourcesManager;
 import android.app.WindowConfiguration;
 import android.app.compat.CompatChanges;
@@ -516,17 +515,6 @@
                 int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) {
             onConfigurationChanged(overrideConfig, newDisplayId);
         }
-
-        /**
-         * Notify the corresponding activity about the request to show or hide a camera compat
-         * control for stretched issues in the viewfinder.
-         *
-         * @param showControl Whether the control should be shown or hidden.
-         * @param transformationApplied Whether the treatment is already applied.
-         * @param callback The callback executed when the user clicks on a control.
-         */
-        void requestCompatCameraControl(boolean showControl, boolean transformationApplied,
-                ICompatCameraControlCallback callback);
     }
 
     /**
@@ -12503,20 +12491,6 @@
         }
     }
 
-    /**
-     * Shows or hides a Camera app compat toggle for stretched issues with the requested state
-     * for the corresponding activity.
-     *
-     * @param showControl Whether the control should be shown or hidden.
-     * @param transformationApplied Whether the treatment is already applied.
-     * @param callback The callback executed when the user clicks on a control.
-    */
-    public void requestCompatCameraControl(boolean showControl, boolean transformationApplied,
-                ICompatCameraControlCallback callback) {
-        mActivityConfigCallback.requestCompatCameraControl(
-                showControl, transformationApplied, callback);
-    }
-
     boolean wasRelayoutRequested() {
         return mRelayoutRequested;
     }
diff --git a/core/java/android/view/autofill/AutofillClientController.java b/core/java/android/view/autofill/AutofillClientController.java
index d505c733..95cae226 100644
--- a/core/java/android/view/autofill/AutofillClientController.java
+++ b/core/java/android/view/autofill/AutofillClientController.java
@@ -582,4 +582,9 @@
             Log.e(TAG, "authenticate() failed for intent:" + intent, e);
         }
     }
+
+    @Override
+    public boolean isActivityResumed() {
+        return mActivity.isResumed();
+    }
 }
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 02a86c9..79ecfe1e 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -114,6 +114,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -774,6 +775,13 @@
     // dataset in responses. Used to avoid request pre-fill request again and again.
     private final ArraySet<AutofillId> mAllTrackedViews = new ArraySet<>();
 
+    // Whether we need to re-attempt fill again. Needed for case of relayout.
+    private boolean mFillReAttemptNeeded = false;
+
+    private Map<Integer, AutofillId> mFingerprintToViewMap = new ArrayMap<>();
+
+    private AutofillStateFingerprint mAutofillStateFingerprint;
+
     /** @hide */
     public interface AutofillClient {
         /**
@@ -909,6 +917,11 @@
          * @return An ID that is unique in the activity.
          */
         @Nullable AutofillId autofillClientGetNextAutofillId();
+
+        /**
+         * @return Whether the activity is resumed or not.
+         */
+        boolean isActivityResumed();
     }
 
     /**
@@ -919,6 +932,7 @@
         mService = service;
         mOptions = context.getAutofillOptions();
         mIsFillRequested = new AtomicBoolean(false);
+        mAutofillStateFingerprint = AutofillStateFingerprint.createInstance();
 
         mIsFillDialogEnabled = AutofillFeatureFlags.isFillDialogEnabled();
         mFillDialogEnabledHints = AutofillFeatureFlags.getFillDialogEnabledHints();
@@ -1357,6 +1371,20 @@
             mOnInvisibleCalled = true;
 
             if (isExpiredResponse) {
+                if (mRelayoutFix && isAuthenticationPending()) {
+                    Log.i(TAG, "onInvisibleForAutofill(): Ignoring expiringResponse due to pending"
+                            + " authentication");
+                    try {
+                        mService.notifyNotExpiringResponseDuringAuth(
+                                mSessionId, mContext.getUserId());
+                    } catch (RemoteException e) {
+                        // The failure could be a consequence of something going wrong on the
+                        // server side. Do nothing here since it's just logging, but it's
+                        // possible follow-up actions may fail.
+                    }
+                    return;
+                }
+                Log.i(TAG, "onInvisibleForAutofill(): expiringResponse");
                 // Notify service the response has expired.
                 updateSessionLocked(/* id= */ null, /* bounds= */ null, /* value= */ null,
                         ACTION_RESPONSE_EXPIRED, /* flags= */ 0);
@@ -1513,14 +1541,29 @@
     }
 
     /**
+     * Called to log notify view entered was ignored due to pending auth
+     * @hide
+     */
+    public void notifyViewEnteredIgnoredDuringAuthCount() {
+        try {
+            mService.notifyViewEnteredIgnoredDuringAuthCount(mSessionId, mContext.getUserId());
+        } catch (RemoteException e) {
+            // The failure could be a consequence of something going wrong on the
+            // server side. Do nothing here since it's just logging, but it's
+            // possible follow-up actions may fail.
+        }
+    }
+
+    /**
      * Called to check if we should retry fill.
      * Useful for knowing whether to attempt refill after relayout.
      *
      * @hide
      */
     public boolean shouldRetryFill() {
-        // TODO: Implement in follow-up cl
-        return false;
+        synchronized (mLock) {
+            return isAuthenticationPending() && mFillReAttemptNeeded;
+        }
     }
 
     /**
@@ -1531,8 +1574,13 @@
      */
     public boolean attemptRefill() {
         Log.i(TAG, "Attempting refill");
-        // TODO: Implement in follow-up cl
-        return false;
+        // Find active autofillable views. Compute their fingerprints
+        List<View> autofillableViews =
+                getClient().autofillClientFindAutofillableViewsByTraversal();
+        if (sDebug) {
+            Log.d(TAG, "Autofillable views count:" + autofillableViews.size());
+        }
+        return mAutofillStateFingerprint.attemptRefill(autofillableViews, this);
     }
 
     /**
@@ -2493,7 +2541,13 @@
 
     /** @hide */
     public void onAuthenticationResult(int authenticationId, Intent data, View focusView) {
+        if (sVerbose) {
+            Log.v(TAG, "onAuthenticationResult(): authId= " + authenticationId + ", data=" + data);
+        }
         if (!hasAutofillFeature()) {
+            if (sVerbose) {
+                Log.v(TAG, "onAuthenticationResult(): autofill not enabled");
+            }
             return;
         }
         // TODO: the result code is being ignored, so this method is not reliably
@@ -2501,10 +2555,6 @@
         // set the EXTRA_AUTHENTICATION_RESULT extra, but it could cause weird results if the
         // service set the extra and returned RESULT_CANCELED...
 
-        if (sDebug) {
-            Log.d(TAG, "onAuthenticationResult(): id= " + authenticationId + ", data=" + data);
-        }
-
         synchronized (mLock) {
             if (!isActiveLocked()) {
                 Log.w(TAG, "onAuthenticationResult(): sessionId=" + mSessionId + " not active");
@@ -2661,6 +2711,7 @@
             mSessionId = receiver.getIntResult();
             if (mSessionId != NO_SESSION) {
                 mState = STATE_ACTIVE;
+                mAutofillStateFingerprint.setSessionId(mSessionId);
             }
             final int extraFlags = receiver.getOptionalExtraIntResult(0);
             if ((extraFlags & RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) != 0) {
@@ -2722,6 +2773,9 @@
         if (resetEnteredIds) {
             mEnteredIds = null;
         }
+        mFillReAttemptNeeded = false;
+        mFingerprintToViewMap.clear();
+        mAutofillStateFingerprint = AutofillStateFingerprint.createInstance();
     }
 
     @GuardedBy("mLock")
@@ -2984,8 +3038,12 @@
             Intent fillInIntent, boolean authenticateInline) {
         synchronized (mLock) {
             if (sessionId == mSessionId) {
-                if (mRelayoutFixDeprecated) {
+                if (mRelayoutFixDeprecated || mRelayoutFix) {
                     mState = STATE_PENDING_AUTHENTICATION;
+                    if (sVerbose) {
+                        Log.v(TAG, "entering STATE_PENDING_AUTHENTICATION : mRelayoutFix:"
+                                + mRelayoutFix);
+                    }
                 }
                 final AutofillClient client = getClient();
                 if (client != null) {
@@ -3191,17 +3249,56 @@
 
     @GuardedBy("mLock")
     private void handleFailedIdsLocked(@NonNull ArrayList<AutofillId> failedIds) {
+        handleFailedIdsLocked(failedIds, null, false, false);
+    }
+
+    @GuardedBy("mLock")
+    private void handleFailedIdsLocked(@NonNull ArrayList<AutofillId> failedIds,
+            ArrayList<AutofillValue> failedAutofillValues, boolean hideHighlight,
+            boolean isRefill) {
         if (!failedIds.isEmpty() && sVerbose) {
             Log.v(TAG, "autofill(): total failed views: " + failedIds);
         }
+
+        if (mRelayoutFix && !failedIds.isEmpty()) {
+            // Activity isn't in resumed state, so it's very possible that relayout could've
+            // occurred, so wait for it to declare proper failure. It's a temporary failure at the
+            // moment. We'll try again later when the activity is resumed.
+
+            // The above doesn't seem to be the correct way. Look for pending auth cases.
+            // TODO(b/238252288): Check whether there was any auth done at all
+            mFillReAttemptNeeded = true;
+            mAutofillStateFingerprint.storeFailedIdsAndValues(
+                    failedIds, failedAutofillValues, hideHighlight);
+        }
         try {
-            mService.setAutofillFailure(mSessionId, failedIds, mContext.getUserId());
+            mService.setAutofillFailure(mSessionId, failedIds, isRefill, mContext.getUserId());
         } catch (RemoteException e) {
             // In theory, we could ignore this error since it's not a big deal, but
             // in reality, we rather crash the app anyways, as the failure could be
             // a consequence of something going wrong on the server side...
             throw e.rethrowFromSystemServer();
         }
+        if (mRelayoutFix && !failedIds.isEmpty()) {
+            if (!getClient().isActivityResumed()) {
+                if (sVerbose) {
+                    Log.v(TAG, "handleFailedIdsLocked(): failed id's exist, but activity not"
+                            + " resumed");
+                }
+            } else {
+                if (isRefill) {
+                    Log.i(TAG, "handleFailedIdsLocked(): Attempted refill, but failed");
+                } else {
+                    // activity has been resumed, try to re-fill
+                    // getClient().isActivityResumed() && !failedIds.isEmpty() && !isRefill
+                    // TODO(b/238252288): Do better state management, and only trigger the following
+                    //  if there was auth previously.
+                    Log.i(TAG, "handleFailedIdsLocked(): Attempting refill");
+                    attemptRefill();
+                    mFillReAttemptNeeded = false;
+                }
+            }
+        }
     }
 
     private void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> values,
@@ -3216,13 +3313,46 @@
                 return;
             }
 
-            final int itemCount = ids.size();
-            int numApplied = 0;
-            ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null;
             final View[] views = client.autofillClientFindViewsByAutofillIdTraversal(
                     Helper.toArray(ids));
 
+            autofill(views, ids, values, hideHighlight, false);
+        }
+    }
+
+    void autofill(View[] views, List<AutofillId> ids, List<AutofillValue> values,
+            boolean hideHighlight, boolean isRefill) {
+        if (sVerbose) {
+            Log.v(TAG, "autofill() ids:" + ids + " isRefill:" + isRefill);
+        }
+        synchronized (mLock) {
+            final AutofillClient client = getClient();
+            if (client == null) {
+                return;
+            }
+
+            if (ids == null) {
+                Log.i(TAG, "autofill(): No id's to fill");
+                return;
+            }
+
+            if (mRelayoutFix && isRefill) {
+                try {
+                    mService.setAutofillIdsAttemptedForRefill(
+                            mSessionId, ids, mContext.getUserId());
+                } catch (RemoteException e) {
+                    // The failure could be a consequence of something going wrong on the
+                    // server side. Do nothing here since it's just logging, but it's
+                    // possible follow-up actions may fail.
+                }
+            }
+
+            final int itemCount = ids.size();
+            int numApplied = 0;
+            ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null;
+
             ArrayList<AutofillId> failedIds = new ArrayList<>();
+            ArrayList<AutofillValue> failedAutofillValues = new ArrayList<>();
 
             if (mLastAutofilledData == null) {
                 mLastAutofilledData = new ParcelableMap(itemCount);
@@ -3237,7 +3367,9 @@
                     // the service; this is fine, but we need to update the view status in the
                     // server side so it can be triggered again.
                     Log.d(TAG, "autofill(): no View with id " + id);
+                    // Possible relayout scenario
                     failedIds.add(id);
+                    failedAutofillValues.add(value);
                     continue;
                 }
                 // Mark the view as to be autofilled with 'value'
@@ -3268,7 +3400,8 @@
                 }
             }
 
-            handleFailedIdsLocked(failedIds);
+            handleFailedIdsLocked(
+                    failedIds, failedAutofillValues, hideHighlight, isRefill);
 
             if (virtualValues != null) {
                 for (int i = 0; i < virtualValues.size(); i++) {
@@ -3322,7 +3455,7 @@
     private void reportAutofillContentFailure(AutofillId id) {
         try {
             mService.setAutofillFailure(mSessionId, Collections.singletonList(id),
-                    mContext.getUserId());
+                    false /* isRefill */, mContext.getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -3349,20 +3482,22 @@
     }
 
     /**
-     *  Set the tracked views.
+     * Set the tracked views.
      *
-     * @param trackedIds The views to be tracked.
+     * @param trackedIds              The views to be tracked.
      * @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible.
-     * @param saveOnFinish Finish the session once the activity is finished.
-     * @param fillableIds Views that might anchor FillUI.
-     * @param saveTriggerId View that when clicked triggers commit().
+     * @param saveOnFinish            Finish the session once the activity is finished.
+     * @param fillableIds             Views that might anchor FillUI.
+     * @param saveTriggerId           View that when clicked triggers commit().
      */
     private void setTrackedViews(int sessionId, @Nullable AutofillId[] trackedIds,
             boolean saveOnAllViewsInvisible, boolean saveOnFinish,
-            @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId) {
+            @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId,
+            boolean shouldGrabViewFingerprints) {
         if (saveTriggerId != null) {
             saveTriggerId.resetSessionId();
         }
+        final ArraySet<AutofillId> allFillableIds = new ArraySet<>();
         synchronized (mLock) {
             if (sVerbose) {
                 Log.v(TAG, "setTrackedViews(): sessionId=" + sessionId
@@ -3372,6 +3507,7 @@
                         + ", fillableIds=" + Arrays.toString(fillableIds)
                         + ", saveTrigerId=" + saveTriggerId
                         + ", mFillableIds=" + mFillableIds
+                        + ", shouldGrabViewFingerprints=" + shouldGrabViewFingerprints
                         + ", mEnabled=" + mEnabled
                         + ", mSessionId=" + mSessionId);
             }
@@ -3405,7 +3541,6 @@
                     trackedIds = null;
                 }
 
-                final ArraySet<AutofillId> allFillableIds = new ArraySet<>();
                 if (mFillableIds != null) {
                     allFillableIds.addAll(mFillableIds);
                 }
@@ -3424,6 +3559,12 @@
                     mTrackedViews = null;
                 }
             }
+            if (mRelayoutFix && shouldGrabViewFingerprints) {
+                // For all the views: tracked and others, calculate fingerprints and store them.
+                mAutofillStateFingerprint.setUseRelativePosition(mRelativePositionForRelayout);
+                mAutofillStateFingerprint.storeStatePriorToAuthentication(
+                        getClient(), allFillableIds);
+            }
         }
     }
 
@@ -3845,7 +3986,7 @@
 
     @GuardedBy("mLock")
     private boolean isPendingAuthenticationLocked() {
-        return mRelayoutFixDeprecated && mState == STATE_PENDING_AUTHENTICATION;
+        return (mRelayoutFixDeprecated || mRelayoutFix) && mState == STATE_PENDING_AUTHENTICATION;
     }
 
     @GuardedBy("mLock")
@@ -3858,7 +3999,7 @@
         return mState == STATE_FINISHED;
     }
 
-    private void post(Runnable runnable) {
+    void post(Runnable runnable) {
         final AutofillClient client = getClient();
         if (client == null) {
             if (sVerbose) Log.v(TAG, "ignoring post() because client is null");
@@ -4700,11 +4841,11 @@
         @Override
         public void setTrackedViews(int sessionId, AutofillId[] ids,
                 boolean saveOnAllViewsInvisible, boolean saveOnFinish, AutofillId[] fillableIds,
-                AutofillId saveTriggerId) {
+                AutofillId saveTriggerId, boolean shouldGrabViewFingerprints) {
             final AutofillManager afm = mAfm.get();
             if (afm != null) {
                 afm.post(() -> afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible,
-                        saveOnFinish, fillableIds, saveTriggerId));
+                        saveOnFinish, fillableIds, saveTriggerId, shouldGrabViewFingerprints));
             }
         }
 
diff --git a/core/java/android/view/autofill/AutofillStateFingerprint.java b/core/java/android/view/autofill/AutofillStateFingerprint.java
new file mode 100644
index 0000000..2db4285
--- /dev/null
+++ b/core/java/android/view/autofill/AutofillStateFingerprint.java
@@ -0,0 +1,352 @@
+/*
+ * 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 android.view.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Slog;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class manages and stores the autofillable views fingerprints for use in relayout situations.
+ * @hide
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public final class AutofillStateFingerprint {
+
+    ArrayList<AutofillId> mPriorAutofillIds;
+    ArrayList<Integer> mViewHashCodes; // each entry corresponding to mPriorAutofillIds .
+
+    boolean mHideHighlight = false;
+
+    private int mSessionId;
+
+    Map<Integer, AutofillId> mHashToAutofillIdMap = new ArrayMap<>();
+    Map<AutofillId, AutofillId> mOldIdsToCurrentAutofillIdMap = new ArrayMap<>();
+
+    // These failed id's are attempted to be refilled again after relayout.
+    private ArrayList<AutofillId> mFailedIds = new ArrayList<>();
+    private ArrayList<AutofillValue> mFailedAutofillValues = new ArrayList<>();
+
+    // whether to use relative positions for computing hashes.
+    private boolean mUseRelativePosition;
+
+    private static final String TAG = "AutofillStateFingerprint";
+
+    /**
+     * Returns an instance of this class
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public static AutofillStateFingerprint createInstance() {
+        return new AutofillStateFingerprint();
+    }
+
+    private AutofillStateFingerprint() {
+    }
+
+    /**
+     * Set sessionId for the instance
+     */
+    void setSessionId(int sessionId) {
+        mSessionId = sessionId;
+    }
+
+    /**
+     * Sets whether relative position of the views should be used to calculate fingerprints.
+     */
+    void setUseRelativePosition(boolean useRelativePosition) {
+        mUseRelativePosition = useRelativePosition;
+    }
+
+    /**
+     * Store the state of the views prior to the authentication.
+     */
+    void storeStatePriorToAuthentication(
+            AutofillManager.AutofillClient client, Set<AutofillId> autofillIds) {
+        if (mUseRelativePosition) {
+            List<View> autofillableViews = client.autofillClientFindAutofillableViewsByTraversal();
+            if (sDebug) {
+                Log.d(TAG, "Autofillable views count prior to auth:" + autofillableViews.size());
+            }
+//            ArrayList<Integer> hashes = getFingerprintIds(autofillableViews);
+
+            ArrayMap<Integer, View> hashes = getFingerprintIds(autofillableViews);
+            for (Map.Entry<Integer, View> entry : hashes.entrySet()) {
+                View view = entry.getValue();
+                if (view != null) {
+                    mHashToAutofillIdMap.put(entry.getKey(), view.getAutofillId());
+                } else {
+                    if (sDebug) {
+                        Log.d(TAG, "Encountered null view");
+                    }
+                }
+            }
+        } else {
+            // Just use the provided autofillIds and get their hashes
+            if (sDebug) {
+                Log.d(TAG, "Size of autofillId's being stored: " + autofillIds.size()
+                        + " list:" + autofillIds);
+            }
+            AutofillId[] autofillIdsArr = Helper.toArray(autofillIds);
+            View[] views = client.autofillClientFindViewsByAutofillIdTraversal(autofillIdsArr);
+            for (int i = 0; i < autofillIdsArr.length; i++) {
+                View view = views[i];
+                if (view != null) {
+                    int id = getEphemeralFingerprintId(view, 0 /* position irrelevant */);
+                    AutofillId autofillId = view.getAutofillId();
+                    autofillId.setSessionId(mSessionId);
+                    mHashToAutofillIdMap.put(id, autofillId);
+                } else {
+                    if (sDebug) {
+                        Log.d(TAG, "Encountered null view");
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Store failed ids, so that they can be refilled later
+     */
+    void storeFailedIdsAndValues(
+            @NonNull ArrayList<AutofillId> failedIds,
+            ArrayList<AutofillValue> failedAutofillValues,
+            boolean hideHighlight) {
+        for (AutofillId failedId : failedIds) {
+            if (failedId != null) {
+                failedId.setSessionId(mSessionId);
+            } else {
+                if (sDebug) {
+                    Log.d(TAG, "Got null failed ids");
+                }
+            }
+        }
+        mFailedIds = failedIds;
+        mFailedAutofillValues = failedAutofillValues;
+        mHideHighlight = hideHighlight;
+    }
+
+    private void dumpCurrentState() {
+        Log.d(TAG, "FailedId's: " + mFailedIds);
+        Log.d(TAG, "Hashes from map" + mHashToAutofillIdMap);
+    }
+
+    boolean attemptRefill(
+            List<View> currentAutofillableViews, @NonNull AutofillManager autofillManager) {
+        if (sDebug) {
+            dumpCurrentState();
+        }
+        // For the autofillable views, compute their hashes
+        ArrayMap<Integer, View> currentHashes = getFingerprintIds(currentAutofillableViews);
+
+        // For the computed hashes, try to look for the old fingerprints.
+        // If match found, update the new autofill ids of those views
+        Map<AutofillId, View> oldFailedIdsToCurrentViewMap = new HashMap<>();
+        for (Map.Entry<Integer, View> entry : currentHashes.entrySet()) {
+            View view = entry.getValue();
+            int currentHash = entry.getKey();
+            AutofillId currentAutofillId = view.getAutofillId();
+            currentAutofillId.setSessionId(mSessionId);
+            if (mHashToAutofillIdMap.containsKey(currentHash)) {
+                AutofillId oldAutofillId = mHashToAutofillIdMap.get(currentHash);
+                oldAutofillId.setSessionId(mSessionId);
+                mOldIdsToCurrentAutofillIdMap.put(oldAutofillId, currentAutofillId);
+                Log.i(TAG, "Mapping current autofill id: " + view.getAutofillId()
+                        + " to existing autofill id " + oldAutofillId);
+
+                oldFailedIdsToCurrentViewMap.put(oldAutofillId, view);
+            } else {
+                Log.i(TAG, "Couldn't map current autofill id: " + view.getAutofillId()
+                        + " with currentHash:" + currentHash + " for view:" + view);
+            }
+        }
+
+        int viewsCount = 0;
+        View[] views = new View[mFailedIds.size()];
+        for (int i = 0; i < mFailedIds.size(); i++) {
+            AutofillId oldAutofillId = mFailedIds.get(i);
+            AutofillId currentAutofillId = mOldIdsToCurrentAutofillIdMap.get(oldAutofillId);
+            if (currentAutofillId == null) {
+                if (sDebug) {
+                    Log.d(TAG, "currentAutofillId = null");
+                }
+            }
+            mFailedIds.set(i, currentAutofillId);
+            views[i] = oldFailedIdsToCurrentViewMap.get(oldAutofillId);
+            if (views[i] != null) {
+                viewsCount++;
+            }
+        }
+
+        if (sDebug) {
+            dumpCurrentState();
+        }
+
+        // Attempt autofill now
+        Slog.i(TAG, "Attempting refill of views. Found " + viewsCount
+                + " views to refill from previously " + mFailedIds.size()
+                + " failed ids:" + mFailedIds);
+        autofillManager.post(
+                () -> autofillManager.autofill(
+                        views, mFailedIds, mFailedAutofillValues, mHideHighlight,
+                        true /* isRefill */));
+
+        return false;
+    }
+
+    /**
+     * Retrieves fingerprint hashes for the views
+     */
+    ArrayMap<Integer, View> getFingerprintIds(@NonNull List<View> views) {
+        ArrayMap<Integer, View> map = new ArrayMap<>();
+        if (mUseRelativePosition) {
+            Collections.sort(views, (View v1, View v2) -> {
+                int[] posV1 = v1.getLocationOnScreen();
+                int[] posV2 = v2.getLocationOnScreen();
+
+                int compare = posV1[0] - posV2[0]; // x coordinate
+                if (compare != 0) {
+                    return compare;
+                }
+                compare = posV1[1] - posV2[1]; // y coordinate
+                if (compare != 0) {
+                    return compare;
+                }
+                // Sort on vertical
+                compare = compareTop(v1, v2);
+                if (compare != 0) {
+                    return compare;
+                }
+                compare = compareBottom(v1, v2);
+                if (compare != 0) {
+                    return compare;
+                }
+                compare = compareLeft(v1, v2);
+                if (compare != 0) {
+                    return compare;
+                }
+                return compareRight(v1, v2);
+                // Note that if compareRight also returned 0, that means both the views have exact
+                // same location, so just treat them as equal
+            });
+        }
+        for (int i = 0; i < views.size(); i++) {
+            View view = views.get(i);
+            map.put(getEphemeralFingerprintId(view, i), view);
+        }
+        return map;
+    }
+
+    /**
+     * Returns fingerprint hash for the view.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public int getEphemeralFingerprintId(View v, int position) {
+        if (v == null) return -1;
+        int inputType = Integer.MIN_VALUE;
+        int imeOptions = Integer.MIN_VALUE;
+        boolean isSingleLine = false;
+        CharSequence hints = "";
+        if (v instanceof TextView) {
+            TextView tv = (TextView) v;
+            inputType = tv.getInputType();
+            hints = tv.getHint();
+            isSingleLine = tv.isSingleLine();
+            imeOptions = tv.getImeOptions();
+            // TODO(b/238252288): Consider adding more IME related fields.
+        }
+        CharSequence contentDesc = v.getContentDescription();
+        CharSequence tooltip = v.getTooltipText();
+
+        int autofillType = v.getAutofillType();
+        String[] autofillHints = v.getAutofillHints();
+        int visibility = v.getVisibility();
+
+        int paddingLeft = v.getPaddingLeft();
+        int paddingRight = v.getPaddingRight();
+        int paddingTop = v.getPaddingTop();
+        int paddingBottom = v.getPaddingBottom();
+
+        // TODO(b/238252288): Following are making relayout flaky. Do more analysis to figure out
+        //  why.
+        int height = v.getHeight();
+        int width = v.getWidth();
+
+        // Order doesn't matter much here. We can change the order, as long as we use the same
+        // order for storing and fetching fingerprints. The order can be changed in platform
+        // versions.
+        int hash = Objects.hash(visibility, inputType, imeOptions, isSingleLine, hints,
+                contentDesc, tooltip, autofillType, Arrays.deepHashCode(autofillHints),
+                paddingBottom, paddingTop, paddingRight, paddingLeft);
+        if (mUseRelativePosition) {
+            hash = Objects.hash(hash, position);
+        }
+        if (sDebug) {
+            Log.d(TAG, "Hash: " + hash + " for AutofillId:" + v.getAutofillId()
+                    + " visibility:" + visibility
+                    + " inputType:" + inputType
+                    + " imeOptions:" + imeOptions
+                    + " isSingleLine:" + isSingleLine
+                    + " hints:" + hints
+                    + " contentDesc:" + contentDesc
+                    + " tooltipText:" + tooltip
+                    + " autofillType:" + autofillType
+                    + " autofillHints:" + Arrays.toString(autofillHints)
+                    + " height:" + height
+                    + " width:" + width
+                    + " paddingLeft:" + paddingLeft
+                    + " paddingRight:" + paddingRight
+                    + " paddingTop:" + paddingTop
+                    + " paddingBottom:" + paddingBottom
+                    + " mUseRelativePosition" + mUseRelativePosition
+                    + " position:" + position
+            );
+        }
+        return hash;
+    }
+
+    private int compareTop(View v1, View v2) {
+        return v1.getTop() - v2.getTop();
+    }
+
+    private int compareBottom(View v1, View v2) {
+        return v1.getBottom() - v2.getBottom();
+    }
+
+    private int compareLeft(View v1, View v2) {
+        return v1.getLeft() - v2.getLeft();
+    }
+
+    private int compareRight(View v1, View v2) {
+        return v1.getRight() - v2.getRight();
+    }
+}
diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl
index 2039b4d..f67405f 100644
--- a/core/java/android/view/autofill/IAutoFillManager.aidl
+++ b/core/java/android/view/autofill/IAutoFillManager.aidl
@@ -48,7 +48,7 @@
         in IResultReceiver result);
     void updateSession(int sessionId, in AutofillId id, in Rect bounds,
         in AutofillValue value, int action, int flags, int userId);
-    void setAutofillFailure(int sessionId, in List<AutofillId> ids, int userId);
+    void setAutofillFailure(int sessionId, in List<AutofillId> ids, boolean isRefill, int userId);
     void setViewAutofilled(int sessionId, in AutofillId id, int userId);
     void finishSession(int sessionId, int userId, int commitReason);
     void cancelSession(int sessionId, int userId);
@@ -67,4 +67,7 @@
     void getDefaultFieldClassificationAlgorithm(in IResultReceiver result);
     void setAugmentedAutofillWhitelist(in List<String> packages, in List<ComponentName> activities,
         in IResultReceiver result);
+    void notifyNotExpiringResponseDuringAuth(int sessionId, int userId);
+    void notifyViewEnteredIgnoredDuringAuthCount(int sessionId, int userId);
+    void setAutofillIdsAttemptedForRefill(int sessionId, in List<AutofillId> ids, int userId);
 }
diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
index 904a7e0..39d71da 100644
--- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl
+++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl
@@ -72,7 +72,8 @@
       */
     void setTrackedViews(int sessionId, in @nullable AutofillId[] savableIds,
             boolean saveOnAllViewsInvisible, boolean saveOnFinish,
-            in @nullable AutofillId[] fillableIds, in AutofillId saveTriggerId);
+            in @nullable AutofillId[] fillableIds, in AutofillId saveTriggerId,
+            in boolean shouldGrabViewFingerprints);
 
     /**
      * Requests showing the fill UI.
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index 07a9794..2f515fe 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -196,16 +196,22 @@
 
     /**
      * Invokes {@link IInputMethodManager#removeImeSurface()}
+     *
+     * @param displayId display ID from which this request originates
+     * @param exceptionHandler an optional {@link RemoteException} handler
      */
     @AnyThread
-    @RequiresPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
-    static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) {
+    @RequiresPermission(allOf = {
+            Manifest.permission.INTERNAL_SYSTEM_WINDOW,
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL})
+    static void removeImeSurface(int displayId,
+            @Nullable Consumer<RemoteException> exceptionHandler) {
         final IInputMethodManager service = getService();
         if (service == null) {
             return;
         }
         try {
-            service.removeImeSurface();
+            service.removeImeSurface(displayId);
         } catch (RemoteException e) {
             handleRemoteExceptionOrRethrow(e, exceptionHandler);
         }
@@ -437,7 +443,9 @@
     }
 
     @AnyThread
-    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @RequiresPermission(allOf = {
+            Manifest.permission.WRITE_SECURE_SETTINGS,
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL})
     static void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
         final IInputMethodManager service = getService();
         if (service == null) {
@@ -465,7 +473,9 @@
     }
 
     @AnyThread
-    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @RequiresPermission(allOf = {
+            Manifest.permission.WRITE_SECURE_SETTINGS,
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL})
     static void onImeSwitchButtonClickFromSystem(int displayId) {
         final IInputMethodManager service = getService();
         if (service == null) {
diff --git a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
index 5df9fd1..244b239 100644
--- a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
+++ b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
@@ -95,11 +95,13 @@
     /**
      * Invokes {@link IInputMethodManager#removeImeSurface()}
      *
+     * @param displayId display ID from which this request originates.
      * @param exceptionHandler an optional {@link RemoteException} handler.
      */
     @AnyThread
     @RequiresPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
-    public static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) {
-        IInputMethodManagerGlobalInvoker.removeImeSurface(exceptionHandler);
+    public static void removeImeSurface(int displayId,
+            @Nullable Consumer<RemoteException> exceptionHandler) {
+        IInputMethodManagerGlobalInvoker.removeImeSurface(displayId, exceptionHandler);
     }
 }
diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl
index 478aeec..1748b9d 100644
--- a/core/java/android/window/ITaskOrganizerController.aidl
+++ b/core/java/android/window/ITaskOrganizerController.aidl
@@ -67,7 +67,4 @@
      * Restarts the top activity in the given task by killing its process if it is visible.
      */
     void restartTaskTopActivityProcessIfVisible(in WindowContainerToken task);
-
-    /** Updates a state of camera compat control for stretched issues in the viewfinder. */
-    void updateCameraCompatControlState(in WindowContainerToken task, int state);
 }
diff --git a/core/java/android/window/TaskFragmentInfo.java b/core/java/android/window/TaskFragmentInfo.java
index fa51957..23a1224 100644
--- a/core/java/android/window/TaskFragmentInfo.java
+++ b/core/java/android/window/TaskFragmentInfo.java
@@ -102,6 +102,8 @@
     @NonNull
     private final Point mMinimumDimensions = new Point();
 
+    private final boolean mIsTopNonFishingChild;
+
     /** @hide */
     public TaskFragmentInfo(
             @NonNull IBinder fragmentToken, @NonNull WindowContainerToken token,
@@ -110,7 +112,7 @@
             @NonNull List<IBinder> inRequestedTaskFragmentActivities,
             @NonNull Point positionInParent, boolean isTaskClearedForReuse,
             boolean isTaskFragmentClearedForPip, boolean isClearedForReorderActivityToFront,
-            @NonNull Point minimumDimensions) {
+            @NonNull Point minimumDimensions, boolean isTopNonFinishingChild) {
         mFragmentToken = requireNonNull(fragmentToken);
         mToken = requireNonNull(token);
         mConfiguration.setTo(configuration);
@@ -123,6 +125,7 @@
         mIsTaskFragmentClearedForPip = isTaskFragmentClearedForPip;
         mIsClearedForReorderActivityToFront = isClearedForReorderActivityToFront;
         mMinimumDimensions.set(minimumDimensions);
+        mIsTopNonFishingChild = isTopNonFinishingChild;
     }
 
     @NonNull
@@ -212,6 +215,16 @@
     }
 
     /**
+     * Indicates that this TaskFragment is the top non-finishing child of its parent container
+     * among all Activities and TaskFragment siblings.
+     *
+     * @hide
+     */
+    public boolean isTopNonFinishingChild() {
+        return mIsTopNonFishingChild;
+    }
+
+    /**
      * Returns {@code true} if the parameters that are important for task fragment organizers are
      * equal between this {@link TaskFragmentInfo} and {@param that}.
      * Note that this method is usually called with
@@ -236,7 +249,8 @@
                 && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse
                 && mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip
                 && mIsClearedForReorderActivityToFront == that.mIsClearedForReorderActivityToFront
-                && mMinimumDimensions.equals(that.mMinimumDimensions);
+                && mMinimumDimensions.equals(that.mMinimumDimensions)
+                && mIsTopNonFishingChild == that.mIsTopNonFishingChild;
     }
 
     private TaskFragmentInfo(Parcel in) {
@@ -252,6 +266,7 @@
         mIsTaskFragmentClearedForPip = in.readBoolean();
         mIsClearedForReorderActivityToFront = in.readBoolean();
         mMinimumDimensions.readFromParcel(in);
+        mIsTopNonFishingChild = in.readBoolean();
     }
 
     /** @hide */
@@ -269,6 +284,7 @@
         dest.writeBoolean(mIsTaskFragmentClearedForPip);
         dest.writeBoolean(mIsClearedForReorderActivityToFront);
         mMinimumDimensions.writeToParcel(dest, flags);
+        dest.writeBoolean(mIsTopNonFishingChild);
     }
 
     @NonNull
@@ -299,6 +315,7 @@
                 + " isTaskFragmentClearedForPip=" + mIsTaskFragmentClearedForPip
                 + " mIsClearedForReorderActivityToFront=" + mIsClearedForReorderActivityToFront
                 + " minimumDimensions=" + mMinimumDimensions
+                + " isTopNonFinishingChild=" + mIsTopNonFishingChild
                 + "}";
     }
 
diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java
index b9ffdbc..3ecb619 100644
--- a/core/java/android/window/TaskOrganizer.java
+++ b/core/java/android/window/TaskOrganizer.java
@@ -24,7 +24,6 @@
 import android.annotation.SuppressLint;
 import android.annotation.TestApi;
 import android.app.ActivityManager;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.view.SurfaceControl;
@@ -252,20 +251,6 @@
     }
 
     /**
-     * Updates a state of camera compat control for stretched issues in the viewfinder.
-     * @hide
-     */
-    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
-    public void updateCameraCompatControlState(@NonNull WindowContainerToken task,
-            @CameraCompatControlState int state) {
-        try {
-            mTaskOrganizerController.updateCameraCompatControlState(task, state);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Gets the executor to run callbacks on.
      * @hide
      */
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 725d496..e5a9b6a 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -65,14 +65,6 @@
 }
 
 flag {
-    name: "defer_display_updates"
-    namespace: "windowing_frontend"
-    description: "Feature flag for deferring DisplayManager updates to WindowManager if Shell transition is running"
-    bug: "259220649"
-    is_fixed_read_only: true
-}
-
-flag {
   name: "close_to_square_config_includes_status_bar"
   namespace: "windowing_frontend"
   description: "On close to square display, when necessary, configuration includes status bar"
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 4230641..4c18bbf 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -51,13 +51,6 @@
 
 flag {
     namespace: "windowing_sdk"
-    name: "embedded_activity_back_nav_flag"
-    description: "Refines embedded activity back navigation behavior"
-    bug: "293642394"
-}
-
-flag {
-    namespace: "windowing_sdk"
     name: "cover_display_opt_in"
     is_exported: true
     description: "Properties to allow apps and activities to opt-in to cover display rendering"
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index f9c2947..e8831ec 100644
--- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
+++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -36,6 +36,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.database.ContentObserver;
@@ -63,6 +64,7 @@
 import com.android.internal.R;
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.internal.accessibility.util.ShortcutUtils;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.lang.annotation.Retention;
@@ -122,6 +124,13 @@
             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
             .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
             .build();
+
+    /**
+     * An intent action to launch Extra Dim dialog.
+     */
+    @VisibleForTesting
+    static final String ACTION_LAUNCH_REMOVE_EXTRA_DIM_DIALOG =
+            "com.android.systemui.action.LAUNCH_REMOVE_EXTRA_DIM_DIALOG";
     private static Map<ComponentName, FrameworkFeatureInfo> sFrameworkShortcutFeaturesMap;
 
     private final Context mContext;
@@ -846,7 +855,7 @@
             if (com.android.server.display.feature.flags.Flags.evenDimmer()
                     && context.getResources().getBoolean(
                     com.android.internal.R.bool.config_evenDimmerEnabled)) {
-                launchExtraDimDialog();
+                launchExtraDimDialog(context);
                 return true;
             } else {
                 // Assuming that the default state will be to have the feature off
@@ -863,8 +872,12 @@
             }
         }
 
-        private void launchExtraDimDialog() {
-            // TODO: launch Extra dim dialog for feature migration
+        private void launchExtraDimDialog(Context context) {
+            final Intent intent = new Intent(ACTION_LAUNCH_REMOVE_EXTRA_DIM_DIALOG);
+            intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+            intent.setPackage(
+                    context.getString(com.android.internal.R.string.config_systemUi));
+            context.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
         }
     }
 
diff --git a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
index a3fcfad..44dceb9 100644
--- a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
+++ b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
@@ -72,7 +72,8 @@
             UserShortcutType.TRIPLETAP,
             UserShortcutType.TWOFINGER_DOUBLETAP,
             UserShortcutType.QUICK_SETTINGS,
-            UserShortcutType.GESTURE
+            UserShortcutType.GESTURE,
+            UserShortcutType.ALL
     })
     public @interface UserShortcutType {
         int DEFAULT = 0;
@@ -84,6 +85,7 @@
         int QUICK_SETTINGS = 1 << 4;
         int GESTURE = 1 << 5;
         // LINT.ThenChange(:shortcut_type_array)
+        int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE;
     }
 
     /**
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index ab456a8..6258f5c 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -544,6 +544,14 @@
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        if (Settings.Secure.getIntForUser(getContentResolver(),
+                Settings.Secure.SECURE_FRP_MODE, 0,
+                getUserId()) == 1) {
+            Log.e(TAG, "Sharing disabled due to active FRP lock.");
+            super.onCreate(savedInstanceState);
+            finish();
+            return;
+        }
         final long intentReceivedTime = System.currentTimeMillis();
         mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
 
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index cba27ce..b51678e 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -125,9 +125,9 @@
     void showInputMethodPickerFromClient(in IInputMethodClient client,
             int auxiliarySubtypeMode);
 
-    @EnforcePermission("WRITE_SECURE_SETTINGS")
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
-            + "android.Manifest.permission.WRITE_SECURE_SETTINGS)")
+    @EnforcePermission(allOf = {"WRITE_SECURE_SETTINGS", "INTERACT_ACROSS_USERS_FULL"})
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf = {android.Manifest."
+    + "permission.WRITE_SECURE_SETTINGS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})")
     void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId);
 
     @EnforcePermission("TEST_INPUT_METHOD")
@@ -143,9 +143,9 @@
      * @param displayId The ID of the display where the input method picker dialog should be shown.
      * @param userId    The ID of the user that triggered the click.
      */
-    @EnforcePermission("WRITE_SECURE_SETTINGS")
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
-            + "android.Manifest.permission.WRITE_SECURE_SETTINGS)")
+    @EnforcePermission(allOf = {"WRITE_SECURE_SETTINGS" ,"INTERACT_ACROSS_USERS_FULL"})
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf = {android.Manifest."
+    + "permission.WRITE_SECURE_SETTINGS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})")
     oneway void onImeSwitchButtonClickFromSystem(int displayId);
 
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
@@ -168,10 +168,10 @@
 
     oneway void reportPerceptibleAsync(in IBinder windowToken, boolean perceptible);
 
-    @EnforcePermission("INTERNAL_SYSTEM_WINDOW")
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
-            + "android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)")
-    void removeImeSurface();
+    @EnforcePermission(allOf = {"INTERNAL_SYSTEM_WINDOW", "INTERACT_ACROSS_USERS_FULL"})
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf = {android.Manifest."
+    + "permission.INTERNAL_SYSTEM_WINDOW, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})")
+    void removeImeSurface(int displayId);
 
     /** Remove the IME surface. Requires passing the currently focused window. */
     oneway void removeImeSurfaceFromWindowAsync(in IBinder windowToken);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
index fc8668e..4b8dbf6 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -37,6 +37,7 @@
 import com.android.internal.widget.remotecompose.core.operations.FloatConstant;
 import com.android.internal.widget.remotecompose.core.operations.FloatExpression;
 import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.IntegerExpression;
 import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
 import com.android.internal.widget.remotecompose.core.operations.MatrixRotate;
 import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
@@ -54,6 +55,8 @@
 import com.android.internal.widget.remotecompose.core.operations.TextMerge;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
 import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap;
+import com.android.internal.widget.remotecompose.core.types.BooleanConstant;
+import com.android.internal.widget.remotecompose.core.types.IntegerConstant;
 
 /**
  * List of operations supported in a RemoteCompose document
@@ -109,6 +112,9 @@
     public static final int TEXT_MERGE = 136;
     public static final int NAMED_VARIABLE = 137;
     public static final int COLOR_CONSTANT = 138;
+    public static final int DATA_INT = 140;
+    public static final int DATA_BOOLEAN = 143;
+    public static final int INTEGER_EXPRESSION = 144;
 
     /////////////////////////////////////////======================
     public static IntMap<CompanionOperation> map = new IntMap<>();
@@ -153,6 +159,9 @@
         map.put(TEXT_MERGE, TextMerge.COMPANION);
         map.put(NAMED_VARIABLE, NamedVariable.COMPANION);
         map.put(COLOR_CONSTANT, ColorConstant.COMPANION);
+        map.put(DATA_INT, IntegerConstant.COMPANION);
+        map.put(INTEGER_EXPRESSION, IntegerExpression.COMPANION);
+        map.put(DATA_BOOLEAN, BooleanConstant.COMPANION);
     }
 
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
index ecd0efc..6d8a442 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
@@ -59,6 +59,16 @@
 
     public abstract void drawRect(float left, float top, float right, float bottom);
 
+    /**
+     * this caches the paint to a paint stack
+     */
+    public abstract void  savePaint();
+
+    /**
+     * This restores the paint form the paint stack
+     */
+    public abstract void  restorePaint();
+
     public abstract void drawRoundRect(float left,
                                        float top,
                                        float right,
@@ -119,6 +129,10 @@
                                        float start,
                                        float stop);
 
+    /**
+     * This applies changes to the current paint
+     * @param mPaintData the list of changes
+     */
     public abstract void applyPaint(PaintBundle mPaintData);
 
     /**
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
index d462c7d..f5f155e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -37,6 +37,7 @@
 import com.android.internal.widget.remotecompose.core.operations.FloatConstant;
 import com.android.internal.widget.remotecompose.core.operations.FloatExpression;
 import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.IntegerExpression;
 import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
 import com.android.internal.widget.remotecompose.core.operations.MatrixRotate;
 import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
@@ -55,6 +56,7 @@
 import com.android.internal.widget.remotecompose.core.operations.Utils;
 import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
 import com.android.internal.widget.remotecompose.core.operations.utilities.easing.FloatAnimation;
+import com.android.internal.widget.remotecompose.core.types.IntegerConstant;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -876,6 +878,27 @@
         return Utils.asNan(id);
     }
 
+
+    /**
+     * Add a Integer return an id number pointing to that float.
+     * @param value
+     * @return
+     */
+    public int addInteger(int value) {
+        int id = mRemoteComposeState.cacheInteger(value);
+        IntegerConstant.COMPANION.apply(mBuffer, id, value);
+        return id;
+    }
+
+    /**
+     * Add a IntegerId as float ID.
+     * @param id id to be converted
+     * @return
+     */
+    public float asFloatId(int id) {
+        return Utils.asNan(id);
+    }
+
     /**
      * Add a float that is a computation based on variables
      * @param value A RPN style float operation i.e. "4, 3, ADD" outputs 7
@@ -901,6 +924,18 @@
     }
 
     /**
+     * Add and integer expression
+     * @param mask defines which elements are operators or variables
+     * @param value array of values to calculate maximum 32
+     * @return
+     */
+    public int addIntegerExpression(int mask, int[] value) {
+        int id = mRemoteComposeState.cache(value);
+        IntegerExpression.COMPANION.apply(mBuffer, id, mask, value);
+        return  id;
+    }
+
+    /**
      * Add a simple color
      * @param color
      * @return id that represents that color
@@ -1038,5 +1073,6 @@
         NamedVariable.COMPANION.apply(mBuffer, id,
                 NamedVariable.COLOR_TYPE, name);
     }
+
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
index bfe67c8..6b06a54 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeState.java
@@ -21,6 +21,8 @@
 import static com.android.internal.widget.remotecompose.core.RemoteContext.ID_WINDOW_HEIGHT;
 import static com.android.internal.widget.remotecompose.core.RemoteContext.ID_WINDOW_WIDTH;
 
+import com.android.internal.widget.remotecompose.core.operations.utilities.IntFloatMap;
+import com.android.internal.widget.remotecompose.core.operations.utilities.IntIntMap;
 import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap;
 
 import java.util.ArrayList;
@@ -37,16 +39,12 @@
     private final IntMap<Object> mIntDataMap = new IntMap<>();
     private final IntMap<Boolean> mIntWrittenMap = new IntMap<>();
     private final HashMap<Object, Integer> mDataIntMap = new HashMap();
-    private final float[] mFloatMap = new float[MAX_FLOATS]; // efficient cache
-    private final int[] mColorMap = new int[MAX_COLORS]; // efficient cache
+    private final IntFloatMap mFloatMap = new IntFloatMap(); // efficient cache
+    private final IntIntMap mIntegerMap = new IntIntMap(); // efficient cache
+    private final IntIntMap mColorMap = new IntIntMap(); // efficient cache
     private final boolean[] mColorOverride = new boolean[MAX_COLORS];
     private int mNextId = START_ID;
 
-    {
-        for (int i = 0; i < mFloatMap.length; i++) {
-            mFloatMap[i] = Float.NaN;
-        }
-    }
 
     /**
      * Get Object based on id. The system will cache things like bitmaps
@@ -113,7 +111,18 @@
      */
     public int cacheFloat(float item) {
         int id = nextId();
-        mFloatMap[id] = item;
+        mFloatMap.put(id, item);
+        mIntegerMap.put(id, (int) item);
+        return id;
+    }
+
+    /**
+     * Insert an item in the cache
+     */
+    public int cacheInteger(int item) {
+        int id = nextId();
+        mIntegerMap.put(id, item);
+        mFloatMap.put(id, item);
         return id;
     }
 
@@ -121,21 +130,43 @@
      * Insert an item in the cache
      */
     public void cacheFloat(int id, float item) {
-        mFloatMap[id] = item;
+        mFloatMap.put(id, item);
     }
 
     /**
-     * Insert an item in the cache
+     * Insert an float item in the cache
      */
     public void updateFloat(int id, float item) {
-        mFloatMap[id] = item;
+        mFloatMap.put(id, item);
+        mIntegerMap.put(id, (int) item);
     }
 
     /**
-     * get float
+     * Insert an integer item in the cache
+     */
+    public void updateInteger(int id, int item) {
+        mFloatMap.put(id, item);
+        mIntegerMap.put(id, item);
+    }
+
+    /**
+     * get a float from the float cache
+     *
+     * @param id of the float value
+     * @return the float value
      */
     public float getFloat(int id) {
-        return mFloatMap[id];
+        return mFloatMap.get(id);
+    }
+
+    /**
+     * get an integer from the cache
+     *
+     * @param id of the integer value
+     * @return the integer
+     */
+    public int getInteger(int id) {
+        return mIntegerMap.get(id);
     }
 
     /**
@@ -145,11 +176,12 @@
      * @return
      */
     public int getColor(int id) {
-        return mColorMap[id];
+        return mColorMap.get(id);
     }
 
     /**
      * Modify the color at id.
+     *
      * @param id
      * @param color
      */
@@ -157,7 +189,7 @@
         if (mColorOverride[id]) {
             return;
         }
-        mColorMap[id] = color;
+        mColorMap.put(id, color);
     }
 
     /**
@@ -169,7 +201,7 @@
      */
     public void overrideColor(int id, int color) {
         mColorOverride[id] = true;
-        mColorMap[id] = color;
+        mColorMap.put(id, color);
     }
 
     /**
@@ -205,6 +237,7 @@
 
     /**
      * Get the next available id
+     *
      * @return
      */
     public int nextId() {
@@ -213,6 +246,7 @@
 
     /**
      * Set the next id
+     *
      * @param id
      */
     public void setNextId(int id) {
@@ -234,6 +268,7 @@
 
     /**
      * Commands that listen to variables add themselves.
+     *
      * @param id
      * @param variableSupport
      */
@@ -243,6 +278,7 @@
 
     /**
      * List of Commands that need to be updated
+     *
      * @param context
      * @return
      */
@@ -264,6 +300,7 @@
 
     /**
      * Set the width of the overall document on screen.
+     *
      * @param width
      */
     public void setWindowWidth(float width) {
@@ -272,6 +309,7 @@
 
     /**
      * Set the width of the overall document on screen.
+     *
      * @param height
      */
     public void setWindowHeight(float height) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
index 32027d8..41eeb5b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
@@ -39,6 +39,7 @@
 
     public float mWidth = 0f;
     public float mHeight = 0f;
+    private float mAnimationTime;
 
     /**
      * Load a path under an id.
@@ -65,11 +66,20 @@
     public abstract void loadColor(int id, int color);
 
     /**
+     * Set the animation time allowing the creator to control animation rates
+     * @param time
+     */
+    public void setAnimationTime(float time) {
+        mAnimationTime = time;
+    }
+
+    /**
      * gets the time animation clock as float in seconds
      * @return a monotonic time in seconds (arbitrary zero point)
      */
     public float getAnimationTime() {
-        return (System.nanoTime() - mStart) * 1E-9f;
+        mAnimationTime = (System.nanoTime() - mStart) * 1E-9f; // Eliminate
+        return mAnimationTime;
     }
 
     /**
@@ -213,6 +223,13 @@
     public abstract void loadFloat(int id, float value);
 
     /**
+     * Load a float
+     * @param id
+     * @param value
+     */
+    public abstract void loadInteger(int id, int value);
+
+    /**
      * Load an animated float associated with an id
      * Todo: Remove?
      * @param id
@@ -235,6 +252,13 @@
     public abstract float getFloat(int id);
 
     /**
+     * Get a float given an id
+     * @param id
+     * @return
+     */
+    public abstract int getInteger(int id);
+
+    /**
      * Get the color given and ID
      * @param id
      * @return
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java
index 56b2f1f..5a4a9f3 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java
@@ -63,23 +63,23 @@
 
     @Override
     public void updateVariables(RemoteContext context) {
-        mV1 = (Float.isNaN(mValue1))
+        mV1 = (Utils.isVariable(mValue1))
                 ? context.getFloat(Utils.idFromNan(mValue1)) : mValue1;
-        mV2 = (Float.isNaN(mValue2))
+        mV2 = (Utils.isVariable(mValue2))
                 ? context.getFloat(Utils.idFromNan(mValue2)) : mValue2;
-        mV3 = (Float.isNaN(mValue3))
+        mV3 = (Utils.isVariable(mValue3))
                 ? context.getFloat(Utils.idFromNan(mValue3)) : mValue3;
     }
 
     @Override
     public void registerListening(RemoteContext context) {
-        if (Float.isNaN(mValue1)) {
+        if (Utils.isVariable(mValue1)) {
             context.listensTo(Utils.idFromNan(mValue1), this);
         }
-        if (Float.isNaN(mValue2)) {
+        if (Utils.isVariable(mValue2)) {
             context.listensTo(Utils.idFromNan(mValue2), this);
         }
-        if (Float.isNaN(mValue3)) {
+        if (Utils.isVariable(mValue3)) {
             context.listensTo(Utils.idFromNan(mValue3), this);
         }
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextRun.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextRun.java
deleted file mode 100644
index a099252..0000000
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextRun.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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.internal.widget.remotecompose.core.operations;
-
-import com.android.internal.widget.remotecompose.core.CompanionOperation;
-import com.android.internal.widget.remotecompose.core.Operation;
-import com.android.internal.widget.remotecompose.core.Operations;
-import com.android.internal.widget.remotecompose.core.PaintContext;
-import com.android.internal.widget.remotecompose.core.PaintOperation;
-import com.android.internal.widget.remotecompose.core.WireBuffer;
-
-import java.util.List;
-
-public class DrawTextRun extends PaintOperation {
-    public static final Companion COMPANION = new Companion();
-    int mTextID;
-    int mStart = 0;
-    int mEnd = 0;
-    int mContextStart = 0;
-    int mContextEnd = 0;
-    float mX = 0f;
-    float mY = 0f;
-    boolean mRtl = false;
-
-    public DrawTextRun(int textID,
-                       int start,
-                       int end,
-                       int contextStart,
-                       int contextEnd,
-                       float x,
-                       float y,
-                       boolean rtl) {
-        mTextID = textID;
-        mStart = start;
-        mEnd = end;
-        mContextStart = contextStart;
-        mContextEnd = contextEnd;
-        mX = x;
-        mY = y;
-        mRtl = rtl;
-    }
-
-    @Override
-    public void write(WireBuffer buffer) {
-        COMPANION.apply(buffer, mTextID, mStart, mEnd, mContextStart, mContextEnd, mX, mY, mRtl);
-
-    }
-
-    @Override
-    public String toString() {
-        return "";
-    }
-
-    public static class Companion implements CompanionOperation {
-        private Companion() {
-        }
-
-        @Override
-        public void read(WireBuffer buffer, List<Operation> operations) {
-            int text = buffer.readInt();
-            int start = buffer.readInt();
-            int end = buffer.readInt();
-            int contextStart = buffer.readInt();
-            int contextEnd = buffer.readInt();
-            float x = buffer.readFloat();
-            float y = buffer.readFloat();
-            boolean rtl = buffer.readBoolean();
-            DrawTextRun op = new DrawTextRun(text, start, end, contextStart, contextEnd, x, y, rtl);
-
-            operations.add(op);
-        }
-
-        @Override
-        public String name() {
-            return "";
-        }
-
-        @Override
-        public int id() {
-            return 0;
-        }
-
-        public void apply(WireBuffer buffer,
-                          int textID,
-                          int start,
-                          int end,
-                          int contextStart,
-                          int contextEnd,
-                          float x,
-                          float y,
-                          boolean rtl) {
-            buffer.start(Operations.DRAW_TEXT_RUN);
-            buffer.writeInt(textID);
-            buffer.writeInt(start);
-            buffer.writeInt(end);
-            buffer.writeInt(contextStart);
-            buffer.writeInt(contextEnd);
-            buffer.writeFloat(x);
-            buffer.writeFloat(y);
-            buffer.writeBoolean(rtl);
-        }
-    }
-
-    @Override
-    public void paint(PaintContext context) {
-        context.drawTextRun(mTextID, mStart, mEnd, mContextStart, mContextEnd, mX, mY, mRtl);
-    }
-}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java
new file mode 100644
index 0000000..d52df5d
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java
@@ -0,0 +1,177 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.VariableSupport;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.utilities.IntegerExpressionEvaluator;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Operation to deal with AnimatedFloats
+ * This is designed to be an optimized calculation for things like
+ * injecting the width of the component int draw rect
+ * As well as supporting generalized animation floats.
+ * The floats represent a RPN style calculator
+ */
+public class IntegerExpression implements Operation, VariableSupport {
+    public int mId;
+    private int mMask;
+    private int mPreMask;
+    public int[] mSrcValue;
+    public int[] mPreCalcValue;
+    private float mLastChange = Float.NaN;
+    public static final Companion COMPANION = new Companion();
+    public static final int MAX_STRING_SIZE = 4000;
+    IntegerExpressionEvaluator mExp = new IntegerExpressionEvaluator();
+
+    public IntegerExpression(int id, int mask, int[] value) {
+        this.mId = id;
+        this.mMask = mask;
+        this.mSrcValue = value;
+    }
+
+    @Override
+    public void updateVariables(RemoteContext context) {
+        if (mPreCalcValue == null || mPreCalcValue.length != mSrcValue.length) {
+            mPreCalcValue = new int[mSrcValue.length];
+        }
+        mPreMask = mMask;
+        for (int i = 0; i < mSrcValue.length; i++) {
+            if (isId(mMask, i, mSrcValue[i])) {
+                mPreMask &= ~(0x1 << i);
+                mPreCalcValue[i] = context.getInteger(mSrcValue[i]);
+            } else {
+                mPreCalcValue[i] = mSrcValue[i];
+            }
+        }
+    }
+
+
+    @Override
+    public void registerListening(RemoteContext context) {
+        for (int i = 0; i < mSrcValue.length; i++) {
+            if (isId(mMask, i, mSrcValue[i])) {
+                context.listensTo(mSrcValue[i], this);
+            }
+        }
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        updateVariables(context);
+        float t = context.getAnimationTime();
+        if (Float.isNaN(mLastChange)) {
+            mLastChange = t;
+        }
+        int v = mExp.eval(mPreMask, Arrays.copyOf(mPreCalcValue, mPreCalcValue.length));
+        context.loadInteger(mId, v);
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mId, mMask, mSrcValue);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder s = new StringBuilder();
+        for (int i = 0; i < mPreCalcValue.length; i++) {
+            if (i != 0) {
+                s.append(" ");
+            }
+            if (IntegerExpressionEvaluator.isOperation(mMask, i)) {
+                if (isId(mMask, i, mSrcValue[i])) {
+                    s.append("[" + mSrcValue[i] + "]");
+                } else {
+                    s.append(IntegerExpressionEvaluator.toMathName(mPreCalcValue[i]));
+                }
+            } else {
+                s.append(mSrcValue[i]);
+            }
+        }
+        return "IntegerExpression[" + mId + "] = (" + s + ")";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public String name() {
+            return "FloatExpression";
+        }
+
+        @Override
+        public int id() {
+            return Operations.INTEGER_EXPRESSION;
+        }
+
+        /**
+         * Writes out the operation to the buffer
+         *
+         * @param buffer
+         * @param id
+         * @param mask
+         * @param value
+         */
+        public void apply(WireBuffer buffer, int id, int mask, int[] value) {
+            buffer.start(Operations.INTEGER_EXPRESSION);
+            buffer.writeInt(id);
+            buffer.writeInt(mask);
+            buffer.writeInt(value.length);
+            for (int i = 0; i < value.length; i++) {
+                buffer.writeInt(value[i]);
+            }
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int id = buffer.readInt();
+            int mask = buffer.readInt();
+            int len = buffer.readInt();
+
+            int[] values = new int[len];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = buffer.readInt();
+            }
+
+            operations.add(new IntegerExpression(id, mask, values));
+        }
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return indent + toString();
+    }
+
+    /**
+     * given the "i" position in the mask is this an ID
+     * @param mask 32 bit mask used for defining numbers vs other
+     * @param i the bit in question
+     * @param value the value
+     * @return true if this is an ID
+     */
+    public static boolean isId(int mask, int i, int value) {
+        return ((1 << i) & mask) != 0 && value < IntegerExpressionEvaluator.OFFSET;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
index fcb3bfa..e9b0c3b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
@@ -40,7 +40,7 @@
      * @param n
      * @return
      */
-    static String trimString(String str, int n) {
+    public static String trimString(String str, int n) {
         if (str.length() > n) {
             str = str.substring(0, n - 3) + "...";
         }
@@ -55,6 +55,9 @@
      */
     public static String floatToString(float idvalue, float value) {
         if (Float.isNaN(idvalue)) {
+            if (idFromNan(value) == 0) {
+                return "NaN";
+            }
             return "[" + idFromNan(idvalue) + "]" + floatToString(value);
         }
         return floatToString(value);
@@ -67,6 +70,9 @@
      */
     public static String floatToString(float value) {
         if (Float.isNaN(value)) {
+            if (idFromNan(value) == 0) {
+                return "NaN";
+            }
             return "[" + idFromNan(value) + "]";
         }
         return Float.toString(value);
@@ -107,6 +113,7 @@
     public static boolean isVariable(float v) {
         if (Float.isNaN(v)) {
             int id = idFromNan(v);
+            if (id == 0) return false;
             return id > 40 || id < 10;
         }
         return false;
@@ -222,6 +229,4 @@
         }
         return 0;
     }
-
-
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/Painter.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/Painter.java
new file mode 100644
index 0000000..ada3757
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/Painter.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2023 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.internal.widget.remotecompose.core.operations.paint;
+
+
+/**
+ * Provides a Builder pattern for a PaintBundle
+ */
+class Painter {
+    PaintBundle mPaint;
+
+    /**
+     * Write the paint to the buffer
+     */
+    public PaintBundle commit() {
+        return mPaint;
+    }
+
+    public Painter setAntiAlias(boolean aa) {
+        mPaint.setAntiAlias(aa);
+        return this;
+    }
+
+    public Painter setColor(int color) {
+        mPaint.setColor(color);
+        return this;
+    }
+
+    public Painter setColorId(int colorId) {
+        mPaint.setColorId(colorId);
+        return this;
+    }
+
+    /**
+     * Set the paint's Join.
+     *
+     * @param join set the paint's Join, used whenever the paint's style is
+     *             Stroke or StrokeAndFill.
+     */
+    public Painter setStrokeJoin(int join) {
+        mPaint.setStrokeJoin(join);
+        return this;
+    }
+
+    /**
+     * Set the width for stroking. Pass 0 to stroke in hairline mode.
+     * Hairlines always draws a single
+     * pixel independent of the canvas's matrix.
+     *
+     * @param width set the paint's stroke width, used whenever the paint's
+     *             style is Stroke or StrokeAndFill.
+     */
+    public Painter setStrokeWidth(float width) {
+        mPaint.setStrokeWidth(width);
+        return this;
+    }
+
+    /**
+     * Set the paint's style, used for controlling how primitives' geometries
+     * are interpreted (except for drawBitmap, which always assumes Fill).
+     *
+     * @param style The new style to set in the paint
+     */
+    public Painter setStyle(int style) {
+        mPaint.setStyle(style);
+        return this;
+    }
+
+    /**
+     * Set the paint's Cap.
+     *
+     * @param cap set the paint's line cap style, used whenever the paint's
+     *           style is Stroke or StrokeAndFill.
+     */
+    public Painter setStrokeCap(int cap) {
+        mPaint.setStrokeCap(cap);
+        return this;
+    }
+
+    /**
+     * Set the paint's stroke miter value. This is used to control the behavior
+     * of miter joins when the joins angle is sharp. This value must be >= 0.
+     *
+     * @param miter set the miter limit on the paint, used whenever the paint's
+     *             style is Stroke or StrokeAndFill.
+     */
+    public Painter setStrokeMiter(float miter) {
+        mPaint.setStrokeMiter(miter);
+        return this;
+    }
+
+    /**
+     * Helper to setColor(), that only assigns the color's alpha value,
+     * leaving its r,g,b values unchanged. Results are undefined if the alpha
+     * value is outside of the range [0..1.0]
+     *
+     * @param alpha set the alpha component [0..1.0] of the paint's color.
+     */
+    public Painter setAlpha(float alpha) {
+        mPaint.setAlpha((alpha > 2) ? alpha / 255f : alpha);
+        return this;
+    }
+
+    /**
+     * Create a color filter that uses the specified color and Porter-Duff mode.
+     *
+     * @param color The ARGB source color used with the specified Porter-Duff
+     *             mode
+     * @param mode  The porter-duff mode that is applied
+     */
+    public Painter setPorterDuffColorFilter(int color, int mode) {
+        mPaint.setColorFilter(color, mode);
+        return this;
+    }
+
+    /**
+     * sets a shader that draws a linear gradient along a line.
+     *
+     * @param startX    The x-coordinate for the start of the gradient line
+     * @param startY    The y-coordinate for the start of the gradient line
+     * @param endX      The x-coordinate for the end of the gradient line
+     * @param endY      The y-coordinate for the end of the gradient line
+     * @param colors    The sRGB colors to be distributed along the gradient
+     *                  line
+     * @param positions May be null. The relative positions [0..1] of each
+     *                 corresponding color in the colors array. If this is null,
+     *                 the colors are distributed evenly along the gradient
+     *                 line.
+     * @param tileMode  The Shader tiling mode
+     */
+    public Painter setLinearGradient(
+            float startX,
+            float startY,
+            float endX,
+            float endY,
+            int[] colors,
+            float[] positions,
+            int tileMode
+    ) {
+        mPaint.setLinearGradient(colors, positions, startX,
+                startY, endX, endY, tileMode);
+        return this;
+    }
+
+    /**
+     * Sets a shader that draws a radial gradient given the center and radius.
+     *
+     * @param centerX   The x-coordinate of the center of the radius
+     * @param centerY   The y-coordinate of the center of the radius
+     * @param radius    Must be positive. The radius of the circle for this
+     *                  gradient.
+     * @param colors    The sRGB colors to be distributed between the center
+     *                  and edge of the circle
+     * @param positions May be <code>null</code>. Valid values are between
+     *                  <code>0.0f</code> and
+     *                  <code>1.0f</code>. The relative position of each
+     *                  corresponding color in the colors array. If
+     *                  <code>null</code>, colors are distributed evenly
+     *                  between the center and edge of the circle.
+     * @param tileMode  The Shader tiling mode
+     */
+    public Painter setRadialGradient(
+            float centerX,
+            float centerY,
+            float radius,
+            int[] colors,
+            float[] positions,
+            int tileMode
+    ) {
+        mPaint.setRadialGradient(colors, positions, centerX,
+                centerY, radius, tileMode);
+        return this;
+    }
+
+    /**
+     * Set a shader that draws a sweep gradient around a center point.
+     *
+     * @param centerX   The x-coordinate of the center
+     * @param centerY   The y-coordinate of the center
+     * @param colors    The sRGB colors to be distributed between around the
+     *                  center. There must be at least 2 colors in the array.
+     * @param positions May be NULL. The relative position of each corresponding
+     *                 color in the colors array, beginning with 0 and ending
+     *                 with 1.0. If the values are not monotonic, the drawing
+     *                  may produce unexpected results. If positions is NULL,
+     *                  then the colors are automatically spaced evenly.
+     */
+    public Painter setSweepGradient(
+            float centerX,
+            float centerY,
+            int[] colors,
+            float[] positions
+    ) {
+        mPaint.setSweepGradient(colors, positions, centerX, centerY);
+        return this;
+    }
+
+    /**
+     * Set the paint's text size. This value must be > 0
+     *
+     * @param size set the paint's text size in pixel units.
+     */
+    public Painter setTextSize(float size) {
+        mPaint.setTextSize(size);
+        return this;
+    }
+
+    /**
+     * sets a typeface object that best matches the specified existing
+     * typeface and the specified weight and italic style
+     *
+     * <p>Below are numerical values and corresponding common weight names.</p>
+     * <table> <thead>
+     * <tr><th>Value</th><th>Common weight name</th></tr> </thead> <tbody>
+     * <tr><td>100</td><td>Thin</td></tr>
+     * <tr><td>200</td><td>Extra Light</td></tr>
+     * <tr><td>300</td><td>Light</td></tr>
+     * <tr><td>400</td><td>Normal</td></tr>
+     * <tr><td>500</td><td>Medium</td></tr>
+     * <tr><td>600</td><td>Semi Bold</td></tr>
+     * <tr><td>700</td><td>Bold</td></tr>
+     * <tr><td>800</td><td>Extra Bold</td></tr>
+     * <tr><td>900</td><td>Black</td></tr> </tbody> </table>
+     *
+     * @param fontType 0 = default 1 = sans serif 2 = serif 3 = monospace
+     * @param weight   The desired weight to be drawn.
+     * @param italic   {@code true} if italic style is desired to be drawn.
+     *                            Otherwise, {@code false}
+     */
+    public Painter setTypeface(int fontType, int weight, boolean italic) {
+        mPaint.setTextStyle(fontType, weight, italic);
+        return this;
+    }
+
+
+    public Painter setFilterBitmap(boolean filter) {
+        mPaint.setFilterBitmap(filter);
+        return this;
+    }
+
+
+    public Painter setShader(int id) {
+        mPaint.setShader(id);
+        return this;
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java
new file mode 100644
index 0000000..23c3ec5
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 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.internal.widget.remotecompose.core.operations.utilities;
+
+import java.util.Arrays;
+
+public class IntFloatMap {
+
+    private static final int DEFAULT_CAPACITY = 16;
+    private static final float LOAD_FACTOR = 0.75f;
+    private static final int NOT_PRESENT = Integer.MIN_VALUE;
+    private int[] mKeys;
+    private float[] mValues;
+    int mSize;
+
+    public IntFloatMap() {
+        mKeys = new int[DEFAULT_CAPACITY];
+        Arrays.fill(mKeys, NOT_PRESENT);
+        mValues = new float[DEFAULT_CAPACITY];
+    }
+
+    /**
+     * clear the map
+     */
+    public void clear() {
+        Arrays.fill(mKeys, NOT_PRESENT);
+        Arrays.fill(mValues, Float.NaN); // not strictly necessary but defensive
+        mSize = 0;
+    }
+
+    /**
+     * is the key contained in map
+     *
+     * @param key the key to check
+     * @return true if the map contains the key
+     */
+    public boolean contains(int key) {
+        return findKey(key) != -1;
+    }
+
+    /**
+     * Put a item in the map
+     *
+     * @param key   item'values key
+     * @param value item's value
+     * @return old value if exist
+     */
+    public float put(int key, float value) {
+        if (key == NOT_PRESENT) {
+            throw new IllegalArgumentException("Key cannot be NOT_PRESENT");
+        }
+        if (mSize > mKeys.length * LOAD_FACTOR) {
+            resize();
+        }
+        return insert(key, value);
+    }
+
+    /**
+     * get an element given the key
+     *
+     * @param key the key to fetch
+     * @return the value
+     */
+    public float get(int key) {
+        int index = findKey(key);
+        if (index == -1) {
+            return 0;
+        } else
+            return mValues[index];
+    }
+
+    /**
+     * how many elements in the map
+     *
+     * @return number of elements
+     */
+    public int size() {
+        return mSize;
+    }
+
+    private float insert(int key, float value) {
+        int index = hash(key) % mKeys.length;
+        while (mKeys[index] != NOT_PRESENT && mKeys[index] != key) {
+            index = (index + 1) % mKeys.length;
+        }
+        float oldValue = 0;
+        if (mKeys[index] == NOT_PRESENT) {
+            mSize++;
+        } else {
+            oldValue = mValues[index];
+        }
+        mKeys[index] = key;
+        mValues[index] = value;
+        return oldValue;
+    }
+
+    private int findKey(int key) {
+        int index = hash(key) % mKeys.length;
+        while (mKeys[index] != NOT_PRESENT) {
+            if (mKeys[index] == key) {
+                return index;
+            }
+            index = (index + 1) % mKeys.length;
+        }
+        return -1;
+    }
+
+    private int hash(int key) {
+        return key;
+    }
+
+    private void resize() {
+        int[] oldKeys = mKeys;
+        float[] oldValues = mValues;
+        mKeys = new int[(oldKeys.length * 2)];
+        for (int i = 0; i < mKeys.length; i++) {
+            mKeys[i] = NOT_PRESENT;
+        }
+        mValues = new float[oldKeys.length * 2];
+        mSize = 0;
+        for (int i = 0; i < oldKeys.length; i++) {
+            if (oldKeys[i] != NOT_PRESENT) {
+                put(oldKeys[i], oldValues[i]);
+            }
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java
new file mode 100644
index 0000000..221014c
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 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.internal.widget.remotecompose.core.operations.utilities;
+
+import java.util.Arrays;
+
+public class IntIntMap {
+    private static final int DEFAULT_CAPACITY = 16;
+    private static final float LOAD_FACTOR = 0.75f;
+    private static final int NOT_PRESENT = Integer.MIN_VALUE;
+    private int[] mKeys;
+    private int[] mValues;
+    int mSize;
+
+    public IntIntMap() {
+        mKeys = new int[DEFAULT_CAPACITY];
+        Arrays.fill(mKeys, NOT_PRESENT);
+        mValues = new int[DEFAULT_CAPACITY];
+    }
+
+    /**
+     * clear the map
+     */
+    public void clear() {
+        Arrays.fill(mKeys, NOT_PRESENT);
+        Arrays.fill(mValues, 0);
+        mSize = 0;
+    }
+
+    /**
+     * is the key contained in map
+     *
+     * @param key the key to check
+     * @return true if the map contains the key
+     */
+    public boolean contains(int key) {
+        return findKey(key) != -1;
+    }
+
+    /**
+     * Put a item in the map
+     *
+     * @param key   item'values key
+     * @param value item's value
+     * @return old value if exist
+     */
+    public int put(int key, int value) {
+        if (key == NOT_PRESENT) {
+            throw new IllegalArgumentException("Key cannot be NOT_PRESENT");
+        }
+        if (mSize > mKeys.length * LOAD_FACTOR) {
+            resize();
+        }
+        return insert(key, value);
+    }
+
+    /**
+     * get an element given the key
+     *
+     * @param key the key to fetch
+     * @return the value
+     */
+    public int get(int key) {
+        int index = findKey(key);
+        if (index == -1) {
+            return 0;
+        } else
+            return mValues[index];
+    }
+
+    /**
+     * how many elements in the map
+     *
+     * @return number of elements
+     */
+    public int size() {
+        return mSize;
+    }
+
+    private int insert(int key, int value) {
+        int index = hash(key) % mKeys.length;
+        while (mKeys[index] != NOT_PRESENT && mKeys[index] != key) {
+            index = (index + 1) % mKeys.length;
+        }
+        int oldValue = 0;
+        if (mKeys[index] == NOT_PRESENT) {
+            mSize++;
+        } else {
+            oldValue = mValues[index];
+        }
+        mKeys[index] = key;
+        mValues[index] = value;
+        return oldValue;
+    }
+
+    private int findKey(int key) {
+        int index = hash(key) % mKeys.length;
+        while (mKeys[index] != NOT_PRESENT) {
+            if (mKeys[index] == key) {
+                return index;
+            }
+            index = (index + 1) % mKeys.length;
+        }
+        return -1;
+    }
+
+    private int hash(int key) {
+        return key;
+    }
+
+    private void resize() {
+        int[] oldKeys = mKeys;
+        int[] oldValues = mValues;
+        mKeys = new int[(oldKeys.length * 2)];
+        for (int i = 0; i < mKeys.length; i++) {
+            mKeys[i] = NOT_PRESENT;
+        }
+        mValues = new int[oldKeys.length * 2];
+        mSize = 0;
+        for (int i = 0; i < oldKeys.length; i++) {
+            if (oldKeys[i] != NOT_PRESENT) {
+                put(oldKeys[i], oldValues[i]);
+            }
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
new file mode 100644
index 0000000..4c1389c
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
@@ -0,0 +1,429 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.utilities;
+
+/**
+ * High performance Integer expression evaluator
+ */
+public class IntegerExpressionEvaluator {
+    static IntMap<String> sNames = new IntMap<>();
+    public static final int OFFSET = 0x10000;
+    // add, sub, mul,div,mod,min,max, shl, shr, ushr, OR, AND , XOR, COPY_SIGN
+    public static final int I_ADD = OFFSET + 1;
+    public static final int I_SUB = OFFSET + 2;
+    public static final int I_MUL = OFFSET + 3;
+    public static final int I_DIV = OFFSET + 4;
+    public static final int I_MOD = OFFSET + 5;
+    public static final int I_SHL = OFFSET + 6;
+    public static final int I_SHR = OFFSET + 7;
+    public static final int I_USHR = OFFSET + 8;
+    public static final int I_OR = OFFSET + 9;
+    public static final int I_AND = OFFSET + 10;
+    public static final int I_XOR = OFFSET + 11;
+    public static final int I_COPY_SIGN = OFFSET + 12;
+    public static final int I_MIN = OFFSET + 13;
+    public static final int I_MAX = OFFSET + 14;
+
+    public static final int I_NEG = OFFSET + 15;
+    public static final int I_ABS = OFFSET + 16;
+    public static final int I_INCR = OFFSET + 17;
+    public static final int I_DECR = OFFSET + 18;
+    public static final int I_NOT = OFFSET + 19;
+    public static final int I_SIGN = OFFSET + 20;
+
+    public static final int I_CLAMP = OFFSET + 21;
+    public static final int I_IFELSE = OFFSET + 22;
+    public static final int I_MAD = OFFSET + 23;
+
+    public static final float LAST_OP = 24;
+
+    public static final int I_VAR1 = OFFSET + 24;
+    public static final int I_VAR2 = OFFSET + 24;
+
+
+    int[] mStack;
+    int[] mLocalStack = new int[128];
+    int[] mVar;
+
+
+    interface Op {
+        int eval(int sp);
+    }
+
+    /**
+     * Evaluate a float expression
+     *
+     * @param exp
+     * @param var
+     * @return
+     */
+    public int eval(int mask, int[] exp, int... var) {
+        mStack = exp;
+        mVar = var;
+        int sp = -1;
+        for (int i = 0; i < mStack.length; i++) {
+            int v = mStack[i];
+            if (((1 << i) & mask) != 0) {
+                sp = mOps[v - OFFSET].eval(sp);
+            } else {
+                mStack[++sp] = v;
+            }
+        }
+        return mStack[sp];
+    }
+
+    /**
+     * Evaluate a int expression
+     *
+     * @param exp
+     * @param len
+     * @param var
+     * @return
+     */
+    public int eval(int mask, int[] exp, int len, int... var) {
+        System.arraycopy(exp, 0, mLocalStack, 0, len);
+        mStack = mLocalStack;
+        mVar = var;
+        int sp = -1;
+        for (int i = 0; i < len; i++) {
+            int v = mStack[i];
+            if (((1 << i) & mask) != 0) {
+                sp = mOps[v - OFFSET].eval(sp);
+            } else {
+                mStack[++sp] = v;
+            }
+        }
+        return mStack[sp];
+    }
+
+    /**
+     * Evaluate a int expression
+     *
+     * @param exp
+     * @param var
+     * @return
+     */
+    public int evalDB(int mask, int[] exp, int... var) {
+        mStack = exp;
+        mVar = var;
+        int sp = -1;
+        for (int i = 0; i < exp.length; i++) {
+            int v = mStack[i];
+            if (((1 << i) & mask) != 0) {
+                System.out.print(" " + sNames.get((v - OFFSET)));
+                sp = mOps[v - OFFSET].eval(sp);
+            } else {
+                System.out.print(" " + v);
+                mStack[++sp] = v;
+            }
+        }
+        return mStack[sp];
+    }
+
+    Op[] mOps = {
+            null,
+            (sp) -> { // ADD
+                mStack[sp - 1] = mStack[sp - 1] + mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> { // SUB
+                mStack[sp - 1] = mStack[sp - 1] - mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> { // MUL
+                mStack[sp - 1] = mStack[sp - 1] * mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> {  // DIV
+                mStack[sp - 1] = mStack[sp - 1] / mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> {  // MOD
+                mStack[sp - 1] = mStack[sp - 1] % mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> { // SHL shift left
+                mStack[sp - 1] = mStack[sp - 1] << mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> { // SHR shift right
+                mStack[sp - 1] = mStack[sp - 1] >> mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> { // USHR unsigned shift right
+                mStack[sp - 1] = mStack[sp - 1] >>> mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> { // OR operator
+                mStack[sp - 1] = mStack[sp - 1] | mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> { // AND operator
+                mStack[sp - 1] = mStack[sp - 1] & mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> { // XOR xor operator
+                mStack[sp - 1] = mStack[sp - 1] ^ mStack[sp];
+                return sp - 1;
+            },
+            (sp) -> { // COPY_SIGN copy the sing of (using bit magic)
+                mStack[sp - 1] = (mStack[sp - 1] ^ (mStack[sp] >> 31))
+                        - (mStack[sp] >> 31);
+                return sp - 1;
+            },
+            (sp) -> { // MIN
+                mStack[sp - 1] = Math.min(mStack[sp - 1], mStack[sp]);
+                return sp - 1;
+            },
+            (sp) -> { // MAX
+                mStack[sp - 1] = Math.max(mStack[sp - 1], mStack[sp]);
+                return sp - 1;
+            },
+            (sp) -> { // NEG
+                mStack[sp] = -mStack[sp];
+                return sp;
+            },
+            (sp) -> { // ABS
+                mStack[sp] = Math.abs(mStack[sp]);
+                return sp;
+            },
+            (sp) -> { // INCR increment
+                mStack[sp] = mStack[sp] + 1;
+                return sp;
+            },
+            (sp) -> { // DECR decrement
+                mStack[sp] = mStack[sp] - 1;
+                return sp;
+            },
+            (sp) -> { // NOT Bit invert
+                mStack[sp] = ~mStack[sp];
+                return sp;
+            },
+            (sp) -> { // SIGN x<0 = -1,x==0 =  0 , x>0 = 1
+                mStack[sp] = (mStack[sp] >> 31) | (-mStack[sp] >>> 31);
+                return sp;
+            },
+
+            (sp) -> { // CLAMP(min,max, val)
+                mStack[sp - 2] = Math.min(Math.max(mStack[sp - 2], mStack[sp]),
+                        mStack[sp - 1]);
+                return sp - 2;
+            },
+            (sp) -> { // Ternary conditional
+                mStack[sp - 2] = (mStack[sp] > 0)
+                        ? mStack[sp - 1] : mStack[sp - 2];
+                return sp - 2;
+            },
+            (sp) -> { // MAD
+                mStack[sp - 2] = mStack[sp] + mStack[sp - 1] * mStack[sp - 2];
+                return sp - 2;
+            },
+
+            (sp) -> { // first var =
+                mStack[sp] = mVar[0];
+                return sp;
+            },
+            (sp) -> { // second var y?
+                mStack[sp] = mVar[1];
+                return sp;
+            },
+            (sp) -> { // 3rd var z?
+                mStack[sp] = mVar[2];
+                return sp;
+            },
+    };
+
+    static {
+        int k = 0;
+        sNames.put(k++, "NOP");
+        sNames.put(k++, "+");
+        sNames.put(k++, "-");
+        sNames.put(k++, "*");
+        sNames.put(k++, "/");
+        sNames.put(k++, "%");
+        sNames.put(k++, "<<");
+        sNames.put(k++, ">>");
+        sNames.put(k++, ">>>");
+        sNames.put(k++, "|");
+        sNames.put(k++, "&");
+        sNames.put(k++, "^");
+        sNames.put(k++, "copySign");
+        sNames.put(k++, "min");
+        sNames.put(k++, "max");
+        sNames.put(k++, "neg");
+        sNames.put(k++, "abs");
+        sNames.put(k++, "incr");
+        sNames.put(k++, "decr");
+        sNames.put(k++, "not");
+        sNames.put(k++, "sign");
+        sNames.put(k++, "clamp");
+        sNames.put(k++, "ifElse");
+        sNames.put(k++, "mad");
+        sNames.put(k++, "ceil");
+        sNames.put(k++, "a[0]");
+        sNames.put(k++, "a[1]");
+        sNames.put(k++, "a[2]");
+    }
+
+    /**
+     * given a int command return its math name (e.g sin, cos etc.)
+     *
+     * @param f
+     * @return
+     */
+    public static String toMathName(int f) {
+        int id = f - OFFSET;
+        return sNames.get(id);
+    }
+
+    /**
+     * Convert an expression encoded as an array of ints int ot a string
+     *
+     * @param exp
+     * @param labels
+     * @return
+     */
+    public static String toString(int mask, int[] exp, String[] labels) {
+        StringBuilder s = new StringBuilder();
+        for (int i = 0; i < exp.length; i++) {
+            int v = exp[i];
+
+            if (((1 << i) & mask) != 0) {
+                if (v < OFFSET) {
+                    s.append(toMathName(v));
+                } else {
+                    s.append("[");
+                    s.append(v);
+                    s.append("]");
+                }
+            } else {
+                if (labels[i] != null) {
+                    s.append(labels[i]);
+                }
+                s.append(v);
+            }
+            s.append(" ");
+        }
+        return s.toString();
+    }
+
+    /**
+     * Convert an expression encoded as an array of ints int ot a string
+     *
+     * @param mask bit mask of operators vs commands
+     * @param exp
+     * @return
+     */
+    public static String toString(int mask, int[] exp) {
+        StringBuilder s = new StringBuilder();
+        s.append(Integer.toBinaryString(mask));
+        s.append(" : ");
+        for (int i = 0; i < exp.length; i++) {
+            int v = exp[i];
+
+            if (((1 << i) & mask) != 0) {
+                if (v > OFFSET) {
+                    s.append(" ");
+                    s.append(toMathName(v));
+                    s.append(" ");
+
+                } else {
+                    s.append("[");
+                    s.append(v);
+                    s.append("]");
+                }
+            }
+            s.append(" " + v);
+        }
+        return s.toString();
+    }
+
+    /**
+     * This creates an infix string expression
+     * @param mask The bits that are operators
+     * @param exp the array of expressions
+     * @return infix string
+     */
+    public static String toStringInfix(int mask, int[] exp) {
+        return toString(mask, exp, exp.length - 1);
+    }
+
+    static String toString(int mask, int[] exp, int sp) {
+        String[] str = new String[exp.length];
+        if (((1 << sp) & mask) != 0) {
+            int id = exp[sp] - OFFSET;
+            switch (NO_OF_OPS[id]) {
+                case -1:
+                    return "nop";
+                case 1:
+                    return sNames.get(id) + "(" + toString(mask, exp, sp - 1) + ") ";
+                case 2:
+                    if (infix(id)) {
+                        return "(" + toString(mask, exp, sp - 2)
+                                + " " + sNames.get(id) + " "
+                                + toString(mask, exp, sp - 1) + ") ";
+                    } else {
+                        return sNames.get(id) + "("
+                                + toString(mask, exp, sp - 2) + ", "
+                                + toString(mask, exp, sp - 1) + ")";
+                    }
+                case 3:
+                    if (infix(id)) {
+                        return "((" + toString(mask, exp, sp + 3) + ") ? "
+                                + toString(mask, exp, sp - 2) + ":"
+                                + toString(mask, exp, sp - 1) + ")";
+                    } else {
+                        return sNames.get(id)
+                                + "(" + toString(mask, exp, sp - 3)
+                                + ", " + toString(mask, exp, sp - 2)
+                                + ", " + toString(mask, exp, sp - 1) + ")";
+                    }
+            }
+        }
+        return Integer.toString(exp[sp]);
+    }
+
+    static final int[] NO_OF_OPS = {
+            -1, // no op
+            2, 2, 2, 2, 2, // + - * / %
+            2, 2, 2, 2, 2, 2, 2, 2, 2, //<<, >> , >>> , | , &, ^, min max
+            1, 1, 1, 1, 1, 1,   // neg, abs, ++, -- , not , sign
+
+            3, 3, 3, // clamp, ifElse, mad,
+            0, 0, 0 // mad, ?:,
+            // a[0],a[1],a[2]
+    };
+
+    /**
+     * to be used by parser to determine if command is infix
+     *
+     * @param n the operator (minus the offset)
+     * @return true if the operator is infix
+     */
+    static boolean infix(int n) {
+        return ((n < 12));
+    }
+
+    /**
+     * is it an id or operation
+     * @param mask the bits that mark elements as an operation
+     * @param i the bit to check
+     * @return true if the bit is 1
+     */
+    public static boolean isOperation(int mask, int i) {
+        return ((1 << i) & mask) != 0;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java
new file mode 100644
index 0000000..1051192
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java
@@ -0,0 +1,96 @@
+/*
+ * 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.internal.widget.remotecompose.core.types;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Used to represent a boolean
+ */
+public class BooleanConstant implements Operation {
+    boolean mValue = false;
+    private int mId;
+
+    public BooleanConstant(int id, boolean value) {
+        mId = id;
+        mValue = value;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mId, mValue);
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return toString();
+    }
+
+    @Override
+    public String toString() {
+        return "BooleanConstant[" + mId + "] = " + mValue + "";
+    }
+
+    public static final Companion COMPANION = new Companion();
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public String name() {
+            return "OrigamiBoolean";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DATA_BOOLEAN;
+        }
+
+        /**
+         * Writes out the operation to the buffer
+         *
+         * @param buffer
+         * @param id
+         * @param value
+         */
+        public void apply(WireBuffer buffer, int id, boolean value) {
+            buffer.start(Operations.DATA_BOOLEAN);
+            buffer.writeInt(id);
+            buffer.writeBoolean(value);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int id = buffer.readInt();
+
+            boolean value = buffer.readBoolean();
+            operations.add(new BooleanConstant(id, value));
+        }
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java
new file mode 100644
index 0000000..ceb3236
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java
@@ -0,0 +1,96 @@
+/*
+ * 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.internal.widget.remotecompose.core.types;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+/**
+ * Represents a single integer typically used for states
+ * or named for input into the system
+ */
+public class IntegerConstant implements Operation {
+    private int mValue = 0;
+    private int mId;
+
+    IntegerConstant(int id, int value) {
+        mId = id;
+        mValue = value;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mId, mValue);
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        context.loadInteger(mId, mValue);
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return toString();
+    }
+
+    @Override
+    public String toString() {
+        return "IntegerConstant[" + mId + "] = " + mValue + "";
+    }
+
+    public static final Companion COMPANION = new Companion();
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public String name() {
+            return "IntegerConstant";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DATA_INT;
+        }
+
+        /**
+         * Writes out the operation to the buffer
+         *
+         * @param buffer
+         * @param textId
+         * @param value
+         */
+        public void apply(WireBuffer buffer, int textId, int value) {
+            buffer.start(Operations.DATA_INT);
+            buffer.writeInt(textId);
+            buffer.writeInt(value);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int id = buffer.readInt();
+
+            int value = buffer.readInt();
+            operations.add(new IntegerConstant(id, value));
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
index 73e94fa..b2406bf 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
@@ -410,4 +410,3 @@
         }
     }
 }
-
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
index ecb68bb..39a770a 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
@@ -39,12 +39,16 @@
 import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
 import com.android.internal.widget.remotecompose.core.operations.paint.PaintChanges;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * An implementation of PaintContext for the Android Canvas.
  * This is used to play the RemoteCompose operations on Android.
  */
 public class AndroidPaintContext extends PaintContext {
     Paint mPaint = new Paint();
+    List<Paint> mPaintList = new ArrayList<>();
     Canvas mCanvas;
     Rect mTmpRect = new Rect(); // use in calculation of bounds
 
@@ -162,6 +166,16 @@
     }
 
     @Override
+    public void savePaint() {
+        mPaintList.add(new Paint(mPaint));
+    }
+
+    @Override
+    public void restorePaint() {
+        mPaint = mPaintList.remove(mPaintList.size() - 1);
+    }
+
+    @Override
     public void drawRoundRect(float left,
                               float top,
                               float right,
@@ -335,6 +349,11 @@
         return null;
     }
 
+    /**
+     * This applies paint changes to the current paint
+     *
+     * @param mPaintData the list change to the paint
+     */
     @Override
     public void applyPaint(PaintBundle mPaintData) {
         mPaintData.applyPaintChange((PaintContext) this, new PaintChanges() {
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
index dd43bd5..5a87c70 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
@@ -120,6 +120,11 @@
         mRemoteComposeState.updateFloat(id, value);
     }
 
+    @Override
+    public void loadInteger(int id, int value) {
+        mRemoteComposeState.updateInteger(id, value);
+    }
+
 
     @Override
     public void loadColor(int id, int color) {
@@ -142,6 +147,11 @@
     }
 
     @Override
+    public int getInteger(int id) {
+        return  mRemoteComposeState.getInteger(id);
+    }
+
+    @Override
     public int getColor(int id) {
         return mRemoteComposeState.getColor(id);
     }
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java b/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java
index 2d766f8..7a85427 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java
@@ -58,7 +58,7 @@
                 break;
                 case PathData.CONIC: {
                     i += 3;
-                    if (Build.VERSION.SDK_INT >= 34) {
+                    if (Build.VERSION.SDK_INT >= 34) { // REMOVE IN PLATFORM
                         path.conicTo(
                                 floatPath[i + 0], floatPath[i + 1],
                                 floatPath[i + 2], floatPath[i + 3],
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 7c62615..638591f 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -2292,7 +2292,7 @@
                                  criteria.mValue.mUsage);
                 jMixMatchCriterion = env->NewObject(gAudioMixMatchCriterionClass,
                                                     gAudioMixMatchCriterionAttrCstor,
-                                                    jMixMatchCriterion, criteria.mRule);
+                                                    jAudioAttributes, criteria.mRule);
                 break;
             case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
                 jAudioAttributes = env->NewObject(gAudioAttributesClass, gAudioAttributesCstor);
@@ -2300,7 +2300,7 @@
                                  criteria.mValue.mSource);
                 jMixMatchCriterion = env->NewObject(gAudioMixMatchCriterionClass,
                                                     gAudioMixMatchCriterionAttrCstor,
-                                                    jMixMatchCriterion, criteria.mRule);
+                                                    jAudioAttributes, criteria.mRule);
                 break;
         }
         env->CallBooleanMethod(jAudioMixMatchCriterionList, gArrayListMethods.add,
diff --git a/core/res/res/drawable/tooltip_frame.xml b/core/res/res/drawable/tooltip_frame.xml
index 14130c8..e2618ca 100644
--- a/core/res/res/drawable/tooltip_frame.xml
+++ b/core/res/res/drawable/tooltip_frame.xml
@@ -17,5 +17,5 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
     <solid android:color="?attr/tooltipBackgroundColor" />
-    <corners android:radius="@dimen/tooltip_corner_radius" />
-</shape>
\ No newline at end of file
+    <corners android:radius="?attr/tooltipCornerRadius" />
+</shape>
diff --git a/core/res/res/layout/tooltip.xml b/core/res/res/layout/tooltip.xml
index 376c5eb..5b6799e 100644
--- a/core/res/res/layout/tooltip.xml
+++ b/core/res/res/layout/tooltip.xml
@@ -27,10 +27,10 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_margin="@dimen/tooltip_margin"
-        android:paddingStart="@dimen/tooltip_horizontal_padding"
-        android:paddingEnd="@dimen/tooltip_horizontal_padding"
-        android:paddingTop="@dimen/tooltip_vertical_padding"
-        android:paddingBottom="@dimen/tooltip_vertical_padding"
+        android:paddingStart="?attr/tooltipHorizontalPadding"
+        android:paddingEnd="?attr/tooltipHorizontalPadding"
+        android:paddingTop="?attr/tooltipVerticalPadding"
+        android:paddingBottom="?attr/tooltipVerticalPadding"
         android:maxWidth="256dp"
         android:background="?android:attr/tooltipFrameBackground"
         android:textAppearance="@style/TextAppearance.Tooltip"
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 7cc9e13..440219d 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -1078,6 +1078,11 @@
         <!-- Background color to use for tooltip popups. -->
         <attr name="tooltipBackgroundColor" format="reference|color" />
 
+        <attr name="tooltipCornerRadius" format="dimension" />
+        <attr name="tooltipHorizontalPadding" format="dimension" />
+        <attr name="tooltipVerticalPadding" format="dimension" />
+        <attr name="tooltipFontSize" format="dimension" />
+
         <!-- Theme to use for Search Dialogs. -->
         <attr name="searchDialogTheme" format="reference" />
 
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 2e3dbda..0be33c2 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -3296,8 +3296,8 @@
              usually TVs.
              <p>Requires permission {@code android.permission.DISABLE_SYSTEM_SOUND_EFFECTS}. -->
         <attr name="playHomeTransitionSound" format="boolean"/>
-        <!-- Indicates whether the activity can be displayed on a remote device which may or
-             may not be running Android. -->
+        <!-- Indicates whether the activity can be displayed on a display that may belong to a
+             remote device which may or may not be running Android. -->
         <attr name="canDisplayOnRemoteDevices" format="boolean"/>
         <attr name="allowUntrustedActivityEmbedding" />
         <attr name="knownActivityEmbeddingCerts" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2afc303..495af5b 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1399,6 +1399,10 @@
          Settings.System.RING_VIBRATION_INTENSITY more details on the constant values and
          meanings. -->
     <integer name="config_defaultRingVibrationIntensity">2</integer>
+    <!-- The default intensity level for keyboard vibrations. Note that this will only be applied
+         on devices where config_keyboardVibrationSettingsSupported is true, otherwise the
+         keyboard vibration will follow config_defaultHapticFeedbackIntensity -->
+    <integer name="config_defaultKeyboardVibrationIntensity">2</integer>
 
     <!-- Whether to use the strict phone number matcher by default. -->
     <bool name="config_use_strict_phone_number_comparation">false</bool>
@@ -6154,10 +6158,6 @@
         is enabled and activity is connected to the camera in fullscreen. -->
     <bool name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled">false</bool>
 
-    <!-- Whether a camera compat controller is enabled to allow the user to apply or revert
-         treatment for stretched issues in camera viewfinder. -->
-    <bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool>
-
     <!-- Docking is a uiMode configuration change and will cause activities to relaunch if it's not
          handled. If true, the configuration change will be sent but activities will not be
          relaunched upon docking. Apps with desk resources will behave like normal, since they may
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 6cba84b..77b5587 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -771,6 +771,7 @@
     <dimen name="tooltip_precise_anchor_threshold">96dp</dimen>
     <!-- Extra tooltip offset used when anchoring to the mouse/touch position -->
     <dimen name="tooltip_precise_anchor_extra_offset">8dp</dimen>
+    <dimen name="tooltip_font_size">14sp</dimen>
 
     <!-- The max amount of scroll ItemTouchHelper will trigger if dragged view is out of
          RecyclerView's bounds.-->
diff --git a/core/res/res/values/dimens_material.xml b/core/res/res/values/dimens_material.xml
index 972fe7e..35f35fb 100644
--- a/core/res/res/values/dimens_material.xml
+++ b/core/res/res/values/dimens_material.xml
@@ -204,4 +204,9 @@
     <dimen name="progress_bar_size_small">16dip</dimen>
     <dimen name="progress_bar_size_medium">48dp</dimen>
     <dimen name="progress_bar_size_large">76dp</dimen>
+
+    <dimen name="tooltip_corner_radius_material">4dp</dimen>
+    <dimen name="tooltip_horizontal_padding_material">8dp</dimen>
+    <dimen name="tooltip_vertical_padding_material">4dp</dimen>
+    <dimen name="tooltip_font_size_material">12sp</dimen>
 </resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index ec865f6..e94db2d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6555,4 +6555,7 @@
     <string name="keyboard_shortcut_group_applications_maps">Maps</string>
     <!-- User visible title for the keyboard shortcut group containing system-wide application launch shortcuts. [CHAR-LIMIT=70] -->
     <string name="keyboard_shortcut_group_applications">Applications</string>
+
+    <!-- Fingerprint loe notification string -->
+    <string name="fingerprint_loe_notification_msg">Your fingerprints can no longer be recognized. Set up Fingerprint Unlock again.</string>
 </resources>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index aabc8ca..c084b4c 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -998,7 +998,7 @@
 
     <style name="TextAppearance.Tooltip">
         <item name="fontFamily">sans-serif</item>
-        <item name="textSize">14sp</item>
+        <item name="textSize">?android:attr/tooltipFontSize</item>
     </style>
 
     <style name="Widget.ActivityChooserView">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index bc8c778..8734b44 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4223,6 +4223,7 @@
   <java-symbol type="integer" name="config_defaultMediaVibrationIntensity" />
   <java-symbol type="integer" name="config_defaultNotificationVibrationIntensity" />
   <java-symbol type="integer" name="config_defaultRingVibrationIntensity" />
+  <java-symbol type="integer" name="config_defaultKeyboardVibrationIntensity" />
 
   <java-symbol type="bool" name="config_maskMainBuiltInDisplayCutout" />
 
@@ -4766,7 +4767,6 @@
   <java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
   <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
   <java-symbol type="bool" name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled" />
-  <java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" />
   <java-symbol type="bool" name="config_skipActivityRelaunchWhenDocking" />
 
   <java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
@@ -5583,4 +5583,7 @@
   <java-symbol type="string" name="keyboard_shortcut_group_applications_music" />
   <java-symbol type="string" name="keyboard_shortcut_group_applications_sms" />
   <java-symbol type="string" name="keyboard_shortcut_group_applications" />
+
+  <!-- Fingerprint loe notification string -->
+  <java-symbol type="string" name="fingerprint_loe_notification_msg" />
 </resources>
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index c3d304d..3b3bb8d 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -461,6 +461,10 @@
         <item name="tooltipFrameBackground">@drawable/tooltip_frame</item>
         <item name="tooltipForegroundColor">@color/bright_foreground_light</item>
         <item name="tooltipBackgroundColor">@color/tooltip_background_light</item>
+        <item name="tooltipCornerRadius">@dimen/tooltip_corner_radius</item>
+        <item name="tooltipHorizontalPadding">@dimen/tooltip_horizontal_padding</item>
+        <item name="tooltipVerticalPadding">@dimen/tooltip_vertical_padding</item>
+        <item name="tooltipFontSize">@dimen/tooltip_font_size</item>
 
         <!-- Autofill: max width/height of the dataset picker as a fraction of screen size -->
         <item name="autofillDatasetPickerMaxWidth">@dimen/autofill_dataset_picker_max_width</item>
@@ -582,9 +586,10 @@
         <item name="floatingToolbarOpenDrawable">@drawable/ic_menu_moreoverflow_material_light</item>
         <item name="floatingToolbarDividerColor">@color/floating_popup_divider_light</item>
 
-        <!-- Tooltip popup colors -->
+        <!-- Tooltip popup styles -->
         <item name="tooltipForegroundColor">@color/bright_foreground_dark</item>
         <item name="tooltipBackgroundColor">@color/tooltip_background_dark</item>
+
     </style>
 
     <!-- Variant of {@link #Theme_Light} with no title bar -->
diff --git a/core/res/res/values/themes_material.xml b/core/res/res/values/themes_material.xml
index 8e2fb34..9f11208 100644
--- a/core/res/res/values/themes_material.xml
+++ b/core/res/res/values/themes_material.xml
@@ -408,8 +408,12 @@
         <item name="colorProgressBackgroundNormal">?attr/colorControlNormal</item>
 
         <!-- Tooltip popup properties -->
-        <item name="tooltipForegroundColor">@color/foreground_material_light</item>
-        <item name="tooltipBackgroundColor">@color/tooltip_background_light</item>
+        <item name="tooltipForegroundColor">@color/system_on_surface_light</item>
+        <item name="tooltipBackgroundColor">@color/system_surface_light</item>
+        <item name="tooltipCornerRadius">@dimen/tooltip_corner_radius_material</item>
+        <item name="tooltipHorizontalPadding">@dimen/tooltip_horizontal_padding_material</item>
+        <item name="tooltipVerticalPadding">@dimen/tooltip_vertical_padding_material</item>
+        <item name="tooltipFontSize">@dimen/tooltip_font_size_material</item>
     </style>
 
     <!-- Material theme (light version). -->
@@ -785,8 +789,13 @@
         <item name="colorProgressBackgroundNormal">?attr/colorControlNormal</item>
 
         <!-- Tooltip popup properties -->
-        <item name="tooltipForegroundColor">@color/foreground_material_dark</item>
-        <item name="tooltipBackgroundColor">@color/tooltip_background_dark</item>
+        <item name="tooltipForegroundColor">@color/system_on_surface_dark</item>
+        <item name="tooltipBackgroundColor">@color/system_surface_dark</item>
+        <item name="tooltipCornerRadius">@dimen/tooltip_corner_radius_material</item>
+        <item name="tooltipHorizontalPadding">@dimen/tooltip_horizontal_padding_material</item>
+        <item name="tooltipVerticalPadding">@dimen/tooltip_vertical_padding_material</item>
+        <item name="tooltipFontSize">@dimen/tooltip_font_size_material</item>
+
     </style>
 
     <!-- Variant of the material (light) theme that has a solid (opaque) action bar
diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
index a102b3e..eb463fd 100644
--- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
+++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
@@ -30,9 +30,9 @@
 import android.view.Choreographer;
 import android.view.animation.LinearInterpolator;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/app/activity/ServiceTest.java b/core/tests/coretests/src/android/app/activity/ServiceTest.java
index 3f3d6a3..4e3b2af 100644
--- a/core/tests/coretests/src/android/app/activity/ServiceTest.java
+++ b/core/tests/coretests/src/android/app/activity/ServiceTest.java
@@ -157,6 +157,21 @@
         assertThat(mCurrentConnection.takePid(), is(NOT_STARTED));
     }
 
+    @Test
+    public void testRestart_stickyStartedService_unbindHappenedAfterRestart_restarted() {
+        final int servicePid = startService(Service.START_STICKY);
+        assertThat(servicePid, not(NOT_STARTED));
+        assertThat(bindService(0 /* flags */), is(servicePid));
+
+        final int restartedServicePid = waitForServiceStarted(
+                () -> {
+                    Process.killProcess(servicePid);
+                    mContext.unbindService(mCurrentConnection);
+                    mCurrentConnection = null;
+                });
+        assertThat(restartedServicePid, not(NOT_STARTED));
+    }
+
     /** @return The pid of the started service. */
     private int startService(int code) {
         return waitForServiceStarted(
diff --git a/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java b/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java
index e025fae..b91263e 100644
--- a/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java
+++ b/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java
@@ -35,7 +35,7 @@
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Rule;
diff --git a/core/tests/coretests/src/android/debug/AdbNotificationsTest.java b/core/tests/coretests/src/android/debug/AdbNotificationsTest.java
index 3496e2c..10eeb35 100644
--- a/core/tests/coretests/src/android/debug/AdbNotificationsTest.java
+++ b/core/tests/coretests/src/android/debug/AdbNotificationsTest.java
@@ -25,8 +25,8 @@
 import android.text.TextUtils;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/graphics/FontListParserTest.java b/core/tests/coretests/src/android/graphics/FontListParserTest.java
index 5f96c17..52f53dd 100644
--- a/core/tests/coretests/src/android/graphics/FontListParserTest.java
+++ b/core/tests/coretests/src/android/graphics/FontListParserTest.java
@@ -16,16 +16,16 @@
 
 package android.graphics;
 
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
 import static android.graphics.fonts.FontStyle.FONT_SLANT_ITALIC;
 import static android.graphics.fonts.FontStyle.FONT_SLANT_UPRIGHT;
 import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
 import static android.text.FontConfig.FontFamily.VARIANT_COMPACT;
 import static android.text.FontConfig.FontFamily.VARIANT_DEFAULT;
 import static android.text.FontConfig.FontFamily.VARIANT_ELEGANT;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -38,8 +38,8 @@
 import android.text.FontConfig;
 import android.util.Xml;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/graphics/RectTest.java b/core/tests/coretests/src/android/graphics/RectTest.java
index 2918f44..d0cb5d5 100644
--- a/core/tests/coretests/src/android/graphics/RectTest.java
+++ b/core/tests/coretests/src/android/graphics/RectTest.java
@@ -24,8 +24,8 @@
 
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
index 6ae7eb7..a94f412 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
@@ -23,8 +23,8 @@
 import android.graphics.fonts.Font;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
index 0d687b2..10aed8d 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
@@ -39,8 +39,8 @@
 import android.util.ArrayMap;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/graphics/TypefaceTest.java b/core/tests/coretests/src/android/graphics/TypefaceTest.java
index 6bf8f56..80efa51 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceTest.java
@@ -30,10 +30,10 @@
 import android.util.ArrayMap;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.coretests.R;
 
diff --git a/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java b/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java
index d0a6ff9..4991cd0 100644
--- a/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java
+++ b/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java
@@ -25,8 +25,8 @@
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Xfermode;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java b/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java
index 0a25bd7..244024d 100644
--- a/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java
+++ b/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java
@@ -24,8 +24,8 @@
 
 import android.os.Parcel;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java b/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
index 1c7ab74..9f12e51 100644
--- a/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
+++ b/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
@@ -25,8 +25,8 @@
 import android.util.Pair;
 import android.util.Xml;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index 969ae8e..5a0dacb 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -31,8 +31,8 @@
 import android.view.DisplayInfo;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
index 5aeab42..b4f1dee 100644
--- a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
+++ b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
@@ -21,8 +21,8 @@
 
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/net/NetworkKeyTest.java b/core/tests/coretests/src/android/net/NetworkKeyTest.java
index b13bcd1..444ed51 100644
--- a/core/tests/coretests/src/android/net/NetworkKeyTest.java
+++ b/core/tests/coretests/src/android/net/NetworkKeyTest.java
@@ -25,7 +25,7 @@
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java b/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java
index 3e45a79..46f22ce 100644
--- a/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java
+++ b/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java
@@ -26,7 +26,7 @@
 import android.Manifest.permission;
 import android.content.Context;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/net/SSLCertificateSocketFactoryTest.java b/core/tests/coretests/src/android/net/SSLCertificateSocketFactoryTest.java
index bc12e72..7413ede 100644
--- a/core/tests/coretests/src/android/net/SSLCertificateSocketFactoryTest.java
+++ b/core/tests/coretests/src/android/net/SSLCertificateSocketFactoryTest.java
@@ -19,7 +19,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/net/ScoredNetworkTest.java b/core/tests/coretests/src/android/net/ScoredNetworkTest.java
index d984d86..63eeaa1 100644
--- a/core/tests/coretests/src/android/net/ScoredNetworkTest.java
+++ b/core/tests/coretests/src/android/net/ScoredNetworkTest.java
@@ -26,7 +26,7 @@
 import android.os.Bundle;
 import android.os.Parcel;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/net/SntpClientTest.java b/core/tests/coretests/src/android/net/SntpClientTest.java
index 267fc2b..024d614 100644
--- a/core/tests/coretests/src/android/net/SntpClientTest.java
+++ b/core/tests/coretests/src/android/net/SntpClientTest.java
@@ -29,7 +29,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.Log;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import libcore.util.HexEncoding;
 
diff --git a/core/tests/coretests/src/android/net/sntp/Duration64Test.java b/core/tests/coretests/src/android/net/sntp/Duration64Test.java
index b228596..b177e18 100644
--- a/core/tests/coretests/src/android/net/sntp/Duration64Test.java
+++ b/core/tests/coretests/src/android/net/sntp/Duration64Test.java
@@ -23,7 +23,7 @@
 
 import android.platform.test.annotations.Presubmit;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java b/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java
index 200c80e..9f95132 100644
--- a/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java
+++ b/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java
@@ -23,7 +23,7 @@
 
 import android.platform.test.annotations.Presubmit;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java b/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java
index 0deb77e..55a347e 100644
--- a/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java
+++ b/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java
@@ -27,8 +27,8 @@
 import android.widget.ImageView;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
index c25aa51..746c8ca 100644
--- a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
+++ b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
@@ -42,9 +42,9 @@
 import android.print.test.services.StubbablePrinterDiscoverySession;
 import android.printservice.recommendation.IRecommendationsChangeListener;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
 import androidx.test.uiautomator.UiDevice;
 
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java b/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java
index e20258a..a60746f 100644
--- a/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java
+++ b/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java
@@ -23,8 +23,8 @@
 
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
index 9300d1e..681396e 100644
--- a/core/tests/coretests/src/android/provider/DeviceConfigTest.java
+++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
@@ -29,9 +29,9 @@
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Assert;
diff --git a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java
index 7e02be8..4010171 100644
--- a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java
+++ b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java
@@ -33,8 +33,8 @@
 import android.provider.FontsContract.FontFamilyResult;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
index 4d446901..6eaf2e4 100644
--- a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
+++ b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
@@ -45,9 +45,9 @@
 import android.service.controls.actions.ControlActionWrapper;
 import android.service.controls.templates.ThumbnailTemplate;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.R;
 
diff --git a/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
index d8088b7..44bdc53 100644
--- a/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
+++ b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
@@ -23,8 +23,8 @@
 
 import android.os.Parcel;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
index 91a3ba7..73b6f648 100644
--- a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
+++ b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
@@ -25,8 +25,8 @@
 import android.graphics.drawable.Icon;
 import android.os.Parcel;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.coretests.R;
 
diff --git a/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java b/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java
index 6792d0b..f4206c8 100644
--- a/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java
+++ b/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java
@@ -26,8 +26,8 @@
 import android.service.carrier.CarrierIdentifier;
 import android.telephony.UiccAccessRule;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java
index a121941..44456e9 100644
--- a/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java
+++ b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java
@@ -27,8 +27,8 @@
 import android.os.Parcel;
 import android.util.ArraySet;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java b/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java
index 76c9f88..5042408 100644
--- a/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java
+++ b/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java
@@ -37,8 +37,8 @@
 import android.os.UserHandle;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 
diff --git a/core/tests/coretests/src/android/service/quicksettings/TileTest.java b/core/tests/coretests/src/android/service/quicksettings/TileTest.java
index ca6c3b4..43f9122 100644
--- a/core/tests/coretests/src/android/service/quicksettings/TileTest.java
+++ b/core/tests/coretests/src/android/service/quicksettings/TileTest.java
@@ -18,8 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java
index 64edda5..85659d6 100644
--- a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java
+++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java
@@ -23,9 +23,9 @@
 import android.os.RemoteException;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.rule.ServiceTestRule;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java
index e0eb197..03096de 100644
--- a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java
+++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java
@@ -26,8 +26,8 @@
 import android.os.Parcel;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/telephony/PinResultTest.java b/core/tests/coretests/src/android/telephony/PinResultTest.java
index c260807..f5432ee 100644
--- a/core/tests/coretests/src/android/telephony/PinResultTest.java
+++ b/core/tests/coretests/src/android/telephony/PinResultTest.java
@@ -18,7 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java
index 5ff659b..8a41678 100644
--- a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java
+++ b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java
@@ -21,11 +21,18 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.text.method.OffsetMapping;
+import android.text.style.UpdateLayout;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.text.flags.Flags;
+
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -36,6 +43,9 @@
     private static final int WIDTH = 10000;
     private static final TextPaint sTextPaint = new TextPaint();
 
+    @Rule
+    public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Test
     public void textWithOffsetMapping() {
         final String text = "abcde";
@@ -120,6 +130,84 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_CRASH_UPDATE_LAYOUT_SPAN)
+    public void textWithOffsetMapping_deletion_withUpdateLayoutSpan() {
+        final String text = "abcdef";
+        final SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+        // UpdateLayout span covers the letter 'd'.
+        spannable.setSpan(new UpdateLayout() {}, 3, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+        final CharSequence transformedText =
+                new TestOffsetMapping(spannable, 3, "\n\n");
+
+        final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
+                .setAlignment(ALIGN_NORMAL)
+                .setIncludePad(false)
+                .setDisplayText(transformedText)
+                .build();
+
+        // delete character 'c', original text becomes "abdef"
+        spannable.delete(2, 3);
+        assertThat(transformedText.toString()).isEqualTo("ab\n\ndef");
+        assertLineRange(layout, /* lineBreaks */ 0, 3, 4, 7);
+
+        // delete character 'd', original text becomes "abef"
+        spannable.delete(2, 3);
+        assertThat(transformedText.toString()).isEqualTo("ab\n\nef");
+        assertLineRange(layout, /* lineBreaks */ 0, 3, 4, 6);
+
+        // delete "be", original text becomes "af"
+        spannable.delete(1, 3);
+        assertThat(transformedText.toString()).isEqualTo("a\n\nf");
+        assertLineRange(layout, /* lineBreaks */ 0, 2, 3, 4);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_CRASH_UPDATE_LAYOUT_SPAN)
+    public void textWithOffsetMapping_insert_withUpdateLayoutSpan() {
+        final String text = "abcdef";
+        final SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+        final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n");
+
+        // UpdateLayout span covers the letter 'de'.
+        spannable.setSpan(new UpdateLayout() {}, 3, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+        final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
+                .setAlignment(ALIGN_NORMAL)
+                .setIncludePad(false)
+                .setDisplayText(transformedText)
+                .build();
+
+        spannable.insert(3, "x");
+        assertThat(transformedText.toString()).isEqualTo("abcx\n\ndef");
+        assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 9);
+
+        spannable.insert(5, "x");
+        assertThat(transformedText.toString()).isEqualTo("abcx\n\ndxef");
+        assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 10);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_CRASH_UPDATE_LAYOUT_SPAN)
+    public void textWithOffsetMapping_replace_withUpdateLayoutSpan() {
+        final String text = "abcdef";
+        final SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+        final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n");
+        // UpdateLayout span covers the letter 'de'.
+        spannable.setSpan(new UpdateLayout() {}, 3, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+        final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
+                .setAlignment(ALIGN_NORMAL)
+                .setIncludePad(false)
+                .setDisplayText(transformedText)
+                .build();
+
+        spannable.replace(2, 4, "xx");
+        assertThat(transformedText.toString()).isEqualTo("abxx\n\nef");
+        assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8);
+    }
+
+    @Test
     public void textWithOffsetMapping_blockBeforeTextChanged_deletion() {
         final String text = "abcdef";
         final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text);
diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
index df9a89e..bbeb18d 100644
--- a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
+++ b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
@@ -37,7 +37,7 @@
 import android.util.proto.ProtoInputStream;
 import android.util.proto.ProtoOutputStream;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.google.common.truth.Truth;
 import com.google.protobuf.InvalidProtocolBufferException;
diff --git a/core/tests/coretests/src/android/transition/AutoTransitionTest.java b/core/tests/coretests/src/android/transition/AutoTransitionTest.java
index deae967..5d58fead 100644
--- a/core/tests/coretests/src/android/transition/AutoTransitionTest.java
+++ b/core/tests/coretests/src/android/transition/AutoTransitionTest.java
@@ -20,8 +20,8 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 18364ad..b8ff595 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -278,7 +278,7 @@
     @RequiresFlagsEnabled({FLAG_VIEW_VELOCITY_API,
             FLAG_TOOLKIT_FRAME_RATE_VELOCITY_MAPPING_READ_ONLY,
             FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY})
-    public void lowVelocity80() throws Throwable {
+    public void lowVelocity60() throws Throwable {
         if (!ViewProperties.vrr_enabled().orElse(true)) {
             return;
         }
@@ -292,6 +292,31 @@
         mActivityRule.runOnUiThread(() -> {
             mMovingView.setFrameContentVelocity(1f);
             mMovingView.invalidate();
+            runAfterDraw(() -> assertEquals(60f, mViewRoot.getLastPreferredFrameRate(), 0f));
+        });
+        waitForAfterDraw();
+    }
+
+    @Test
+    @RequiresFlagsEnabled({FLAG_VIEW_VELOCITY_API,
+            FLAG_TOOLKIT_FRAME_RATE_VELOCITY_MAPPING_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY})
+    public void midVelocity80() throws Throwable {
+        if (!ViewProperties.vrr_enabled().orElse(true)) {
+            return;
+        }
+        mActivityRule.runOnUiThread(() -> {
+            ViewGroup.LayoutParams layoutParams = mMovingView.getLayoutParams();
+            layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
+            layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
+            mMovingView.setLayoutParams(layoutParams);
+        });
+        waitForFrameRateCategoryToSettle();
+        mActivityRule.runOnUiThread(() -> {
+            float midSpeed =
+                    200f * mMovingView.getContext().getResources().getDisplayMetrics().density;
+            mMovingView.setFrameContentVelocity(midSpeed);
+            mMovingView.invalidate();
             runAfterDraw(() -> assertEquals(80f, mViewRoot.getLastPreferredFrameRate(), 0f));
         });
         waitForAfterDraw();
@@ -321,7 +346,7 @@
             frameLayout.setFrameContentVelocity(1f);
             mMovingView.offsetTopAndBottom(100);
             frameLayout.invalidate();
-            runAfterDraw(() -> assertEquals(80f, mViewRoot.getLastPreferredFrameRate(), 0f));
+            runAfterDraw(() -> assertEquals(60f, mViewRoot.getLastPreferredFrameRate(), 0f));
         });
         waitForAfterDraw();
     }
@@ -590,7 +615,7 @@
             runAfterDraw(() -> {
                 assertEquals(FRAME_RATE_CATEGORY_LOW,
                         mViewRoot.getLastPreferredFrameRateCategory());
-                assertEquals(80f, mViewRoot.getLastPreferredFrameRate());
+                assertEquals(60f, mViewRoot.getLastPreferredFrameRate());
             });
         });
         waitForAfterDraw();
diff --git a/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java b/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java
new file mode 100644
index 0000000..7cbfc40
--- /dev/null
+++ b/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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 android.view.autofill;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.content.Context;
+import android.text.InputType;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class AutofillStateFingerprintTest {
+
+    private static final Context sContext = ApplicationProvider.getApplicationContext();
+
+    private static final int MAGIC_AUTOFILL_NUMBER = 1000;
+
+    private AutofillStateFingerprint mAutofillStateFingerprint =
+            AutofillStateFingerprint.createInstance();
+
+    @Test
+    public void testSameFingerprintsForTextView() throws Exception {
+        TextView tv = new TextView(sContext);
+        tv.setHint("Password");
+        tv.setSingleLine(true);
+        tv.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+        tv.setImeOptions(EditorInfo.IME_FLAG_NAVIGATE_NEXT);
+        fillViewProperties(tv);
+
+        // Create a copy Text View, and compare both id's
+        View tvCopy = copySelectiveViewAttributes(tv);
+        assertIdsEqual(tv, tvCopy);
+    }
+
+    @Test
+    public void testDifferentFingerprintsForTextViewWithDifferentHint() throws Exception {
+        TextView tv = new TextView(sContext);
+        tv.setHint("Password");
+        tv.setSingleLine(true);
+        tv.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+        tv.setImeOptions(EditorInfo.IME_FLAG_NAVIGATE_NEXT);
+        fillViewProperties(tv);
+
+        TextView tvCopy = (TextView) copySelectiveViewAttributes(tv);
+        tvCopy.setHint("what a useless different hint");
+        assertIdsNotEqual(tv, tvCopy);
+    }
+
+    @Test
+    public void testSameFingerprintsForNonTextView() throws Exception {
+        View v = new View(sContext);
+        fillViewProperties(v);
+
+        // Create a copy Text View, and compare both id's
+        View copy = copySelectiveViewAttributes(v);
+        assertIdsEqual(v, copy);
+    }
+
+    @Test
+    public void testDifferentFingerprintsForNonTextViewWithDifferentVisibility() throws Exception {
+        View v = new View(sContext);
+        fillViewProperties(v);
+
+        View copy = copySelectiveViewAttributes(v);
+        copy.setVisibility(View.GONE);
+        assertIdsNotEqual(v, copy);
+    }
+
+    private void assertIdsEqual(View v1, View v2) {
+        assertEquals(mAutofillStateFingerprint.getEphemeralFingerprintId(v1, 0),
+                mAutofillStateFingerprint.getEphemeralFingerprintId(v2, 0));
+    }
+
+    private void assertIdsNotEqual(View v1, View v2) {
+        assertNotEquals(mAutofillStateFingerprint.getEphemeralFingerprintId(v1, 0),
+                mAutofillStateFingerprint.getEphemeralFingerprintId(v2, 0));
+    }
+
+    private void fillViewProperties(View view) {
+        // Fill in relevant view properties
+        view.setContentDescription("ContentDesc");
+        view.setTooltipText("TooltipText");
+        view.setAutofillHints(new String[] {"password"});
+        view.setVisibility(View.VISIBLE);
+        view.setLeft(20);
+        view.setRight(200);
+        view.setTop(20);
+        view.setBottom(200);
+        view.setPadding(0, 1, 2, 3);
+    }
+
+    // Only copy interesting view attributes, particularly the view attributes that are critical
+    // for calculating fingerprint. Keep Autofill Id different.
+    private View copySelectiveViewAttributes(View view) {
+        View copy;
+        if (view instanceof TextView) {
+            copy = new TextView(sContext);
+            copySelectiveTextViewAttributes((TextView) view, (TextView) copy);
+        } else {
+            copy = new View(sContext) {
+                public @AutofillType int getAutofillType() {
+                    return view.getAutofillType();
+                }
+            };
+        }
+        // Copy over interested view properties.
+        // Keep the order same as with the tested code for easier clarity.
+        copy.setVisibility(view.getVisibility());
+        copy.setAutofillHints(view.getAutofillHints());
+        copy.setContentDescription(view.getContentDescription());
+        copy.setTooltip(view.getTooltipText());
+
+        copy.setRight(view.getRight());
+        copy.setLeft(view.getLeft());
+        copy.setTop(view.getTop());
+        copy.setBottom(view.getBottom());
+        copy.setPadding(view.getPaddingLeft(), view.getPaddingTop(),
+                view.getPaddingRight(), view.getPaddingBottom());
+
+        // DO not copy over autofill id
+        AutofillId newId = new AutofillId(view.getAutofillId().getViewId() + MAGIC_AUTOFILL_NUMBER);
+        copy.setAutofillId(newId);
+        return copy;
+    }
+
+    private void copySelectiveTextViewAttributes(TextView fromView, TextView toView) {
+        toView.setInputType(fromView.getInputType());
+        toView.setHint(fromView.getHint());
+        toView.setSingleLine(fromView.isSingleLine());
+        toView.setImeOptions(fromView.getImeOptions());
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java
index 3a27225..178e93a 100644
--- a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java
+++ b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java
@@ -22,7 +22,7 @@
 
 import android.os.Parcel;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java b/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java
index 725dcf3..3d1b565 100644
--- a/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java
+++ b/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java
@@ -29,8 +29,8 @@
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.coretests.aidl.ITestServiceConnectorService;
 import com.android.internal.infra.ServiceConnectorTest.CapturingServiceLifecycleCallbacks.ServiceLifeCycleEvent;
diff --git a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
index 7054cc0..b86cb4a 100644
--- a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
+++ b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
@@ -20,8 +20,8 @@
 
 import android.metrics.LogMaker;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.internal.logging.testing.FakeMetricsLogger;
diff --git a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
index 7840f71..fc28627 100644
--- a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
+++ b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
@@ -18,8 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.logging.testing.UiEventLoggerFake;
 
diff --git a/core/tests/coretests/src/com/android/internal/policy/DecorContextTest.java b/core/tests/coretests/src/com/android/internal/policy/DecorContextTest.java
index 7f4e9ad..2f3b7f9 100644
--- a/core/tests/coretests/src/com/android/internal/policy/DecorContextTest.java
+++ b/core/tests/coretests/src/com/android/internal/policy/DecorContextTest.java
@@ -36,9 +36,9 @@
 import android.view.WindowManagerImpl;
 
 import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Rule;
diff --git a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
index 4921e4a..e037f2a 100644
--- a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
+++ b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
@@ -44,8 +44,8 @@
 import android.view.WindowManager;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.coretests.R;
 
diff --git a/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java b/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java
index d1ef61b..d1c0668 100644
--- a/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java
+++ b/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java
@@ -19,7 +19,7 @@
 
 import android.platform.test.ravenwood.RavenwoodRule;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/AcceptOnceConsumer.java
similarity index 98%
rename from libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
rename to libs/WindowManager/Jetpack/src/androidx/window/common/AcceptOnceConsumer.java
index 63828ab..c2f827a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/AcceptOnceConsumer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.util;
+package androidx.window.common;
 
 import android.annotation.NonNull;
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/BaseDataProducer.java
similarity index 98%
rename from libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
rename to libs/WindowManager/Jetpack/src/androidx/window/common/BaseDataProducer.java
index cd26efd..e7099dc 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/BaseDataProducer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.util;
+package androidx.window.common;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
@@ -125,4 +125,4 @@
             mCallbacksToRemove.add(callback);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
index e37dea4..b95bca1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
@@ -16,7 +16,7 @@
 
 package androidx.window.common;
 
-import static androidx.window.util.ExtensionHelper.isZero;
+import static androidx.window.common.ExtensionHelper.isZero;
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 98935e9..b2bc3de 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -31,9 +31,6 @@
 import android.util.Log;
 import android.util.SparseIntArray;
 
-import androidx.window.util.AcceptOnceConsumer;
-import androidx.window.util.BaseDataProducer;
-
 import com.android.internal.R;
 
 import java.util.ArrayList;
@@ -44,7 +41,7 @@
 import java.util.function.Consumer;
 
 /**
- * An implementation of {@link androidx.window.util.BaseDataProducer} that returns
+ * An implementation of {@link BaseDataProducer} that returns
  * the device's posture by mapping the state returned from {@link DeviceStateManager} to
  * values provided in the resources' config at {@link R.array#config_device_state_postures}.
  */
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java
similarity index 99%
rename from libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
rename to libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java
index a08db79..f466d60 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.util;
+package androidx.window.common;
 
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
index 88264f3..6d758f1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
@@ -26,15 +26,13 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 
-import androidx.window.util.BaseDataProducer;
-
 import com.android.internal.R;
 
 import java.util.Optional;
 import java.util.function.Consumer;
 
 /**
- * Implementation of {@link androidx.window.util.BaseDataProducer} that produces a
+ * Implementation of {@link BaseDataProducer} that produces a
  * {@link String} that can be parsed to a {@link CommonFoldingFeature}.
  * {@link RawFoldingFeatureProducer} searches for the value in two places. The first check is in
  * settings where the {@link String} property is saved with the key
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 8e1fde0..409cde3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -119,7 +119,8 @@
 
     // TODO(b/243518738): Move to WM Extensions if we have requirement of overlay without
     //  association. It's not set in WM Extensions nor Wm Jetpack library currently.
-    private static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY =
+    @VisibleForTesting
+    static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY =
             "androidx.window.extensions.embedding.shouldAssociateWithLaunchingActivity";
 
     @VisibleForTesting
@@ -2742,89 +2743,70 @@
         }
 
         final int taskId = getTaskId(launchActivity);
-        if (!overlayContainers.isEmpty()) {
-            for (final TaskFragmentContainer overlayContainer : overlayContainers) {
-                final boolean isTopNonFinishingOverlay = overlayContainer.equals(
-                        overlayContainer.getTaskContainer().getTopNonFinishingTaskFragmentContainer(
-                                true /* includePin */, true /* includeOverlay */));
-                if (taskId != overlayContainer.getTaskId()) {
-                    // If there's an overlay container with same tag in a different task,
-                    // dismiss the overlay container since the tag must be unique per process.
-                    if (overlayTag.equals(overlayContainer.getOverlayTag())) {
-                        Log.w(TAG, "The overlay container with tag:"
-                                + overlayContainer.getOverlayTag() + " is dismissed because"
-                                + " there's an existing overlay container with the same tag but"
-                                + " different task ID:" + overlayContainer.getTaskId() + ". "
-                                + "The new associated activity is " + launchActivity);
-                        mPresenter.cleanupContainer(wct, overlayContainer,
-                                false /* shouldFinishDependant */);
-                    }
-                    continue;
-                }
-                if (!overlayTag.equals(overlayContainer.getOverlayTag())) {
-                    // If there's an overlay container with different tag on top in the same
-                    // task, dismiss the existing overlay container.
-                    if (isTopNonFinishingOverlay) {
-                        mPresenter.cleanupContainer(wct, overlayContainer,
-                                false /* shouldFinishDependant */);
-                    }
-                    continue;
-                }
-                // The overlay container has the same tag and task ID with the new launching
-                // overlay container.
-                if (!isTopNonFinishingOverlay) {
-                    // Dismiss the invisible overlay container regardless of activity
-                    // association if it collides the tag of new launched overlay container .
-                    Log.w(TAG, "The invisible overlay container with tag:"
-                            + overlayContainer.getOverlayTag() + " is dismissed because"
-                            + " there's a launching overlay container with the same tag."
-                            + " The new associated activity is " + launchActivity);
-                    mPresenter.cleanupContainer(wct, overlayContainer,
-                            false /* shouldFinishDependant */);
-                    continue;
-                }
-                // Requesting an always-on-top overlay.
-                if (!associateLaunchingActivity) {
-                    if (overlayContainer.isOverlayWithActivityAssociation()) {
-                        // Dismiss the overlay container since it has associated with an activity.
-                        Log.w(TAG, "The overlay container with tag:"
-                                + overlayContainer.getOverlayTag() + " is dismissed because"
-                                + " there's an existing overlay container with the same tag but"
-                                + " different associated launching activity. The overlay container"
-                                + " doesn't associate with any activity.");
-                        mPresenter.cleanupContainer(wct, overlayContainer,
-                                false /* shouldFinishDependant */);
-                        continue;
-                    } else {
-                        // The existing overlay container doesn't associate an activity as well.
-                        // Just update the overlay and return.
-                        // Note that going to this condition means the tag, task ID matches a
-                        // visible always-on-top overlay, and won't dismiss any overlay any more.
-                        mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs,
-                                getMinDimensions(intent));
-                        return overlayContainer;
-                    }
-                }
-                if (launchActivity.getActivityToken()
-                        != overlayContainer.getAssociatedActivityToken()) {
-                    Log.w(TAG, "The overlay container with tag:"
-                            + overlayContainer.getOverlayTag() + " is dismissed because"
-                            + " there's an existing overlay container with the same tag but"
-                            + " different associated launching activity. The new associated"
-                            + " activity is " + launchActivity);
-                    // The associated activity must be the same, or it will be dismissed.
-                    mPresenter.cleanupContainer(wct, overlayContainer,
-                            false /* shouldFinishDependant */);
-                    continue;
-                }
-                // Reaching here means the launching activity launch an overlay container with the
-                // same task ID, tag, while there's a previously launching visible overlay
-                // container. We'll regard it as updating the existing overlay container.
+        // Overlay container policy:
+        // 1. Overlay tag must be unique per process.
+        //   a. For associated overlay, if a new launched overlay container has the same tag as
+        //      an existing one, the existing overlay will be dismissed regardless of its task
+        //      and window hierarchy.
+        //   b. For always-on-top overlay, if there's an overlay container has the same tag in the
+        //      launched task, the overlay container will be re-used, which means the
+        //      ActivityStackAttributes will be applied and the launched activity will be positioned
+        //      on top of the overlay container.
+        // 2. There must be at most one overlay that partially occludes a visible activity per task.
+        //   a. For associated overlay, only the top visible overlay container in the launched task
+        //      will be dismissed.
+        //   b. Always-on-top overlay is always visible. If there's an overlay with different tags
+        //      in the same task, the overlay will be dismissed in case an activity above
+        //      the overlay is dismissed and the overlay is shown unexpectedly.
+        for (final TaskFragmentContainer overlayContainer : overlayContainers) {
+            final boolean isTopNonFinishingOverlay = overlayContainer.isTopNonFinishingChild();
+            final boolean areInSameTask = taskId == overlayContainer.getTaskId();
+            final boolean haveSameTag = overlayTag.equals(overlayContainer.getOverlayTag());
+            if (!associateLaunchingActivity && overlayContainer.isAlwaysOnTopOverlay()
+                    && haveSameTag && areInSameTask) {
+                // Just launch the activity and update the existing always-on-top overlay
+                // if the requested overlay is an always-on-top overlay with the same tag
+                // as the existing one.
                 mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs,
                         getMinDimensions(intent));
                 return overlayContainer;
-
             }
+            if (haveSameTag) {
+                // For other tag match, we should clean up the existing overlay since the overlay
+                // tag must be unique per process.
+                Log.w(TAG, "The overlay container with tag:"
+                        + overlayContainer.getOverlayTag() + " is dismissed with "
+                        + " the launching activity=" + launchActivity
+                        + " because there's an existing overlay container with the same tag.");
+                mPresenter.cleanupContainer(wct, overlayContainer,
+                        false /* shouldFinishDependant */);
+            }
+            if (!areInSameTask) {
+                // Early return here because we won't clean-up or update overlay from different
+                // tasks except tag collision.
+                continue;
+            }
+            if (associateLaunchingActivity) {
+                // For associated overlay, we only dismiss the overlay if it's the top non-finishing
+                // child of its parent container.
+                if (isTopNonFinishingOverlay) {
+                    Log.w(TAG, "The on-top overlay container with tag:"
+                            + overlayContainer.getOverlayTag() + " is dismissed with "
+                            + " the launching activity=" + launchActivity
+                            + "because we only allow one overlay on top.");
+                    mPresenter.cleanupContainer(wct, overlayContainer,
+                            false /* shouldFinishDependant */);
+                }
+                continue;
+            }
+            // Otherwise, we should clean up the overlay in the task because we only allow one
+            // overlay when an always-on-top overlay is launched.
+            Log.w(TAG, "The overlay container with tag:"
+                    + overlayContainer.getOverlayTag() + " is dismissed with "
+                    + " the launching activity=" + launchActivity
+                    + "because an always-on-top overlay is launched.");
+            mPresenter.cleanupContainer(wct, overlayContainer,
+                    false /* shouldFinishDependant */);
         }
         // Launch the overlay container to the task with taskId.
         return createEmptyContainer(wct, intent, taskId, attrs, launchActivity, overlayTag,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 7173b0c..d0e2c99 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -340,6 +340,13 @@
         return mInfo != null && mInfo.isVisible();
     }
 
+    /**
+     * See {@link TaskFragmentInfo#isTopNonFinishingChild()}
+     */
+    boolean isTopNonFinishingChild() {
+        return mInfo != null && mInfo.isTopNonFinishingChild();
+    }
+
     /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/
     boolean isInIntermediateState() {
         if (mInfo == null) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 84984a9..a3ef68a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -21,9 +21,9 @@
 
 import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT;
 import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
-import static androidx.window.util.ExtensionHelper.isZero;
-import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
-import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
+import static androidx.window.common.ExtensionHelper.isZero;
+import static androidx.window.common.ExtensionHelper.rotateRectToDisplayRotation;
+import static androidx.window.common.ExtensionHelper.transformToWindowSpaceRect;
 
 import android.app.Activity;
 import android.app.ActivityThread;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
index 339908a..b63fd08 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
@@ -25,11 +25,11 @@
 import android.os.IBinder;
 
 import androidx.annotation.NonNull;
+import androidx.window.common.BaseDataProducer;
 import androidx.window.common.CommonFoldingFeature;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.common.EmptyLifecycleCallbacksAdapter;
 import androidx.window.common.RawFoldingFeatureProducer;
-import androidx.window.util.BaseDataProducer;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
index bb6ab47..4fd03e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
@@ -17,8 +17,8 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
-import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
-import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
+import static androidx.window.common.ExtensionHelper.rotateRectToDisplayRotation;
+import static androidx.window.common.ExtensionHelper.transformToWindowSpaceRect;
 
 import android.annotation.NonNull;
 import android.app.Activity;
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/util/ExtensionHelperTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/ExtensionHelperTest.java
similarity index 99%
rename from libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/util/ExtensionHelperTest.java
rename to libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/ExtensionHelperTest.java
index 3278cdf..b6e9519 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/util/ExtensionHelperTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/ExtensionHelperTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.util;
+package androidx.window.common;
 
 import static org.junit.Assert.assertEquals;
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index d649c6d..7dc78fd 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -163,12 +163,14 @@
     }
 
     /** Creates a mock TaskFragmentInfo for the given TaskFragment. */
+    @NonNull
     static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
             @NonNull Activity activity) {
         return createMockTaskFragmentInfo(container, activity, true /* isVisible */);
     }
 
     /** Creates a mock TaskFragmentInfo for the given TaskFragment. */
+    @NonNull
     static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
             @NonNull Activity activity, boolean isVisible) {
         return new TaskFragmentInfo(container.getTaskFragmentToken(),
@@ -182,7 +184,27 @@
                 false /* isTaskClearedForReuse */,
                 false /* isTaskFragmentClearedForPip */,
                 false /* isClearedForReorderActivityToFront */,
-                new Point());
+                new Point(),
+                false /* isTopChild */);
+    }
+
+    /** Creates a mock TaskFragmentInfo for the given TaskFragment. */
+    @NonNull
+    static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
+            @NonNull Activity activity, boolean isVisible, boolean isOnTop) {
+        return new TaskFragmentInfo(container.getTaskFragmentToken(),
+                mock(WindowContainerToken.class),
+                new Configuration(),
+                1,
+                isVisible,
+                Collections.singletonList(activity.getActivityToken()),
+                new ArrayList<>(),
+                new Point(),
+                false /* isTaskClearedForReuse */,
+                false /* isTaskFragmentClearedForPip */,
+                false /* isClearedForReorderActivityToFront */,
+                new Point(),
+                isOnTop);
     }
 
     static ActivityInfo createActivityInfoWithMinDimensions() {
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index ad41b18..8911d18 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -114,6 +114,7 @@
                 mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */,
                 false /* isVisible */, new ArrayList<>(), new ArrayList<>(), new Point(),
                 false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */,
-                false /* isClearedForReorderActivityToFront */, new Point());
+                false /* isClearedForReorderActivityToFront */, new Point(),
+                false /* isTopChild */);
     }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index 1c4c887..475475b 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -30,6 +30,7 @@
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer;
+import static androidx.window.extensions.embedding.SplitController.KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
@@ -94,6 +95,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
@@ -267,7 +269,7 @@
     }
 
     @Test
-    public void testCreateOrUpdateOverlay_visibleOverlaySameTagInTask_dismissOverlay() {
+    public void testCreateOrUpdateOverlay_topOverlayInTask_dismissOverlay() {
         createExistingOverlayContainers();
 
         final TaskFragmentContainer overlayContainer =
@@ -295,26 +297,6 @@
     }
 
     @Test
-    public void testCreateOrUpdateOverlay_sameTagTaskAndActivity_updateOverlay() {
-        createExistingOverlayContainers();
-
-        final Rect bounds = new Rect(0, 0, 100, 100);
-        mSplitController.setActivityStackAttributesCalculator(params ->
-                new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
-        final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
-                mOverlayContainer1.getOverlayTag());
-
-        assertWithMessage("overlayContainer1 must be updated since the new overlay container"
-                + " is launched with the same tag and task")
-                .that(mSplitController.getAllNonFinishingOverlayContainers())
-                .containsExactly(mOverlayContainer1, mOverlayContainer2);
-
-        assertThat(overlayContainer).isEqualTo(mOverlayContainer1);
-        verify(mSplitPresenter).resizeTaskFragment(eq(mTransaction),
-                eq(mOverlayContainer1.getTaskFragmentToken()), eq(bounds));
-    }
-
-    @Test
     public void testCreateOrUpdateOverlay_sameTagAndTaskButNotActivity_dismissOverlay() {
         createExistingOverlayContainers();
 
@@ -362,6 +344,43 @@
     }
 
     @Test
+    public void testCreateOrUpdateAlwaysOnTopOverlay_dismissMultipleOverlaysInTask() {
+        createExistingOverlayContainers();
+        // Create another overlay in task.
+        final TaskFragmentContainer overlayContainer3 =
+                createTestOverlayContainer(TASK_ID, "test3");
+        assertThat(mSplitController.getAllNonFinishingOverlayContainers())
+                .containsExactly(mOverlayContainer1, mOverlayContainer2, overlayContainer3);
+
+        final TaskFragmentContainer overlayContainer =
+                createOrUpdateAlwaysOnTopOverlay("test4");
+
+        assertWithMessage("overlayContainer1 and overlayContainer3 must be dismissed")
+                .that(mSplitController.getAllNonFinishingOverlayContainers())
+                .containsExactly(mOverlayContainer2, overlayContainer);
+    }
+
+    @Test
+    public void testCreateOrUpdateAlwaysOnTopOverlay_updateOverlay() {
+        createExistingOverlayContainers();
+        // Create another overlay in task.
+        final TaskFragmentContainer alwaysOnTopOverlay = createTestOverlayContainer(TASK_ID,
+                "test3", true /* isVisible */, false /* associateLaunchingActivity */);
+        final ActivityStackAttributes attrs = new ActivityStackAttributes.Builder()
+                .setRelativeBounds(new Rect(0, 0, 100, 100)).build();
+        mSplitController.setActivityStackAttributesCalculator(params -> attrs);
+
+        Mockito.clearInvocations(mSplitPresenter);
+        final TaskFragmentContainer overlayContainer =
+                createOrUpdateAlwaysOnTopOverlay(alwaysOnTopOverlay.getOverlayTag());
+
+        assertWithMessage("overlayContainer1 and overlayContainer3 must be dismissed")
+                .that(mSplitController.getAllNonFinishingOverlayContainers())
+                .containsExactly(mOverlayContainer2, alwaysOnTopOverlay);
+        assertThat(overlayContainer).isEqualTo(alwaysOnTopOverlay);
+    }
+
+    @Test
     public void testCreateOrUpdateOverlay_launchFromSplit_returnNull() {
         final Activity primaryActivity = createMockActivity();
         final Activity secondaryActivity = createMockActivity();
@@ -381,13 +400,13 @@
     }
 
     private void createExistingOverlayContainers() {
-        createExistingOverlayContainers(true /* visible */);
+        createExistingOverlayContainers(true /* isOnTop */);
     }
 
-    private void createExistingOverlayContainers(boolean visible) {
-        mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible,
+    private void createExistingOverlayContainers(boolean isOnTop) {
+        mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", isOnTop,
                 true /* associatedLaunchingActivity */, mActivity);
-        mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", visible);
+        mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", isOnTop);
         List<TaskFragmentContainer> overlayContainers = mSplitController
                 .getAllNonFinishingOverlayContainers();
         assertThat(overlayContainers).containsExactly(mOverlayContainer1, mOverlayContainer2);
@@ -966,6 +985,16 @@
                 launchOptions, mIntent, activity);
     }
 
+    @Nullable
+    private TaskFragmentContainer createOrUpdateAlwaysOnTopOverlay(
+            @NonNull String tag) {
+        final Bundle launchOptions = new Bundle();
+        launchOptions.putBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, false);
+        launchOptions.putString(KEY_OVERLAY_TAG, tag);
+        return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction,
+                launchOptions, mIntent, createMockActivity());
+    }
+
     /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
     @NonNull
     private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
@@ -975,10 +1004,10 @@
     /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
     @NonNull
     private TaskFragmentContainer createMockTaskFragmentContainer(
-            @NonNull Activity activity, boolean isVisible) {
+            @NonNull Activity activity, boolean isOnTop) {
         final TaskFragmentContainer container = createTfContainer(mSplitController,
                 activity.getTaskId(), activity);
-        setupTaskFragmentInfo(container, activity, isVisible);
+        setupTaskFragmentInfo(container, activity, isOnTop);
         return container;
     }
 
@@ -990,8 +1019,8 @@
 
     @NonNull
     private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag,
-            boolean isVisible) {
-        return createTestOverlayContainer(taskId, tag, isVisible,
+            boolean isOnTop) {
+        return createTestOverlayContainer(taskId, tag, isOnTop,
                 true /* associateLaunchingActivity */);
     }
 
@@ -1002,11 +1031,9 @@
                 null /* launchingActivity */);
     }
 
-    // TODO(b/243518738): add more test coverage on overlay container without activity association
-    //  once we have use cases.
     @NonNull
     private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag,
-            boolean isVisible, boolean associateLaunchingActivity,
+            boolean isOnTop, boolean associateLaunchingActivity,
             @Nullable Activity launchingActivity) {
         final Activity activity = launchingActivity != null
                 ? launchingActivity : createMockActivity();
@@ -1017,14 +1044,15 @@
                         .setLaunchOptions(Bundle.EMPTY)
                         .setAssociatedActivity(associateLaunchingActivity ? activity : null)
                         .build();
-        setupTaskFragmentInfo(overlayContainer, createMockActivity(), isVisible);
+        setupTaskFragmentInfo(overlayContainer, createMockActivity(), isOnTop);
         return overlayContainer;
     }
 
     private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container,
                                        @NonNull Activity activity,
-                                       boolean isVisible) {
-        final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity, isVisible);
+                                       boolean isOnTop) {
+        final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity, isOnTop,
+                isOnTop);
         container.setInfo(mTransaction, info);
         mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info);
     }
diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
index 257fe15..62782a7 100644
--- a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
+++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml
@@ -21,38 +21,6 @@
     android:orientation="vertical"
     android:gravity="bottom|end">
 
-    <include android:id="@+id/camera_compat_hint"
-        android:visibility="gone"
-        android:layout_width="@dimen/camera_compat_hint_width"
-        android:layout_height="wrap_content"
-        layout="@layout/compat_mode_hint"/>
-
-    <LinearLayout
-        android:id="@+id/camera_compat_control"
-        android:visibility="gone"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:clipToPadding="false"
-        android:layout_marginEnd="@dimen/compat_button_margin"
-        android:layout_marginBottom="@dimen/compat_button_margin"
-        android:orientation="vertical">
-
-        <ImageButton
-            android:id="@+id/camera_compat_treatment_button"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:background="@android:color/transparent"/>
-
-        <ImageButton
-            android:id="@+id/camera_compat_dismiss_button"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:src="@drawable/camera_compat_dismiss_ripple"
-            android:background="@android:color/transparent"
-            android:contentDescription="@string/camera_compat_dismiss_button_description"/>
-
-    </LinearLayout>
-
     <include android:id="@+id/size_compat_hint"
         android:visibility="gone"
         android:layout_width="@dimen/compat_hint_width"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 269a586..1eb2458 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -296,9 +296,6 @@
     <!-- The width of the compat hint. -->
     <dimen name="compat_hint_width">188dp</dimen>
 
-    <!-- The width of the camera compat hint. -->
-    <dimen name="camera_compat_hint_width">143dp</dimen>
-
     <!-- The corner radius of the letterbox education dialog. -->
     <dimen name="letterbox_education_dialog_corner_radius">28dp</dimen>
 
@@ -554,15 +551,10 @@
          enable_windowing_edge_drag_resize is disabled. -->
     <dimen name="freeform_resize_corner">44dp</dimen>
 
-    <!-- The width of the area at the sides of the screen where a freeform task will transition to
-    split select if dragged until the touch input is within the range. -->
-    <dimen name="desktop_mode_transition_area_width">32dp</dimen>
+    <!-- The thickness in dp for all desktop drag transition regions. -->
+    <dimen name="desktop_mode_transition_region_thickness">44dp</dimen>
 
-    <!-- The width of the area where a desktop task will transition to fullscreen. -->
-    <dimen name="desktop_mode_fullscreen_from_desktop_width">80dp</dimen>
-
-    <!-- The height of the area where a desktop task will transition to fullscreen. -->
-    <dimen name="desktop_mode_fullscreen_from_desktop_height">40dp</dimen>
+    <item type="dimen" format="float" name="desktop_mode_fullscreen_region_scale">0.4</item>
 
     <!-- The height on the screen where drag to the left or right edge will result in a
     desktop task snapping to split size. The empty space between this and the top is to allow
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index b1c9a77..1304969 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -35,6 +35,7 @@
 ) {
   // All desktop mode related flags will be added here
   DESKTOP_WINDOWING_MODE(Flags::enableDesktopWindowingMode, true),
+  CASCADING_WINDOWS(Flags::enableCascadingWindows, true),
   WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity, true),
   MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true),
   THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true),
@@ -42,7 +43,8 @@
   APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true),
   TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true),
   SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true),
-  DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true);
+  DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true),
+  ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true);
 
   /**
    * Determines state of flag based on the actual flag and desktop mode developer option overrides.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index f014e55..452d12a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -24,7 +24,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
-import static com.android.wm.shell.compatui.impl.CompatUIEventsKt.CAMERA_CONTROL_STATE_UPDATE;
 import static com.android.wm.shell.compatui.impl.CompatUIEventsKt.SIZE_COMPAT_RESTART_BUTTON_APPEARED;
 import static com.android.wm.shell.compatui.impl.CompatUIEventsKt.SIZE_COMPAT_RESTART_BUTTON_CLICKED;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
@@ -61,7 +60,6 @@
 import com.android.wm.shell.compatui.CompatUIController;
 import com.android.wm.shell.compatui.api.CompatUIHandler;
 import com.android.wm.shell.compatui.api.CompatUIInfo;
-import com.android.wm.shell.compatui.impl.CompatUIEvents.CameraControlStateUpdated;
 import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonAppeared;
 import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked;
 import com.android.wm.shell.recents.RecentTasksController;
@@ -265,9 +263,6 @@
                     case SIZE_COMPAT_RESTART_BUTTON_CLICKED:
                         onSizeCompatRestartButtonClicked(compatUIEvent.asType());
                         break;
-                    case CAMERA_CONTROL_STATE_UPDATE:
-                        onCameraControlStateUpdated(compatUIEvent.asType());
-                        break;
                     default:
 
                 }
@@ -690,6 +685,15 @@
         return result;
     }
 
+    /** Return list of {@link RunningTaskInfo}s on all the displays. */
+    public ArrayList<RunningTaskInfo> getRunningTasks() {
+        ArrayList<RunningTaskInfo> result = new ArrayList<>();
+        for (int i = 0; i < mTasks.size(); i++) {
+            result.add(mTasks.valueAt(i).getTaskInfo());
+        }
+        return result;
+    }
+
     /** Gets running task by taskId. Returns {@code null} if no such task observed. */
     @Nullable
     public RunningTaskInfo getRunningTaskInfo(int taskId) {
@@ -808,21 +812,6 @@
         restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token);
     }
 
-    @VisibleForTesting
-    void onCameraControlStateUpdated(@NonNull CameraControlStateUpdated compatUIEvent) {
-        final int taskId = compatUIEvent.getTaskId();
-        final int state = compatUIEvent.getState();
-        final TaskAppearedInfo info;
-        synchronized (mLock) {
-            info = mTasks.get(taskId);
-        }
-        if (info == null) {
-            return;
-        }
-        updateCameraCompatControlState(info.getTaskInfo().token, state);
-    }
-
-
     private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
             int event) {
         ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 972dce5..24c568c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -169,6 +169,8 @@
                     R.layout.bubble_overflow_container, null /* root */);
             mOverflowView.initialize(expandedViewManager, positioner);
             addView(mOverflowView);
+            // Don't show handle for overflow
+            mHandleView.setVisibility(View.GONE);
         } else {
             mTaskView = bubbleTaskView.getTaskView();
             mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, expandedViewManager,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 4fbb574..d7d19f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -315,7 +315,7 @@
                         }
                     }
                     if (!mImeShowing) {
-                        removeImeSurface();
+                        removeImeSurface(mDisplayId);
                     }
                 }
             } else if (!android.view.inputmethod.Flags.refactorInsetsController()
@@ -617,7 +617,7 @@
                                 || hasLeash) {
                             t.hide(mImeSourceControl.getLeash());
                         }
-                        removeImeSurface();
+                        removeImeSurface(mDisplayId);
                         ImeTracker.forLogging().onHidden(mStatsToken);
                     } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) {
                         ImeTracker.forLogging().onShown(mStatsToken);
@@ -671,10 +671,10 @@
         }
     }
 
-    void removeImeSurface() {
+    void removeImeSurface(int displayId) {
         // Remove the IME surface to make the insets invisible for
         // non-client controlled insets.
-        InputMethodManagerGlobal.removeImeSurface(
+        InputMethodManagerGlobal.removeImeSurface(displayId,
                 e -> Slog.e(TAG, "Failed to remove IME surface.", e));
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
index 2b0bd32..688f8ca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
@@ -16,10 +16,7 @@
 
 package com.android.wm.shell.compatui;
 
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-
 import android.annotation.IdRes;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.View;
@@ -57,28 +54,10 @@
         mWindowManager = windowManager;
     }
 
-    void updateCameraTreatmentButton(@CameraCompatControlState int newState) {
-        int buttonBkgId = newState == CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED
-                ? R.drawable.camera_compat_treatment_suggested_ripple
-                : R.drawable.camera_compat_treatment_applied_ripple;
-        int hintStringId = newState == CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED
-                ? R.string.camera_compat_treatment_suggested_button_description
-                : R.string.camera_compat_treatment_applied_button_description;
-        final ImageButton button = findViewById(R.id.camera_compat_treatment_button);
-        button.setImageResource(buttonBkgId);
-        button.setContentDescription(getResources().getString(hintStringId));
-        final LinearLayout hint = findViewById(R.id.camera_compat_hint);
-        ((TextView) hint.findViewById(R.id.compat_mode_hint_text)).setText(hintStringId);
-    }
-
     void setSizeCompatHintVisibility(boolean show) {
         setViewVisibility(R.id.size_compat_hint, show);
     }
 
-    void setCameraCompatHintVisibility(boolean show) {
-        setViewVisibility(R.id.camera_compat_hint, show);
-    }
-
     void setRestartButtonVisibility(boolean show) {
         setViewVisibility(R.id.size_compat_restart_button, show);
         // Hint should never be visible without button.
@@ -87,14 +66,6 @@
         }
     }
 
-    void setCameraControlVisibility(boolean show) {
-        setViewVisibility(R.id.camera_compat_control, show);
-        // Hint should never be visible without button.
-        if (!show) {
-            setCameraCompatHintVisibility(/* show= */ false);
-        }
-    }
-
     private void setViewVisibility(@IdRes int resId, boolean show) {
         final View view = findViewById(resId);
         int visibility = show ? View.VISIBLE : View.GONE;
@@ -127,26 +98,5 @@
         ((TextView) sizeCompatHint.findViewById(R.id.compat_mode_hint_text))
                 .setText(R.string.restart_button_description);
         sizeCompatHint.setOnClickListener(view -> setSizeCompatHintVisibility(/* show= */ false));
-
-        final ImageButton cameraTreatmentButton =
-                findViewById(R.id.camera_compat_treatment_button);
-        cameraTreatmentButton.setOnClickListener(
-                view -> mWindowManager.onCameraTreatmentButtonClicked());
-        cameraTreatmentButton.setOnLongClickListener(view -> {
-            mWindowManager.onCameraButtonLongClicked();
-            return true;
-        });
-
-        final ImageButton cameraDismissButton = findViewById(R.id.camera_compat_dismiss_button);
-        cameraDismissButton.setOnClickListener(
-                view -> mWindowManager.onCameraDismissButtonClicked());
-        cameraDismissButton.setOnLongClickListener(view -> {
-            mWindowManager.onCameraButtonLongClicked();
-            return true;
-        });
-
-        final LinearLayout cameraCompatHint = findViewById(R.id.camera_compat_hint);
-        cameraCompatHint.setOnClickListener(
-                view -> setCameraCompatHintVisibility(/* show= */ false));
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index d289ef2..271c07d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -16,10 +16,6 @@
 
 package com.android.wm.shell.compatui;
 
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
 import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
 import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
 
@@ -27,11 +23,9 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.TaskInfo;
 import android.content.Context;
 import android.graphics.Rect;
-import android.util.Log;
 import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -44,7 +38,6 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
 import com.android.wm.shell.compatui.api.CompatUIEvent;
-import com.android.wm.shell.compatui.impl.CompatUIEvents.CameraControlStateUpdated;
 import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonAppeared;
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 
@@ -69,10 +62,6 @@
     boolean mHasSizeCompat;
 
     @VisibleForTesting
-    @CameraCompatControlState
-    int mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN;
-
-    @VisibleForTesting
     @NonNull
     CompatUIHintsState mCompatUIHintsState;
 
@@ -99,8 +88,6 @@
             // Don't show the SCM button for freeform tasks
             mHasSizeCompat &= !taskInfo.isFreeform();
         }
-        mCameraCompatControlState =
-                taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;
         mCompatUIHintsState = compatUIHintsState;
         mCompatUIConfiguration = compatUIConfiguration;
         mOnRestartButtonClicked = onRestartButtonClicked;
@@ -124,8 +111,7 @@
 
     @Override
     protected boolean eligibleToShowLayout() {
-        return (mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo()))
-                || shouldShowCameraControl();
+        return mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo());
     }
 
     @Override
@@ -152,22 +138,18 @@
     public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
             boolean canShow) {
         final boolean prevHasSizeCompat = mHasSizeCompat;
-        final int prevCameraCompatControlState = mCameraCompatControlState;
         mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
         if (DESKTOP_WINDOWING_MODE.isEnabled(mContext)
                 && DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)) {
             // Don't show the SCM button for freeform tasks
             mHasSizeCompat &= !taskInfo.isFreeform();
         }
-        mCameraCompatControlState =
-                taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;
 
         if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) {
             return false;
         }
 
-        if (prevHasSizeCompat != mHasSizeCompat
-                || prevCameraCompatControlState != mCameraCompatControlState) {
+        if (prevHasSizeCompat != mHasSizeCompat) {
             updateVisibilityOfViews();
         }
 
@@ -179,34 +161,6 @@
         mOnRestartButtonClicked.accept(Pair.create(getLastTaskInfo(), getTaskListener()));
     }
 
-    /** Called when the camera treatment button is clicked. */
-    void onCameraTreatmentButtonClicked() {
-        if (!shouldShowCameraControl()) {
-            Log.w(getTag(), "Camera compat shouldn't receive clicks in the hidden state.");
-            return;
-        }
-        // When a camera control is shown, only two states are allowed: "treament applied" and
-        // "treatment suggested". Clicks on the conrol's treatment button toggle between these
-        // two states.
-        mCameraCompatControlState =
-                mCameraCompatControlState == CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED
-                        ? CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED
-                        : CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        mCallback.accept(new CameraControlStateUpdated(mTaskId, mCameraCompatControlState));
-        mLayout.updateCameraTreatmentButton(mCameraCompatControlState);
-    }
-
-    /** Called when the camera dismiss button is clicked. */
-    void onCameraDismissButtonClicked() {
-        if (!shouldShowCameraControl()) {
-            Log.w(getTag(), "Camera compat shouldn't receive clicks in the hidden state.");
-            return;
-        }
-        mCameraCompatControlState = CAMERA_COMPAT_CONTROL_DISMISSED;
-        mCallback.accept(new CameraControlStateUpdated(mTaskId, CAMERA_COMPAT_CONTROL_DISMISSED));
-        mLayout.setCameraControlVisibility(/* show= */ false);
-    }
-
     /** Called when the restart button is long clicked. */
     void onRestartButtonLongClicked() {
         if (mLayout == null) {
@@ -215,14 +169,6 @@
         mLayout.setSizeCompatHintVisibility(/* show= */ true);
     }
 
-    /** Called when either dismiss or treatment camera buttons is long clicked. */
-    void onCameraButtonLongClicked() {
-        if (mLayout == null) {
-            return;
-        }
-        mLayout.setCameraCompatHintVisibility(/* show= */ true);
-    }
-
     @Override
     @VisibleForTesting
     public void updateSurfacePosition() {
@@ -270,6 +216,7 @@
             return false;
         }
         final float percentageAreaOfLetterboxInTask = (float) letterboxArea / taskArea * 100;
+
         return percentageAreaOfLetterboxInTask < mHideScmTolerance;
     }
 
@@ -284,21 +231,5 @@
             mLayout.setSizeCompatHintVisibility(/* show= */ true);
             mCompatUIHintsState.mHasShownSizeCompatHint = true;
         }
-
-        // Camera control for stretched issues.
-        mLayout.setCameraControlVisibility(shouldShowCameraControl());
-        // Only show by default for the first time.
-        if (shouldShowCameraControl() && !mCompatUIHintsState.mHasShownCameraCompatHint) {
-            mLayout.setCameraCompatHintVisibility(/* show= */ true);
-            mCompatUIHintsState.mHasShownCameraCompatHint = true;
-        }
-        if (shouldShowCameraControl()) {
-            mLayout.updateCameraTreatmentButton(mCameraCompatControlState);
-        }
-    }
-
-    private boolean shouldShowCameraControl() {
-        return mCameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN
-                && mCameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED;
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt
index 58ce8ed..23205c3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt
@@ -16,13 +16,10 @@
 
 package com.android.wm.shell.compatui.impl
 
-import android.app.AppCompatTaskInfo
-import android.app.CameraCompatTaskInfo
 import com.android.wm.shell.compatui.api.CompatUIEvent
 
 internal const val SIZE_COMPAT_RESTART_BUTTON_APPEARED = 0
 internal const val SIZE_COMPAT_RESTART_BUTTON_CLICKED = 1
-internal const val CAMERA_CONTROL_STATE_UPDATE = 2
 
 /**
  * All the {@link CompatUIEvent} the Compat UI Framework can handle
@@ -35,10 +32,4 @@
     /** Sent when the size compat restart button is clicked. */
     data class SizeCompatRestartButtonClicked(val taskId: Int) :
             CompatUIEvents(SIZE_COMPAT_RESTART_BUTTON_CLICKED)
-
-    /** Sent when the camera compat control state is updated. */
-    data class CameraControlStateUpdated(
-            val taskId: Int,
-            @CameraCompatTaskInfo.CameraCompatControlState val state: Int
-    ) : CompatUIEvents(CAMERA_CONTROL_STATE_UPDATE)
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index afe46f5..0262507 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.dagger;
 
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
 
 import android.annotation.Nullable;
 import android.app.KeyguardManager;
@@ -569,7 +569,7 @@
             ShellTaskOrganizer shellTaskOrganizer) {
         int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context);
         if (!DesktopModeStatus.canEnterDesktopMode(context)
-                || DESKTOP_WINDOWING_MODE.isEnabled(context)
+                || !ENABLE_DESKTOP_WINDOWING_TASK_LIMIT.isEnabled(context)
                 || maxTaskLimit <= 0) {
             return Optional.empty();
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index ea7e968..06c1e68 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -104,6 +104,7 @@
             TaskStackListenerImpl taskStackListener,
             ShellTaskOrganizer shellTaskOrganizer,
             PipTransitionState pipTransitionState,
+            PipTouchHandler pipTouchHandler,
             @ShellMainThread ShellExecutor mainExecutor) {
         if (!PipUtils.isPip2ExperimentEnabled()) {
             return Optional.empty();
@@ -112,7 +113,7 @@
                     context, shellInit, shellCommandHandler, shellController, displayController,
                     displayInsetsController, pipBoundsState, pipBoundsAlgorithm,
                     pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer,
-                    pipTransitionState, mainExecutor));
+                    pipTransitionState, pipTouchHandler, mainExecutor));
         }
     }
 
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 4299841..4060900c 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
@@ -213,7 +213,7 @@
      * If task was visible on a different display with a different [displayId], removes from
      * the set of visible tasks on that display and notifies listeners.
      */
-    fun updateVisibleFreeformTasks(displayId: Int, taskId: Int, visible: Boolean) {
+    fun updateTaskVisibility(displayId: Int, taskId: Int, visible: Boolean) {
         if (visible) {
             // If task is visible, remove it from any other display besides [displayId].
             removeVisibleTask(taskId, excludedDisplayId = displayId)
@@ -250,11 +250,17 @@
             logD("getVisibleTaskCount=$it")
         }
 
-    /** Adds task (or moves if it already exists) to the top of the ordered list. */
+    /**
+     * Adds task (or moves if it already exists) to the top of the ordered list.
+     *
+     * Unminimizes the task if it is minimized.
+     */
     fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) {
         logD("Add or move task to top: display=%d taskId=%d", taskId, displayId)
         desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.remove(taskId)
         desktopTaskDataByDisplayId.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId)
+        // Unminimize the task if it is minimized.
+        unminimizeTask(displayId, taskId)
     }
 
     /** Minimizes the task for [taskId] and [displayId] */
@@ -270,13 +276,37 @@
             logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId)
     }
 
-    /** Removes task from the ordered list. */
+    private fun getDisplayIdForTask(taskId: Int): Int? {
+        desktopTaskDataByDisplayId.forEach { displayId, data ->
+            if (taskId in data.freeformTasksInZOrder) {
+                return displayId
+            }
+        }
+        logW("No display id found for task: taskId=%d", taskId)
+        return null
+    }
+
+    /**
+     * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id
+     * will be looked up from the task id.
+     */
     fun removeFreeformTask(displayId: Int, taskId: Int) {
         logD("Removes freeform task: taskId=%d", taskId)
+        if (displayId == INVALID_DISPLAY) {
+            // Removes the original display id of the task.
+            getDisplayIdForTask(taskId)?.let { removeTaskFromDisplay(it, taskId) }
+        } else {
+            removeTaskFromDisplay(displayId, taskId)
+        }
+    }
+
+    /** Removes given task from a valid [displayId]. */
+    private fun removeTaskFromDisplay(displayId: Int, taskId: Int) {
+        logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId)
         desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.remove(taskId)
         boundsBeforeMaximizeByTaskId.remove(taskId)
-        logD("Remaining freeform tasks: %d",
-            desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString() ?: "")
+        logD("Remaining freeform tasks: %s",
+            desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString())
     }
 
     /**
@@ -358,3 +388,4 @@
 
 private fun <T> Iterable<T>.toDumpString(): String =
     joinToString(separator = ", ", prefix = "[", postfix = "]")
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index da212e7..9fcf73d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -52,8 +52,10 @@
     val idealSize = calculateIdealSize(screenBounds, scale)
     // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated.
     // Instead default to the desired initial bounds.
+    val stableBounds = Rect()
+    displayLayout.getStableBoundsForDesktopMode(stableBounds)
     val topActivityInfo =
-        taskInfo.topActivityInfo ?: return positionInScreen(idealSize, screenBounds)
+        taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds)
 
     val initialSize: Size =
         when (taskInfo.configuration.orientation) {
@@ -100,7 +102,7 @@
             }
         }
 
-    return positionInScreen(initialSize, screenBounds)
+    return positionInScreen(initialSize, stableBounds)
 }
 
 /**
@@ -163,17 +165,11 @@
 }
 
 /** Adjusts bounds to be positioned in the middle of the screen. */
-private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect {
-    // TODO(b/325240051): Position apps with bottom heavy offset
-    val heightOffset = (screenBounds.height() - desiredSize.height) / 2
-    val widthOffset = (screenBounds.width() - desiredSize.width) / 2
-    return Rect(
-        widthOffset,
-        heightOffset,
-        desiredSize.width + widthOffset,
-        desiredSize.height + heightOffset
-    )
-}
+private fun positionInScreen(desiredSize: Size, stableBounds: Rect): Rect =
+    Rect(0, 0, desiredSize.width, desiredSize.height).apply {
+        val offset = DesktopTaskPosition.Center.getTopLeftCoordinates(stableBounds, this)
+        offsetTo(offset.x, offset.y)
+    }
 
 /**
  * Adjusts bounds to be positioned in the middle of the area provided, not necessarily the
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index ed0d2b8..6011db7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -105,7 +105,7 @@
         // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone.
         IndicatorType result = IndicatorType.NO_INDICATOR;
         final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize(
-                com.android.wm.shell.R.dimen.desktop_mode_transition_area_width);
+                com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness);
         // Because drags in freeform use task position for indicator calculation, we need to
         // account for the possibility of the task going off the top of the screen by captionHeight
         final int captionHeight = mContext.getResources().getDimensionPixelSize(
@@ -140,18 +140,19 @@
         final Region region = new Region();
         int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM
                 ? mContext.getResources().getDimensionPixelSize(
-                com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height)
+                com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness)
                 : 2 * layout.stableInsets().top;
-        // A thin, short Rect at the top of the screen.
+        // A Rect at the top of the screen that takes up the center 40%.
         if (windowingMode == WINDOWING_MODE_FREEFORM) {
-            int fromFreeformWidth = mContext.getResources().getDimensionPixelSize(
-                    com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width);
-            region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2),
+            final float toFullscreenScale = mContext.getResources().getFloat(
+                    R.dimen.desktop_mode_fullscreen_region_scale);
+            final float toFullscreenWidth = (layout.width() * toFullscreenScale);
+            region.union(new Rect((int) ((layout.width() / 2f) - (toFullscreenWidth / 2f)),
                     -captionHeight,
-                    (layout.width() / 2) + (fromFreeformWidth / 2),
+                    (int) ((layout.width() / 2f) + (toFullscreenWidth / 2f)),
                     transitionHeight));
         }
-        // A screen-wide, shorter Rect if the task is in fullscreen or split.
+        // A screen-wide Rect if the task is in fullscreen or split.
         if (windowingMode == WINDOWING_MODE_FULLSCREEN
                 || windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
             region.union(new Rect(0,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
new file mode 100644
index 0000000..97abda8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.TaskInfo
+import android.content.res.Resources
+import android.graphics.Point
+import android.graphics.Rect
+import android.view.Gravity
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomLeft
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomRight
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.Center
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopLeft
+import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopRight
+import com.android.wm.shell.R
+
+/**
+ * The position of a task window in desktop mode.
+ */
+sealed class DesktopTaskPosition {
+    data object Center : DesktopTaskPosition() {
+        private const val WINDOW_HEIGHT_PROPORTION = 0.375
+
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+            val x = (frame.width() - window.width()) / 2
+            // Position with more margin at the bottom.
+            val y = (frame.height() - window.height()) * WINDOW_HEIGHT_PROPORTION + frame.top
+            return Point(x, y.toInt())
+        }
+
+        override fun next(): DesktopTaskPosition {
+            return BottomRight
+        }
+    }
+
+    data object BottomRight : DesktopTaskPosition() {
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+            return Point(frame.right - window.width(), frame.bottom - window.height())
+        }
+
+        override fun next(): DesktopTaskPosition {
+            return TopLeft
+        }
+    }
+
+    data object TopLeft : DesktopTaskPosition() {
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+            return Point(frame.left, frame.top)
+        }
+
+        override fun next(): DesktopTaskPosition {
+            return BottomLeft
+        }
+    }
+
+    data object BottomLeft : DesktopTaskPosition() {
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+            return Point(frame.left, frame.bottom - window.height())
+        }
+
+        override fun next(): DesktopTaskPosition {
+            return TopRight
+        }
+    }
+
+    data object TopRight : DesktopTaskPosition() {
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
+            return Point(frame.right - window.width(), frame.top)
+        }
+
+        override fun next(): DesktopTaskPosition {
+            return Center
+        }
+    }
+
+    /**
+     * Returns the top left coordinates for the window to be placed in the given
+     * DesktopTaskPosition in the frame.
+     */
+    abstract fun getTopLeftCoordinates(frame: Rect, window: Rect): Point
+
+    abstract fun next(): DesktopTaskPosition
+}
+
+/**
+ * If the app has specified horizontal or vertical gravity layout, don't change the
+ * task position for cascading effect.
+ */
+fun canChangeTaskPosition(taskInfo: TaskInfo): Boolean {
+    taskInfo.topActivityInfo?.windowLayout?.let {
+        val horizontalGravityApplied = it.gravity.and(Gravity.HORIZONTAL_GRAVITY_MASK)
+        val verticalGravityApplied = it.gravity.and(Gravity.VERTICAL_GRAVITY_MASK)
+        return horizontalGravityApplied == 0 && verticalGravityApplied == 0
+    }
+    return true
+}
+
+/**
+ * Returns the current DesktopTaskPosition for a given window in the frame.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+fun Rect.getDesktopTaskPosition(bounds: Rect): DesktopTaskPosition {
+    return when {
+        top == bounds.top && left == bounds.left -> TopLeft
+        top == bounds.top && right == bounds.right -> TopRight
+        bottom == bounds.bottom && left == bounds.left -> BottomLeft
+        bottom == bounds.bottom && right == bounds.right -> BottomRight
+        else -> Center
+    }
+}
+
+internal fun cascadeWindow(res: Resources, frame: Rect, prev: Rect, dest: Rect) {
+    val candidateBounds = Rect(dest)
+    val lastPos = frame.getDesktopTaskPosition(prev)
+    var destCoord = Center.getTopLeftCoordinates(frame, candidateBounds)
+    candidateBounds.offsetTo(destCoord.x, destCoord.y)
+    // If the default center position is not free or if last focused window is not at the
+    // center, get the next cascading window position.
+    if (!prevBoundsMovedAboveThreshold(res, prev, candidateBounds) || Center != lastPos) {
+        val nextCascadingPos = lastPos.next()
+        destCoord = nextCascadingPos.getTopLeftCoordinates(frame, dest)
+    }
+    dest.offsetTo(destCoord.x, destCoord.y)
+}
+
+internal fun prevBoundsMovedAboveThreshold(res: Resources, prev: Rect, newBounds: Rect): Boolean {
+    // This is the required minimum dp for a task to be touchable.
+    val moveThresholdPx = res.getDimensionPixelSize(
+        R.dimen.freeform_required_visible_empty_space_in_header)
+    val leftFar = newBounds.left - prev.left > moveThresholdPx
+    val topFar = newBounds.top - prev.top > moveThresholdPx
+    val rightFar = prev.right - newBounds.right > moveThresholdPx
+    val bottomFar = prev.bottom - newBounds.bottom > moveThresholdPx
+
+    return leftFar || topFar || rightFar || bottomFar
+}
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 9d3b2e5..5f838d3 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
@@ -121,7 +121,7 @@
     private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
     private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
     private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
-    private val desktopModeTaskRepository: DesktopModeTaskRepository,
+    private val taskRepository: DesktopModeTaskRepository,
     private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
     private val launchAdjacentController: LaunchAdjacentController,
     private val recentsTransitionHandler: RecentsTransitionHandler,
@@ -181,7 +181,7 @@
     }
 
     private fun onInit() {
-        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController")
+        logD("onInit")
         shellCommandHandler.addDumpCallback(this::dump, this)
         shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, this)
         shellController.addExternalInterface(
@@ -190,16 +190,12 @@
             this
         )
         transitions.addHandler(this)
-        desktopModeTaskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor)
+        taskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor)
         dragToDesktopTransitionHandler.setDragToDesktopStateListener(dragToDesktopStateListener)
         recentsTransitionHandler.addTransitionStateListener(
             object : RecentsTransitionStateListener {
                 override fun onAnimationStateChanged(running: Boolean) {
-                    ProtoLog.v(
-                        WM_SHELL_DESKTOP_MODE,
-                        "DesktopTasksController: recents animation state changed running=%b",
-                        running
-                    )
+                    logV("Recents animation state changed running=%b", running)
                     recentsAnimationRunning = running
                 }
             }
@@ -227,7 +223,7 @@
     /** Returns the transition type for the given remote transition. */
     private fun transitionType(remoteTransition: RemoteTransition?): Int {
         if (remoteTransition == null) {
-            ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: remoteTransition is null")
+            logV("RemoteTransition is null")
             return TRANSIT_NONE
         }
         return TRANSIT_TO_FRONT
@@ -235,7 +231,7 @@
 
     /** Show all tasks, that are part of the desktop, on top of launcher */
     fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) {
-        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps")
+        logV("showDesktopApps")
         val wct = WindowContainerTransaction()
         bringDesktopAppsToFront(displayId, wct)
 
@@ -255,7 +251,7 @@
 
     /** Gets number of visible tasks in [displayId]. */
     fun visibleTaskCount(displayId: Int): Int =
-        desktopModeTaskRepository.getVisibleTaskCount(displayId)
+        taskRepository.getVisibleTaskCount(displayId)
 
     /** Returns true if any tasks are visible in Desktop Mode. */
     fun isDesktopModeShowing(displayId: Int): Boolean = visibleTaskCount(displayId) > 0
@@ -286,14 +282,8 @@
                     // Fullscreen case where we move the current focused task.
                     moveToDesktop(allFocusedTasks[0].taskId, transitionSource = transitionSource)
                 }
-                else -> {
-                    ProtoLog.w(
-                        WM_SHELL_DESKTOP_MODE,
-                        "DesktopTasksController: Cannot enter desktop, expected less " +
-                            "than 3 focused tasks but found %d",
-                        allFocusedTasks.size
-                    )
-                }
+                else -> logW("Cannot enter desktop, expected < 3 focused tasks, found %d",
+                    allFocusedTasks.size)
             }
         }
     }
@@ -317,11 +307,7 @@
         transitionSource: DesktopModeTransitionSource,
     ): Boolean {
         recentTasksController?.findTaskInBackground(taskId)?.let {
-            ProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: moveToDesktopFromNonRunningTask taskId=%d",
-                taskId
-            )
+            logV("moveToDesktopFromNonRunningTask with taskId=%d, displayId=%d", taskId)
             // TODO(342378842): Instead of using default display, support multiple displays
             val taskToMinimize =
                 bringDesktopAppsToFrontBeforeShowingNewTask(DEFAULT_DISPLAY, wct, taskId)
@@ -351,18 +337,10 @@
     ) {
         if (DesktopModeFlags.MODALS_POLICY.isEnabled(context)
             && isTopActivityExemptFromDesktopWindowing(context, task)) {
-            ProtoLog.w(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: Cannot enter desktop, " +
-                        "ineligible top activity found."
-            )
+            logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId)
             return
         }
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: moveToDesktop taskId=%d",
-            task.taskId
-        )
+        logV("moveToDesktop taskId=%d", task.taskId)
         exitSplitIfApplicable(wct, task)
         // Bring other apps to front first
         val taskToMinimize =
@@ -386,11 +364,7 @@
         dragToDesktopValueAnimator: MoveToDesktopAnimator,
         taskSurface: SurfaceControl,
     ) {
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: startDragToDesktop taskId=%d",
-            taskInfo.taskId
-        )
+        logV("startDragToDesktop taskId=%d", taskInfo.taskId)
         interactionJankMonitor.begin(taskSurface, context,
             CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
         dragToDesktopTransitionHandler.startDragToDesktopTransition(
@@ -403,7 +377,7 @@
      * The second part of the animated drag to desktop transition, called after
      * [startDragToDesktop].
      */
-    private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
+    private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) {
         ProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
             "DesktopTasksController: finalizeDragToDesktop taskId=%d",
@@ -415,7 +389,6 @@
         val taskToMinimize =
             bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
         addMoveToDesktopChanges(wct, taskInfo)
-        wct.setBounds(taskInfo.token, freeformBounds)
         val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
         transition?.let { addPendingMinimizeTransition(it, taskToMinimize) }
     }
@@ -443,10 +416,10 @@
      * @param taskId task id of the window that's being closed
      */
     fun onDesktopWindowClose(wct: WindowContainerTransaction, displayId: Int, taskId: Int) {
-        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskId)) {
+        if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
             removeWallpaperActivity(wct)
         }
-        desktopModeTaskRepository.addClosingTask(displayId, taskId)
+        taskRepository.addClosingTask(displayId, taskId)
     }
 
     /** Move a task with given `taskId` to fullscreen */
@@ -465,11 +438,7 @@
 
     /** Move a desktop app to split screen. */
     fun moveToSplit(task: RunningTaskInfo) {
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: moveToSplit taskId=%d",
-            task.taskId
-        )
+        logV( "moveToSplit taskId=%s", task.taskId)
         val wct = WindowContainerTransaction()
         wct.setBounds(task.token, Rect())
         // Rather than set windowing mode to multi-window at task level, set it to
@@ -498,11 +467,7 @@
      * [startDragToDesktop].
      */
     fun cancelDragToDesktop(task: RunningTaskInfo) {
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: cancelDragToDesktop taskId=%d",
-            task.taskId
-        )
+        logV("cancelDragToDesktop taskId=%d", task.taskId)
         dragToDesktopTransitionHandler.cancelDragToDesktopTransition(
             DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
         )
@@ -513,11 +478,7 @@
         position: Point,
         transitionSource: DesktopModeTransitionSource
     ) {
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: moveToFullscreen with animation taskId=%d",
-            task.taskId
-        )
+        logV("moveToFullscreenWithAnimation taskId=%d", task.taskId)
         val wct = WindowContainerTransaction()
         addMoveToFullscreenChanges(wct, task)
 
@@ -541,12 +502,7 @@
 
     /** Move a task to the front */
     fun moveTaskToFront(taskInfo: RunningTaskInfo) {
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: moveTaskToFront taskId=%d",
-            taskInfo.taskId
-        )
-
+        logV("moveTaskToFront taskId=%s", taskInfo.taskId)
         val wct = WindowContainerTransaction()
         wct.reorder(taskInfo.token, true)
         val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo)
@@ -572,15 +528,10 @@
     fun moveToNextDisplay(taskId: Int) {
         val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
         if (task == null) {
-            ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId)
+            logW("moveToNextDisplay: taskId=%d not found", taskId)
             return
         }
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "moveToNextDisplay: taskId=%d taskDisplayId=%d",
-            taskId,
-            task.displayId
-        )
+        logV("moveToNextDisplay: taskId=%d displayId=%d", taskId, task.displayId)
 
         val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted()
         // Get the first display id that is higher than current task display id
@@ -590,7 +541,7 @@
             newDisplayId = displayIds.firstOrNull { displayId -> displayId < task.displayId }
         }
         if (newDisplayId == null) {
-            ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: next display not found")
+            logW("moveToNextDisplay: next display not found")
             return
         }
         moveToDisplay(task, newDisplayId)
@@ -602,21 +553,15 @@
      * No-op if task is already on that display per [RunningTaskInfo.displayId].
      */
     private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) {
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "moveToDisplay: taskId=%d displayId=%d",
-            task.taskId,
-            displayId
-        )
-
+        logV("moveToDisplay: taskId=%d displayId=%d", task.taskId, displayId)
         if (task.displayId == displayId) {
-            ProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display")
+            logD("moveToDisplay: task already on display %d", displayId)
             return
         }
 
         val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
         if (displayAreaInfo == null) {
-            ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToDisplay: display not found")
+            logW("moveToDisplay: display not found")
             return
         }
 
@@ -653,7 +598,7 @@
             // If the task's pre-maximize stable bounds were saved, toggle the task to those bounds.
             // Otherwise, toggle to the default bounds.
             val taskBoundsBeforeMaximize =
-                desktopModeTaskRepository.removeBoundsBeforeMaximize(taskInfo.taskId)
+                taskRepository.removeBoundsBeforeMaximize(taskInfo.taskId)
             if (taskBoundsBeforeMaximize != null) {
                 destinationBounds.set(taskBoundsBeforeMaximize)
             } else {
@@ -666,7 +611,7 @@
         } else {
             // Save current bounds so that task can be restored back to original bounds if necessary
             // and toggle to the stable bounds.
-            desktopModeTaskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
+            taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
 
             if (taskInfo.isResizeable) {
                 // if resizable then expand to entire stable bounds (full display minus insets)
@@ -771,12 +716,7 @@
         wct: WindowContainerTransaction,
         newTaskIdInFront: Int? = null
     ): RunningTaskInfo? {
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s",
-            newTaskIdInFront ?: "null"
-        )
-
+        logV("bringDesktopAppsToFront, newTaskId=%d", newTaskIdInFront)
         // Move home to front, ensures that we go back home when all desktop windows are closed
         moveHomeTask(wct, toTop = true)
 
@@ -787,7 +727,7 @@
         }
 
         val nonMinimizedTasksOrderedFrontToBack =
-            desktopModeTaskRepository.getActiveNonMinimizedOrderedTasks(displayId)
+            taskRepository.getActiveNonMinimizedOrderedTasks(displayId)
         // If we're adding a new Task we might need to minimize an old one
         val taskToMinimize: RunningTaskInfo? =
             if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) {
@@ -817,7 +757,7 @@
     }
 
     private fun addWallpaperActivity(wct: WindowContainerTransaction) {
-        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: addWallpaper")
+        logV("addWallpaperActivity")
         val intent = Intent(context, DesktopWallpaperActivity::class.java)
         val options =
             ActivityOptions.makeBasic().apply {
@@ -835,8 +775,8 @@
     }
 
     private fun removeWallpaperActivity(wct: WindowContainerTransaction) {
-        desktopModeTaskRepository.wallpaperActivityToken?.let { token ->
-            ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: removeWallpaper")
+        taskRepository.wallpaperActivityToken?.let { token ->
+            logV("removeWallpaperActivity")
             wct.removeTask(token)
         }
     }
@@ -874,11 +814,7 @@
         transition: IBinder,
         request: TransitionRequestInfo
     ): WindowContainerTransaction? {
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: handleRequest request=%s",
-            request
-        )
+        logV("handleRequest request=%s", request)
         // Check if we should skip handling this transition
         var reason = ""
         val triggerTask = request.triggerTask
@@ -916,11 +852,7 @@
             }
 
         if (!shouldHandleRequest) {
-            ProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: skipping handleRequest reason=%s",
-                reason
-            )
+            logV("skipping handleRequest reason=%s", reason)
             return null
         }
 
@@ -940,11 +872,7 @@
                     }
                 }
             }
-        ProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: handleRequest result=%s",
-            result ?: "null"
-        )
+        logV("handleRequest result=%s", result)
         return result
     }
 
@@ -978,25 +906,20 @@
         task: RunningTaskInfo,
         transition: IBinder
     ): WindowContainerTransaction? {
-        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch")
+        logV("handleFreeformTaskLaunch")
         if (keyguardManager.isKeyguardLocked) {
             // Do NOT handle freeform task launch when locked.
             // It will be launched in fullscreen windowing mode (Details: b/160925539)
-            ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: skip keyguard is locked")
+            logV("skip keyguard is locked")
             return null
         }
         val wct = WindowContainerTransaction()
         if (!isDesktopModeShowing(task.displayId)) {
-            ProtoLog.d(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: bring desktop tasks to front on transition" +
-                    " taskId=%d",
-                task.taskId
-            )
+            logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId)
             // We are outside of desktop mode and already existing desktop task is being launched.
             // We should make this task go to fullscreen instead of freeform. Note that this means
             // any re-launch of a freeform window outside of desktop will be in fullscreen.
-            if (desktopModeTaskRepository.isActiveTask(task.taskId)) {
+            if (taskRepository.isActiveTask(task.taskId)) {
                 addMoveToFullscreenChanges(wct, task)
                 return wct
             }
@@ -1021,14 +944,9 @@
         task: RunningTaskInfo,
         transition: IBinder
     ): WindowContainerTransaction? {
-        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch")
+        logV("handleFullscreenTaskLaunch")
         if (isDesktopModeShowing(task.displayId)) {
-            ProtoLog.d(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: switch fullscreen task to freeform on transition" +
-                    " taskId=%d",
-                task.taskId
-            )
+            logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId)
             return WindowContainerTransaction().also { wct ->
                 addMoveToDesktopChanges(wct, task)
                 // In some launches home task is moved behind new task being launched. Make sure
@@ -1055,26 +973,28 @@
 
     /** Handle task closing by removing wallpaper activity if it's the last active task */
     private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
-        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleTaskClosing")
+        logV("handleTaskClosing")
         val wct = WindowContainerTransaction()
-        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(task.taskId)
-            && desktopModeTaskRepository.wallpaperActivityToken != null) {
+        if (taskRepository.isOnlyVisibleNonClosingTask(task.taskId)
+            && taskRepository.wallpaperActivityToken != null) {
             // Remove wallpaper activity when the last active task is removed
             removeWallpaperActivity(wct)
         }
-        desktopModeTaskRepository.addClosingTask(task.displayId, task.taskId)
+        taskRepository.addClosingTask(task.displayId, task.taskId)
         // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task.
         if (Flags.enableDesktopWindowingBackNavigation() &&
-            desktopModeTaskRepository.isVisibleTask(task.taskId)) {
+            taskRepository.isVisibleTask(task.taskId)) {
             wct.removeTask(task.token)
         }
         return if (wct.isEmpty) null else wct
     }
 
-    private fun addMoveToDesktopChanges(
+    @VisibleForTesting
+    fun addMoveToDesktopChanges(
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo
     ) {
+        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
         val targetWindowingMode =
@@ -1084,6 +1004,28 @@
             } else {
                 WINDOWING_MODE_FREEFORM
             }
+        val initialBounds = if (DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(context)) {
+            calculateInitialBounds(displayLayout, taskInfo)
+        } else {
+            getDefaultDesktopTaskBounds(displayLayout)
+        }
+
+        if (DesktopModeFlags.CASCADING_WINDOWS.isEnabled(context)) {
+            val stableBounds = Rect()
+            displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+            val activeTasks = taskRepository
+                .getActiveNonMinimizedOrderedTasks(taskInfo.displayId)
+            activeTasks.firstOrNull()?.let { activeTask ->
+                shellTaskOrganizer.getRunningTaskInfo(activeTask)?.let {
+                    cascadeWindow(context.resources, stableBounds,
+                        it.configuration.windowConfiguration.bounds, initialBounds)
+                }
+            }
+        }
+        if (canChangeTaskPosition(taskInfo)) {
+            wct.setBounds(taskInfo.token, initialBounds)
+        }
         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
         wct.reorder(taskInfo.token, true /* onTop */)
         if (useDesktopOverrideDensity()) {
@@ -1109,7 +1051,7 @@
         if (useDesktopOverrideDensity()) {
             wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
         }
-        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) {
+        if (taskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) {
             // Remove wallpaper activity when leaving desktop mode
             removeWallpaperActivity(wct)
         }
@@ -1128,7 +1070,7 @@
         // The task's density may have been overridden in freeform; revert it here as we don't
         // want it overridden in multi-window.
         wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
-        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) {
+        if (taskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) {
             // Remove wallpaper activity when leaving desktop mode
             removeWallpaperActivity(wct)
         }
@@ -1343,16 +1285,10 @@
         val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
         when (indicatorType) {
             IndicatorType.TO_DESKTOP_INDICATOR -> {
-                val displayLayout = displayController.getDisplayLayout(taskInfo.displayId)
-                    ?: return IndicatorType.NO_INDICATOR
                 // Start a new jank interaction for the drag release to desktop window animation.
                 interactionJankMonitor.begin(taskSurface, context,
                     CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, "to_desktop")
-                if (DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(context)) {
-                    finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
-                } else {
-                    finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
-                }
+                finalizeDragToDesktop(taskInfo)
             }
             IndicatorType.NO_INDICATOR,
             IndicatorType.TO_FULLSCREEN_INDICATOR -> {
@@ -1370,12 +1306,12 @@
 
     /** Update the exclusion region for a specified task */
     fun onExclusionRegionChanged(taskId: Int, exclusionRegion: Region) {
-        desktopModeTaskRepository.updateTaskExclusionRegions(taskId, exclusionRegion)
+        taskRepository.updateTaskExclusionRegions(taskId, exclusionRegion)
     }
 
     /** Remove a previously tracked exclusion region for a specified task. */
     fun removeExclusionRegionForTask(taskId: Int) {
-        desktopModeTaskRepository.removeExclusionRegion(taskId)
+        taskRepository.removeExclusionRegion(taskId)
     }
 
     /**
@@ -1385,7 +1321,7 @@
      * @param callbackExecutor the executor to call the listener on.
      */
     fun addVisibleTasksListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
-        desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor)
+        taskRepository.addVisibleTasksListener(listener, callbackExecutor)
     }
 
     /**
@@ -1395,7 +1331,7 @@
      * @param callbackExecutor the executor to call the listener on.
      */
     fun setTaskRegionListener(listener: Consumer<Region>, callbackExecutor: Executor) {
-        desktopModeTaskRepository.setExclusionRegionListener(listener, callbackExecutor)
+        taskRepository.setExclusionRegionListener(listener, callbackExecutor)
     }
 
     override fun onUnhandledDrag(
@@ -1413,7 +1349,7 @@
         if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent)) {
             // TODO(b/320797628): Should only return early if there is an existing running task, and
             //                    notify the user as well. But for now, just ignore the drop.
-            ProtoLog.v(WM_SHELL_DESKTOP_MODE, "Dropped intent does not support multi-instance")
+            logV("Dropped intent does not support multi-instance")
             return false
         }
 
@@ -1445,7 +1381,7 @@
     private fun dump(pw: PrintWriter, prefix: String) {
         val innerPrefix = "$prefix  "
         pw.println("${prefix}DesktopTasksController")
-        desktopModeTaskRepository.dump(pw, innerPrefix)
+        taskRepository.dump(pw, innerPrefix)
     }
 
     /** The interface for calls from outside the shell, within the host process. */
@@ -1520,12 +1456,12 @@
                 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>(
                     controller,
                     { c ->
-                        c.desktopModeTaskRepository.addVisibleTasksListener(
+                        c.taskRepository.addVisibleTasksListener(
                             listener,
                             c.mainExecutor
                         )
                     },
-                    { c -> c.desktopModeTaskRepository.removeVisibleTasksListener(listener) }
+                    { c -> c.taskRepository.removeVisibleTasksListener(listener) }
                 )
         }
 
@@ -1552,10 +1488,7 @@
         }
 
         override fun hideStashedDesktopApps(displayId: Int) {
-            ProtoLog.w(
-                WM_SHELL_DESKTOP_MODE,
-                "IDesktopModeImpl: hideStashedDesktopApps is deprecated"
-            )
+            ProtoLog.w(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: hideStashedDesktopApps is deprecated")
         }
 
         override fun getVisibleTaskCount(displayId: Int): Int {
@@ -1579,11 +1512,7 @@
         }
 
         override fun setTaskListener(listener: IDesktopTaskListener?) {
-            ProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "IDesktopModeImpl: set task listener=%s",
-                listener ?: "null"
-            )
+            ProtoLog.v(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: set task listener=%s", listener)
             executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ ->
                 listener?.let { remoteListener.register(it) } ?: remoteListener.unregister()
             }
@@ -1596,10 +1525,22 @@
         }
     }
 
+    private fun logV(msg: String, vararg arguments: Any?) {
+        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+    private fun logD(msg: String, vararg arguments: Any?) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+    private fun logW(msg: String, vararg arguments: Any?) {
+        ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+
     companion object {
         @JvmField
         val DESKTOP_MODE_INITIAL_BOUNDS_SCALE =
             SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
+
+        private const val TAG = "DesktopTasksController"
     }
 
     /** The positions on a screen that a task can snap to. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index a011ff5..f41d6e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -33,7 +33,8 @@
  * Limits the number of tasks shown in Desktop Mode.
  *
  * This class should only be used if
- * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true.
+ * [com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT]
+ * is enabled and [maxTasksLimit] is strictly greater than 0.
  */
 class DesktopTasksLimiter (
         transitions: Transitions,
@@ -52,6 +53,8 @@
         }
         transitions.registerObserver(minimizeTransitionObserver)
         taskRepository.addActiveTaskListener(leftoverMinimizedTasksRemover)
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksLimiter: starting limiter with a maximum of %d tasks", maxTasksLimit)
     }
 
     private data class TaskDetails (val displayId: Int, val taskId: Int)
@@ -86,10 +89,10 @@
         }
 
         /**
-         * Returns whether the given Task is being reordered to the back in the given transition, or
-         * is already invisible.
+         * Returns whether the Task [taskDetails] is being reordered to the back in the transition
+         * [info], or is already invisible.
          *
-         * <p> This check can be used to double-check that a task was indeed minimized before
+         * This check can be used to double-check that a task was indeed minimized before
          * marking it as such.
          */
         private fun isTaskReorderedToBackOrInvisible(
@@ -138,7 +141,9 @@
             }
             ProtoLog.v(
                 ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksLimiter: removing leftover minimized tasks: $remainingMinimizedTasks")
+                "DesktopTasksLimiter: removing leftover minimized tasks: %s",
+                remainingMinimizedTasks,
+            )
             remainingMinimizedTasks.forEach { taskIdToRemove ->
                 val taskToRemove = shellTaskOrganizer.getRunningTaskInfo(taskIdToRemove)
                 if (taskToRemove != null) {
@@ -149,8 +154,8 @@
     }
 
     /**
-     * Mark a task as minimized, this should only be done after the corresponding transition has
-     * finished so we don't minimize the task if the transition fails.
+     * Mark [taskId], which must be on [displayId], as minimized, this should only be done after the
+     * corresponding transition has finished so we don't minimize the task if the transition fails.
      */
     private fun markTaskMinimized(displayId: Int, taskId: Int) {
         ProtoLog.v(
@@ -161,11 +166,9 @@
 
     /**
      * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task
-     * limit.
+     * limit, returning the task to minimize.
      *
-     * @param transition the transition that the minimize-transition will be appended to, or null if
-     * the transition will be started later.
-     * @return the ID of the minimized task, or null if no task is being minimized.
+     * The task must be on [displayId].
      */
     fun addAndGetMinimizeTaskChangesIfNeeded(
             displayId: Int,
@@ -220,13 +223,15 @@
             // No need to minimize anything
             return null
         }
+        val taskIdToMinimize = visibleFreeformTaskIdsOrderedFrontToBack.last()
         val taskToMinimize =
-                shellTaskOrganizer.getRunningTaskInfo(
-                        visibleFreeformTaskIdsOrderedFrontToBack.last())
+                shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize)
         if (taskToMinimize == null) {
             ProtoLog.e(
                     ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
-                    "DesktopTasksLimiter: taskToMinimize == null")
+                    "DesktopTasksLimiter: taskToMinimize(taskId = %d) == null",
+                    taskIdToMinimize,
+                )
             return null
         }
         return taskToMinimize
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 229d972..640f872 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -99,11 +99,10 @@
         if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
                 repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId);
-                repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
                 if (taskInfo.isVisible) {
                     repository.addActiveTask(taskInfo.displayId, taskInfo.taskId);
-                    repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId,
-                        true);
+                    repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId,
+                        /* visible= */ true);
                 }
             });
         }
@@ -120,7 +119,8 @@
                 repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
                 repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
                 repository.removeActiveTask(taskInfo.taskId, /* excludedDisplayId= */ null);
-                repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, false);
+                repository.updateTaskVisibility(
+                    taskInfo.displayId, taskInfo.taskId, /* visible= */ false);
             });
         }
         mWindowDecorationViewModel.onTaskVanished(taskInfo);
@@ -144,7 +144,7 @@
                 } else if (repository.isClosingTask(taskInfo.taskId)) {
                     repository.removeClosingTask(taskInfo.taskId);
                 }
-                repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId,
+                repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId,
                         taskInfo.isVisible);
             });
         }
@@ -161,7 +161,6 @@
         if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
                 repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId);
-                repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
             });
         }
     }
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 7451d22..284620e 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
@@ -272,6 +272,7 @@
         final boolean changed = onDisplayRotationChanged(mContext, outBounds, currentBounds,
                 mTmpInsetBounds, displayId, fromRotation, toRotation, t);
         if (changed) {
+            mMenuController.hideMenu();
             // If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the
             // movement bounds
             mTouchHandler.adjustBoundsForRotation(outBounds, mPipBoundsState.getBounds(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 8aa0933..94fe286 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -88,6 +88,7 @@
     private final TaskStackListenerImpl mTaskStackListener;
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final PipTransitionState mPipTransitionState;
+    private final PipTouchHandler mPipTouchHandler;
     private final ShellExecutor mMainExecutor;
     private final PipImpl mImpl;
     private Consumer<Boolean> mOnIsInPipStateChangedListener;
@@ -130,6 +131,7 @@
             TaskStackListenerImpl taskStackListener,
             ShellTaskOrganizer shellTaskOrganizer,
             PipTransitionState pipTransitionState,
+            PipTouchHandler pipTouchHandler,
             ShellExecutor mainExecutor) {
         mContext = context;
         mShellCommandHandler = shellCommandHandler;
@@ -144,6 +146,7 @@
         mShellTaskOrganizer = shellTaskOrganizer;
         mPipTransitionState = pipTransitionState;
         mPipTransitionState.addPipTransitionStateChangedListener(this);
+        mPipTouchHandler = pipTouchHandler;
         mMainExecutor = mainExecutor;
         mImpl = new PipImpl();
 
@@ -168,6 +171,7 @@
             TaskStackListenerImpl taskStackListener,
             ShellTaskOrganizer shellTaskOrganizer,
             PipTransitionState pipTransitionState,
+            PipTouchHandler pipTouchHandler,
             ShellExecutor mainExecutor) {
         if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -177,7 +181,7 @@
         return new PipController(context, shellInit, shellCommandHandler, shellController,
                 displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm,
                 pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer,
-                pipTransitionState, mainExecutor);
+                pipTransitionState, pipTouchHandler, mainExecutor);
     }
 
     public PipImpl getPipImpl() {
@@ -204,7 +208,9 @@
         mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(),
                 new ImeListener(mDisplayController, mPipDisplayLayoutState.getDisplayId()) {
                     @Override
-                    public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {}
+                    public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
+                        mPipTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight);
+                    }
                 });
 
         // Allow other outside processes to bind to PiP controller using the key below.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
index e1e072a..83253c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
@@ -134,6 +134,8 @@
     private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig =
             new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY);
 
+    @Nullable private Runnable mUpdateMovementBoundsRunnable;
+
     private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> {
         if (mPipBoundsState.getBounds().equals(newBounds)) {
             return;
@@ -141,6 +143,7 @@
 
         mMenuController.updateMenuLayout(newBounds);
         mPipBoundsState.setBounds(newBounds);
+        maybeUpdateMovementBounds();
     };
 
     /**
@@ -566,11 +569,20 @@
                             + " callers=\n%s", TAG, originalBounds, offset,
                     Debug.getCallers(5, "    "));
         }
+        if (offset == 0) {
+            return;
+        }
+
         cancelPhysicsAnimation();
-        /*
-        mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION,
-                mUpdateBoundsCallback);
-         */
+
+        Rect adjustedBounds = new Rect(originalBounds);
+        adjustedBounds.offset(0, offset);
+
+        setAnimatingToBounds(adjustedBounds);
+        Bundle extra = new Bundle();
+        extra.putBoolean(ANIMATING_BOUNDS_CHANGE, true);
+        extra.putInt(ANIMATING_BOUNDS_CHANGE_DURATION, SHIFT_DURATION);
+        mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra);
     }
 
     /**
@@ -585,11 +597,11 @@
     /** Set new fling configs whose min/max values respect the given movement bounds. */
     private void rebuildFlingConfigs() {
         mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION,
-                mPipBoundsAlgorithm.getMovementBounds(getBounds()).left,
-                mPipBoundsAlgorithm.getMovementBounds(getBounds()).right);
+                mPipBoundsState.getMovementBounds().left,
+                mPipBoundsState.getMovementBounds().right);
         mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION,
-                mPipBoundsAlgorithm.getMovementBounds(getBounds()).top,
-                mPipBoundsAlgorithm.getMovementBounds(getBounds()).bottom);
+                mPipBoundsState.getMovementBounds().top,
+                mPipBoundsState.getMovementBounds().bottom);
         final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets();
         mStashConfigX = new PhysicsAnimator.FlingConfig(
                 DEFAULT_FRICTION,
@@ -671,6 +683,16 @@
         cleanUpHighPerfSessionMaybe();
     }
 
+    void setUpdateMovementBoundsRunnable(Runnable updateMovementBoundsRunnable) {
+        mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
+    }
+
+    private void maybeUpdateMovementBounds() {
+        if (mUpdateMovementBoundsRunnable != null)  {
+            mUpdateMovementBoundsRunnable.run();
+        }
+    }
+
     /**
      * Notifies the floating coordinator that we're moving, and sets the animating to bounds so
      * we return these bounds from
@@ -807,8 +829,14 @@
                 startTx, finishTx, mPipBoundsState.getBounds(), mPipBoundsState.getBounds(),
                 destinationBounds, duration, 0f /* angle */);
         animator.setAnimationEndCallback(() -> {
-            mPipBoundsState.setBounds(destinationBounds);
-            // All motion operations have actually finished, so make bounds cache updates.
+            mUpdateBoundsCallback.accept(destinationBounds);
+
+            // In case an ongoing drag/fling was present before a deterministic resize transition
+            // kicked in, we need to update the update bounds properly before cleaning in-motion
+            // state.
+            mPipBoundsState.getMotionBoundsState().setBoundsInMotion(destinationBounds);
+            settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */);
+
             cleanUpHighPerfSessionMaybe();
             // Signal that we are done with resize transition
             mPipScheduler.scheduleFinishResizePip(true /* configAtEnd */);
@@ -817,7 +845,7 @@
     }
 
     private void settlePipBoundsAfterPhysicsAnimation(boolean animatingAfter) {
-        if (!animatingAfter) {
+        if (!animatingAfter && mPipBoundsState.getMotionBoundsState().isInMotion()) {
             // The physics animation ended, though we may not necessarily be done animating, such as
             // when we're still dragging after moving out of the magnetic target. Only set the final
             // bounds state and clear motion bounds completely if the whole animation is over.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
index 5b0ca18..d28204a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
@@ -146,8 +146,8 @@
         mUpdateResizeBoundsCallback = (rect) -> {
             mUserResizeBounds.set(rect);
             // mMotionHelper.synchronizePinnedStackBounds();
-            mUpdateMovementBoundsRunnable.run();
             mPipBoundsState.setBounds(rect);
+            mUpdateMovementBoundsRunnable.run();
             resetState();
         };
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
index 53b80e8..f387e72 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
@@ -199,6 +199,7 @@
         mMenuController.addListener(new PipMenuListener());
         mGesture = new DefaultPipTouchGesture();
         mMotionHelper = pipMotionHelper;
+        mMotionHelper.setUpdateMovementBoundsRunnable(this::updateMovementBounds);
         mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger,
                 mMotionHelper, mainExecutor);
         mTouchState = new PipTouchState(ViewConfiguration.get(context),
@@ -317,6 +318,8 @@
         mFloatingContentCoordinator.onContentRemoved(mMotionHelper);
         mPipResizeGestureHandler.onActivityUnpinned();
         mPipInputConsumer.unregisterInputConsumer();
+        mPipBoundsState.setHasUserMovedPip(false);
+        mPipBoundsState.setHasUserResizedPip(false);
     }
 
     void onPinnedStackAnimationEnded(
@@ -346,6 +349,22 @@
     void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
         mIsImeShowing = imeVisible;
         mImeHeight = imeHeight;
+
+        // Cache new movement bounds using the new potential IME height.
+        updateMovementBounds();
+
+        mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> {
+            int delta = mPipBoundsState.getMovementBounds().bottom
+                    - mPipBoundsState.getBounds().top;
+
+            boolean hasUserInteracted = (mPipBoundsState.hasUserMovedPip()
+                    || mPipBoundsState.hasUserResizedPip());
+            if ((imeVisible && delta < 0) || (!imeVisible && !hasUserInteracted)) {
+                // The policy is to ignore an IME disappearing if user has interacted with PiP.
+                // Otherwise, only offset due to an appearing IME if PiP occludes it.
+                mMotionHelper.animateToOffset(mPipBoundsState.getBounds(), delta);
+            }
+        });
     }
 
     void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {
@@ -1077,6 +1096,7 @@
         switch (newState) {
             case PipTransitionState.ENTERED_PIP:
                 onActivityPinned();
+                updateMovementBounds();
                 mTouchState.setAllowInputEvents(true);
                 mTouchState.reset();
                 break;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
index 29272be..a132796f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
@@ -149,6 +149,12 @@
     @Nullable
     private SurfaceControl mSwipePipToHomeOverlay;
 
+    //
+    // Scheduling-related state
+    //
+    @Nullable
+    private Runnable mOnIdlePipTransitionStateRunnable;
+
     /**
      * An interface to track state updates as we progress through PiP transitions.
      */
@@ -197,6 +203,8 @@
             mState = state;
             dispatchPipTransitionStateChanged(prevState, mState, extra);
         }
+
+        maybeRunOnIdlePipTransitionStateCallback();
     }
 
     /**
@@ -231,6 +239,29 @@
     }
 
     /**
+     * Schedule a callback to run when in a valid idle PiP state.
+     *
+     * <p>We only allow for one callback to be scheduled to avoid cases with multiple transitions
+     * being scheduled. For instance, if user double taps and IME shows, this would
+     * schedule a bounds change transition for IME appearing. But if some other transition would
+     * want to animate PiP before the scheduled callback executes, we would rather want to replace
+     * the existing callback with a new one, to avoid multiple animations
+     * as soon as we are idle.</p>
+     */
+    public void setOnIdlePipTransitionStateRunnable(
+            @Nullable Runnable onIdlePipTransitionStateRunnable) {
+        mOnIdlePipTransitionStateRunnable = onIdlePipTransitionStateRunnable;
+        maybeRunOnIdlePipTransitionStateCallback();
+    }
+
+    private void maybeRunOnIdlePipTransitionStateCallback() {
+        if (mOnIdlePipTransitionStateRunnable != null && isPipStateIdle()) {
+            mOnIdlePipTransitionStateRunnable.run();
+            mOnIdlePipTransitionStateRunnable = null;
+        }
+    }
+
+    /**
      * Adds a {@link PipTransitionStateChangedListener} for future PiP transition state updates.
      */
     public void addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener) {
@@ -318,6 +349,11 @@
         throw new IllegalStateException("Unknown state: " + state);
     }
 
+    public boolean isPipStateIdle() {
+        // This needs to be a valid in-PiP state that isn't a transient state.
+        return mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS;
+    }
+
     @Override
     public String toString() {
         return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
index e6d1b45..15fe7ab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
@@ -217,6 +217,11 @@
         return null;
     }
 
+    /** Returns true if the given {@code taskInfo} belongs to a task view. */
+    public boolean isTaskViewTask(ActivityManager.RunningTaskInfo taskInfo) {
+        return findTaskView(taskInfo) != null;
+    }
+
     void startTaskView(@NonNull WindowContainerTransaction wct,
             @NonNull TaskViewTaskController taskView, @NonNull IBinder launchCookie) {
         updateVisibilityState(taskView, true /* visible */);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index e91828b..716a148 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -16,10 +16,6 @@
 
 package com.android.wm.shell;
 
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
@@ -443,84 +439,6 @@
     }
 
     @Test
-    public void testOnCameraCompatActivityChanged() {
-        final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1,
-                WINDOWING_MODE_FULLSCREEN);
-        taskInfo1.displayId = DEFAULT_DISPLAY;
-        taskInfo1.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                CAMERA_COMPAT_CONTROL_HIDDEN;
-        final TrackingTaskListener taskListener = new TrackingTaskListener();
-        mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN);
-        mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
-
-        // Task listener sent to compat UI is null if top activity doesn't request a camera
-        // compat control.
-        verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */);
-
-        // Task listener is non-null when request a camera compat control for a visible task.
-        clearInvocations(mCompatUI);
-        final RunningTaskInfo taskInfo2 =
-                createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
-        taskInfo2.displayId = taskInfo1.displayId;
-        taskInfo2.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        taskInfo2.isVisible = true;
-        mOrganizer.onTaskInfoChanged(taskInfo2);
-        verifyOnCompatInfoChangedInvokedWith(taskInfo2, taskListener);
-
-        // CompatUIController#onCompatInfoChanged is called when requested state for a camera
-        // compat control changes for a visible task.
-        clearInvocations(mCompatUI);
-        final RunningTaskInfo taskInfo3 =
-                createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
-        taskInfo3.displayId = taskInfo1.displayId;
-        taskInfo3.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-        taskInfo3.isVisible = true;
-        mOrganizer.onTaskInfoChanged(taskInfo3);
-        verifyOnCompatInfoChangedInvokedWith(taskInfo3, taskListener);
-
-        // CompatUIController#onCompatInfoChanged is called when a top activity goes in size compat
-        // mode for a visible task that has a compat control.
-        clearInvocations(mCompatUI);
-        final RunningTaskInfo taskInfo4 =
-                createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
-        taskInfo4.displayId = taskInfo1.displayId;
-        taskInfo4.appCompatTaskInfo.topActivityInSizeCompat = true;
-        taskInfo4.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-        taskInfo4.isVisible = true;
-        mOrganizer.onTaskInfoChanged(taskInfo4);
-        verifyOnCompatInfoChangedInvokedWith(taskInfo4, taskListener);
-
-        // Task linster is null when a camera compat control is dimissed for a visible task.
-        clearInvocations(mCompatUI);
-        final RunningTaskInfo taskInfo5 =
-                createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
-        taskInfo5.displayId = taskInfo1.displayId;
-        taskInfo5.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                CAMERA_COMPAT_CONTROL_DISMISSED;
-        taskInfo5.isVisible = true;
-        mOrganizer.onTaskInfoChanged(taskInfo5);
-        verifyOnCompatInfoChangedInvokedWith(taskInfo5, null /* taskListener */);
-
-        // Task linster is null when request a camera compat control for a invisible task.
-        clearInvocations(mCompatUI);
-        final RunningTaskInfo taskInfo6 =
-                createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
-        taskInfo6.displayId = taskInfo1.displayId;
-        taskInfo6.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        taskInfo6.isVisible = false;
-        mOrganizer.onTaskInfoChanged(taskInfo6);
-        verifyOnCompatInfoChangedInvokedWith(taskInfo6, null /* taskListener */);
-
-        clearInvocations(mCompatUI);
-        mOrganizer.onTaskVanished(taskInfo1);
-        verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */);
-    }
-
-    @Test
     public void testAddLocusListener() {
         RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         task1.isVisible = true;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index 2c0aa12..764d5a9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -83,7 +83,7 @@
             }
         }, mExecutor) {
             @Override
-            void removeImeSurface() { }
+            void removeImeSurface(int displayId) { }
         }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
     }
 
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 fc7a777..77e22cd 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
@@ -16,8 +16,6 @@
 
 package com.android.wm.shell.compatui;
 
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
 import static android.view.WindowInsets.Type.navigationBars;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -34,7 +32,6 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager.RunningTaskInfo;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.TaskInfo;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -199,8 +196,7 @@
     @Test
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnCompatInfoChanged() {
-        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_HIDDEN);
+        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
 
         // Verify that the compat controls are added with non-null task listener.
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
@@ -214,8 +210,7 @@
         // Verify that the compat controls and letterbox education are updated with new size compat
         // info.
         clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
-        taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
+        taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
@@ -228,7 +223,7 @@
         // Verify that compat controls and letterbox education are removed with null task listener.
         clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
         mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN),
+                /* hasSizeCompat= */ true),
                 /* taskListener= */ null));
 
         verify(mMockCompatLayout).release();
@@ -243,8 +238,7 @@
         doReturn(false).when(mMockLetterboxEduLayout).createLayout(anyBoolean());
         doReturn(false).when(mMockRestartDialogLayout).createLayout(anyBoolean());
 
-        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_HIDDEN);
+        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
@@ -274,8 +268,7 @@
         doReturn(false).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean());
         doReturn(false).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean());
 
-        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_HIDDEN);
+        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
@@ -326,7 +319,7 @@
     public void testOnDisplayRemoved() {
         mController.onDisplayAdded(DISPLAY_ID);
         mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
+                /* hasSizeCompat= */ true), mMockTaskListener));
 
         mController.onDisplayRemoved(DISPLAY_ID + 1);
 
@@ -348,7 +341,7 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnDisplayConfigurationChanged() {
         mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
+                /* hasSizeCompat= */ true), mMockTaskListener));
 
         mController.onDisplayConfigurationChanged(DISPLAY_ID + 1, new Configuration());
 
@@ -368,7 +361,7 @@
     public void testInsetsChanged() {
         mController.onDisplayAdded(DISPLAY_ID);
         mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
+                /* hasSizeCompat= */ true), mMockTaskListener));
         InsetsState insetsState = new InsetsState();
         InsetsSource insetsSource = new InsetsSource(
                 InsetsSource.createId(null, 0, navigationBars()), navigationBars());
@@ -395,7 +388,7 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testChangeLayoutsVisibilityOnImeShowHide() {
         mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
+                /* hasSizeCompat= */ true), mMockTaskListener));
 
         // Verify that the restart button is hidden after IME is showing.
         mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true);
@@ -405,8 +398,7 @@
         verify(mMockRestartDialogLayout).updateVisibility(false);
 
         // Verify button remains hidden while IME is showing.
-        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_HIDDEN);
+        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
@@ -428,7 +420,7 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testChangeLayoutsVisibilityOnKeyguardShowingChanged() {
         mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
+                /* hasSizeCompat= */ true), mMockTaskListener));
 
         // Verify that the restart button is hidden after keyguard becomes showing.
         mController.onKeyguardVisibilityChanged(true, false, false);
@@ -438,8 +430,7 @@
         verify(mMockRestartDialogLayout).updateVisibility(false);
 
         // Verify button remains hidden while keyguard is showing.
-        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_HIDDEN);
+        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
@@ -461,7 +452,7 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testLayoutsRemainHiddenOnKeyguardShowingFalseWhenImeIsShowing() {
         mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
+                /* hasSizeCompat= */ true), mMockTaskListener));
 
         mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true);
         mController.onKeyguardVisibilityChanged(true, false, false);
@@ -491,7 +482,7 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testLayoutsRemainHiddenOnImeHideWhenKeyguardIsShowing() {
         mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
+                /* hasSizeCompat= */ true), mMockTaskListener));
 
         mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true);
         mController.onKeyguardVisibilityChanged(true, false, false);
@@ -520,8 +511,7 @@
     @Test
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRestartLayoutRecreatedIfNeeded() {
-        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN);
+        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         doReturn(true).when(mMockRestartDialogLayout)
                 .needsToBeRecreated(any(TaskInfo.class),
                         any(ShellTaskOrganizer.TaskListener.class));
@@ -536,8 +526,7 @@
     @Test
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRestartLayoutNotRecreatedIfNotNeeded() {
-        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN);
+        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         doReturn(false).when(mMockRestartDialogLayout)
                 .needsToBeRecreated(any(TaskInfo.class),
                         any(ShellTaskOrganizer.TaskListener.class));
@@ -557,9 +546,8 @@
         Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton());
 
         // Create new task
-        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true,
-                /* isFocused */ true);
+        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
+                /* isVisible */ true, /* isFocused */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo);
@@ -574,9 +562,8 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateActiveTaskInfo_newTask_notVisibleOrFocused_notUpdated() {
         // Create new task
-        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true,
-                /* isFocused */ true);
+        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
+                /* isVisible */ true, /* isFocused */ true);
 
         // Simulate task being shown
         mController.updateActiveTaskInfo(taskInfo);
@@ -593,9 +580,8 @@
         final int newTaskId = TASK_ID + 1;
 
         // Create visible but NOT focused task
-        final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true,
-                /* isFocused */ false);
+        final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
+                /* isVisible */ true, /* isFocused */ false);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo1);
@@ -606,9 +592,8 @@
         Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton());
 
         // Create focused but NOT visible task
-        final TaskInfo taskInfo2 = createTaskInfo(DISPLAY_ID, newTaskId,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ false,
-                /* isFocused */ true);
+        final TaskInfo taskInfo2 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
+                /* isVisible */ false, /* isFocused */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo2);
@@ -619,9 +604,8 @@
         Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton());
 
         // Create NOT focused but NOT visible task
-        final TaskInfo taskInfo3 = createTaskInfo(DISPLAY_ID, newTaskId,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ false,
-                /* isFocused */ false);
+        final TaskInfo taskInfo3 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
+                /* isVisible */ false, /* isFocused */ false);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo3);
@@ -636,9 +620,8 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateActiveTaskInfo_sameTask_notUpdated() {
         // Create new task
-        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true,
-                /* isFocused */ true);
+        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
+                /* isVisible */ true, /* isFocused */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo);
@@ -665,9 +648,8 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateActiveTaskInfo_transparentTask_notUpdated() {
         // Create new task
-        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true,
-                /* isFocused */ true);
+        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
+                /* isVisible */ true, /* isFocused */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo);
@@ -684,9 +666,8 @@
         final int newTaskId = TASK_ID + 1;
 
         // Create transparent task
-        final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true,
-                /* isFocused */ true, /* isTopActivityTransparent */ true);
+        final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
+                /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo1);
@@ -699,8 +680,7 @@
 
     @Test
     public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() {
-        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_HIDDEN);
+        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = false;
 
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
@@ -709,29 +689,23 @@
                 eq(mMockTaskListener));
     }
 
-    private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
-            @CameraCompatControlState int cameraCompatControlState) {
-        return createTaskInfo(displayId, taskId, hasSizeCompat, cameraCompatControlState,
-                /* isVisible */ false, /* isFocused */ false,
-                /* isTopActivityTransparent */ false);
+    private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) {
+        return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false,
+                /* isFocused */ false, /* isTopActivityTransparent */ false);
     }
 
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
-            @CameraCompatControlState int cameraCompatControlState, boolean isVisible,
-            boolean isFocused) {
-        return createTaskInfo(displayId, taskId, hasSizeCompat, cameraCompatControlState,
+            boolean isVisible, boolean isFocused) {
+        return createTaskInfo(displayId, taskId, hasSizeCompat,
                 isVisible, isFocused, /* isTopActivityTransparent */ false);
     }
 
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
-            @CameraCompatControlState int cameraCompatControlState, boolean isVisible,
-            boolean isFocused, boolean isTopActivityTransparent) {
+            boolean isVisible, boolean isFocused, boolean isTopActivityTransparent) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
         taskInfo.displayId = displayId;
         taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
-        taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                cameraCompatControlState;
         taskInfo.isVisible = isVisible;
         taskInfo.isFocused = isFocused;
         taskInfo.isTopActivityTransparent = isTopActivityTransparent;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index 33d69f5..3b93861 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -16,20 +16,13 @@
 
 package com.android.wm.shell.compatui;
 
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
-import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.TaskInfo;
 import android.graphics.Rect;
 import android.platform.test.annotations.RequiresFlagsDisabled;
@@ -52,7 +45,6 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
 import com.android.wm.shell.compatui.api.CompatUIEvent;
-import com.android.wm.shell.compatui.impl.CompatUIEvents;
 
 import junit.framework.Assert;
 
@@ -97,7 +89,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
-        mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+        mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false);
         mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
                 mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
                 mCompatUIConfiguration, mOnRestartButtonClicked);
@@ -151,113 +143,13 @@
         verify(mLayout).setSizeCompatHintVisibility(/* show= */ false);
     }
 
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testUpdateCameraTreatmentButton_treatmentAppliedByDefault() {
-        mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-        mWindowManager.createLayout(/* canShow= */ true);
-        final ImageButton button =
-                mLayout.findViewById(R.id.camera_compat_treatment_button);
-        button.performClick();
-
-        verify(mWindowManager).onCameraTreatmentButtonClicked();
-        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-
-        button.performClick();
-
-        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-    }
-
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testUpdateCameraTreatmentButton_treatmentSuggestedByDefault() {
-        mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        mWindowManager.createLayout(/* canShow= */ true);
-        final ImageButton button =
-                mLayout.findViewById(R.id.camera_compat_treatment_button);
-        button.performClick();
-
-        verify(mWindowManager).onCameraTreatmentButtonClicked();
-        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-
-        button.performClick();
-
-        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-    }
-
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testOnCameraDismissButtonClicked() {
-        mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        mWindowManager.createLayout(/* canShow= */ true);
-        final ImageButton button =
-                mLayout.findViewById(R.id.camera_compat_dismiss_button);
-        button.performClick();
-
-        verify(mWindowManager).onCameraDismissButtonClicked();
-        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED);
-        verify(mLayout).setCameraControlVisibility(/* show */ false);
-    }
-
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testOnLongClickForCameraTreatmentButton() {
-        doNothing().when(mWindowManager).onCameraButtonLongClicked();
-
-        final ImageButton button =
-                mLayout.findViewById(R.id.camera_compat_treatment_button);
-        button.performLongClick();
-
-        verify(mWindowManager).onCameraButtonLongClicked();
-    }
-
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testOnLongClickForCameraDismissButton() {
-        doNothing().when(mWindowManager).onCameraButtonLongClicked();
-
-        final ImageButton button = mLayout.findViewById(R.id.camera_compat_dismiss_button);
-        button.performLongClick();
-
-        verify(mWindowManager).onCameraButtonLongClicked();
-    }
-
-    @Test
-    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testOnClickForCameraCompatHint() {
-        mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        mWindowManager.createLayout(/* canShow= */ true);
-        final LinearLayout hint = mLayout.findViewById(R.id.camera_compat_hint);
-        hint.performClick();
-
-        verify(mLayout).setCameraCompatHintVisibility(/* show= */ false);
-    }
-
-    private static TaskInfo createTaskInfo(boolean hasSizeCompat,
-            @CameraCompatControlState int cameraCompatControlState) {
+    private static TaskInfo createTaskInfo(boolean hasSizeCompat) {
         ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
         taskInfo.taskId = TASK_ID;
         taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
-        taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                cameraCompatControlState;
         taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
         taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
         taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000));
         return taskInfo;
     }
-
-    private void verifyOnCameraControlStateUpdatedInvokedWith(int taskId, int state) {
-        final ArgumentCaptor<CompatUIEvent> captureValue = ArgumentCaptor.forClass(
-                CompatUIEvent.class);
-        verify(mCallback).accept(captureValue.capture());
-        final CompatUIEvents.CameraControlStateUpdated compatUIEvent =
-                (CompatUIEvents.CameraControlStateUpdated) captureValue.getValue();
-        Assert.assertEquals((compatUIEvent).getTaskId(), taskId);
-        Assert.assertEquals((compatUIEvent).getState(), state);
-        clearInvocations(mCallback);
-    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index eb3da8f..c5033f3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -16,10 +16,6 @@
 
 package com.android.wm.shell.compatui;
 
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
@@ -36,10 +32,10 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager;
-import android.app.CameraCompatTaskInfo;
 import android.app.TaskInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -65,7 +61,6 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
 import com.android.wm.shell.compatui.api.CompatUIEvent;
-import com.android.wm.shell.compatui.impl.CompatUIEvents;
 
 import junit.framework.Assert;
 
@@ -115,7 +110,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
-        mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+        mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false);
 
         final DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.logicalWidth = TASK_WIDTH;
@@ -186,45 +181,6 @@
 
     @Test
     @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testCreateCameraCompatControl() {
-        // Doesn't create layout if show is false.
-        mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        assertTrue(mWindowManager.createLayout(/* canShow= */ false));
-
-        verify(mWindowManager, never()).inflateLayout();
-
-        // Doesn't create hint popup.
-        mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint = true;
-        assertTrue(mWindowManager.createLayout(/* canShow= */ true));
-
-        verify(mWindowManager).inflateLayout();
-        verify(mLayout).setCameraControlVisibility(/* show= */ true);
-        verify(mLayout, never()).setCameraCompatHintVisibility(/* show= */ true);
-
-        // Creates hint popup.
-        clearInvocations(mWindowManager);
-        clearInvocations(mLayout);
-        mWindowManager.release();
-        mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint = false;
-        assertTrue(mWindowManager.createLayout(/* canShow= */ true));
-
-        verify(mWindowManager).inflateLayout();
-        assertNotNull(mLayout);
-        verify(mLayout).setCameraControlVisibility(/* show= */ true);
-        verify(mLayout).setCameraCompatHintVisibility(/* show= */ true);
-        assertTrue(mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint);
-
-        // Returns false and doesn't create layout if Camera Compat state is hidden
-        clearInvocations(mWindowManager);
-        mWindowManager.release();
-        mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN;
-        assertFalse(mWindowManager.createLayout(/* canShow= */ true));
-
-        verify(mWindowManager, never()).inflateLayout();
-    }
-
-    @Test
-    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRelease() {
         mWindowManager.mHasSizeCompat = true;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -241,10 +197,11 @@
     public void testUpdateCompatInfo() {
         mWindowManager.mHasSizeCompat = true;
         mWindowManager.createLayout(/* canShow= */ true);
+        verify(mLayout).setRestartButtonVisibility(/* show= */ true);
 
         // No diff
         clearInvocations(mWindowManager);
-        TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN);
+        TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ true);
         doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any());
         assertTrue(mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true));
 
@@ -261,58 +218,25 @@
         verify(mWindowManager).release();
         verify(mWindowManager).createLayout(/* canShow= */ true);
 
-        // Change Camera Compat state, show a control.
+        // Change has Size Compat to false, no more CompatIU.
         clearInvocations(mWindowManager);
         clearInvocations(mLayout);
-        taskInfo = createTaskInfo(/* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-        assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
-
-        verify(mLayout).setCameraControlVisibility(/* show= */ true);
-        verify(mLayout).updateCameraTreatmentButton(
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-
-        // Change Camera Compat state, update a control.
-        clearInvocations(mWindowManager);
-        clearInvocations(mLayout);
-        taskInfo = createTaskInfo(/* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-        assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
-
-        verify(mLayout).setCameraControlVisibility(/* show= */ true);
-        verify(mLayout).updateCameraTreatmentButton(
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-
-        // Change has Size Compat to false, hides restart button.
-        clearInvocations(mWindowManager);
-        clearInvocations(mLayout);
-        taskInfo = createTaskInfo(/* hasSizeCompat= */ false,
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-        assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
-
-        verify(mLayout).setRestartButtonVisibility(/* show= */ false);
+        taskInfo = createTaskInfo(/* hasSizeCompat= */ false);
+        assertFalse(mWindowManager.updateCompatInfo(taskInfo, newTaskListener,
+                /* canShow= */ true));
 
         // Change has Size Compat to true, shows restart button.
         clearInvocations(mWindowManager);
         clearInvocations(mLayout);
-        taskInfo = createTaskInfo(/* hasSizeCompat= */ true,
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
+        taskInfo = createTaskInfo(/* hasSizeCompat= */ true);
         assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
 
-        verify(mLayout).setRestartButtonVisibility(/* show= */ true);
-
-        // Change Camera Compat state to dismissed, hide a control.
-        clearInvocations(mWindowManager);
-        clearInvocations(mLayout);
-        taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_DISMISSED);
-        assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
-
-        verify(mLayout).setCameraControlVisibility(/* show= */ false);
+        verify(mLayout, times(2)).setRestartButtonVisibility(/* show= */ true);
 
         // Change task bounds, update position.
         clearInvocations(mWindowManager);
         clearInvocations(mLayout);
-        taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN);
+        taskInfo = createTaskInfo(/* hasSizeCompat= */ true);
         taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000));
         assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
 
@@ -321,7 +245,7 @@
         // Change has Size Compat to false, release layout.
         clearInvocations(mWindowManager);
         clearInvocations(mLayout);
-        taskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+        taskInfo = createTaskInfo(/* hasSizeCompat= */ false);
         assertFalse(
                 mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
 
@@ -338,15 +262,14 @@
         // Change topActivityInSizeCompat to false and pass canShow true, layout shouldn't be
         // inflated
         clearInvocations(mWindowManager);
-        TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ false,
-                CAMERA_COMPAT_CONTROL_HIDDEN);
+        TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ false);
         mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
 
         verify(mWindowManager, never()).inflateLayout();
 
         // Change topActivityInSizeCompat to true and pass canShow true, layout should be inflated.
         clearInvocations(mWindowManager);
-        taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN);
+        taskInfo = createTaskInfo(/* hasSizeCompat= */ true);
         mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
 
         verify(mWindowManager).inflateLayout();
@@ -443,37 +366,6 @@
 
     @Test
     @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testOnCameraDismissButtonClicked() {
-        mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        mWindowManager.createLayout(/* canShow= */ true);
-        clearInvocations(mLayout);
-        mWindowManager.onCameraDismissButtonClicked();
-
-        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED);
-        verify(mLayout).setCameraControlVisibility(/* show= */ false);
-    }
-
-    @Test
-    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testOnCameraTreatmentButtonClicked() {
-        mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        mWindowManager.createLayout(/* canShow= */ true);
-        clearInvocations(mLayout);
-        mWindowManager.onCameraTreatmentButtonClicked();
-
-        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-        verify(mLayout).updateCameraTreatmentButton(CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-
-        mWindowManager.onCameraTreatmentButtonClicked();
-
-        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-        verify(mLayout).updateCameraTreatmentButton(CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-    }
-
-    @Test
-    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnRestartButtonClicked() {
         mWindowManager.onRestartButtonClicked();
 
@@ -505,22 +397,6 @@
 
     @Test
     @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
-    public void testOnCameraControlLongClicked_showHint() {
-       // Not create hint popup.
-        mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint = true;
-        mWindowManager.createLayout(/* canShow= */ true);
-
-        verify(mWindowManager).inflateLayout();
-        verify(mLayout, never()).setCameraCompatHintVisibility(/* show= */ true);
-
-        mWindowManager.onCameraButtonLongClicked();
-
-        verify(mLayout).setCameraCompatHintVisibility(/* show= */ true);
-    }
-
-    @Test
-    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testWhenDockedStateHasChanged_needsToBeRecreated() {
         ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
         newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK;
@@ -538,7 +414,7 @@
                 mCompatUIConfiguration, mOnRestartButtonClicked);
 
         // Simulate rotation of activity in square display
-        TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN);
+        TaskInfo taskInfo = createTaskInfo(true);
         taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT;
         taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850;
 
@@ -567,13 +443,10 @@
         assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
     }
 
-    private static TaskInfo createTaskInfo(boolean hasSizeCompat,
-            @CameraCompatTaskInfo.CameraCompatControlState int cameraCompatControlState) {
+    private static TaskInfo createTaskInfo(boolean hasSizeCompat) {
         ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
         taskInfo.taskId = TASK_ID;
         taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
-        taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                cameraCompatControlState;
         taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK;
         // Letterboxed activity that takes half the screen should show size compat restart button
         taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
@@ -582,15 +455,4 @@
         taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
         return taskInfo;
     }
-
-    private void verifyOnCameraControlStateUpdatedInvokedWith(int taskId, int state) {
-        final ArgumentCaptor<CompatUIEvent> captureValue = ArgumentCaptor.forClass(
-                CompatUIEvent.class);
-        verify(mCallback).accept(captureValue.capture());
-        final CompatUIEvents.CameraControlStateUpdated compatUIEvent =
-                (CompatUIEvents.CameraControlStateUpdated) captureValue.getValue();
-        Assert.assertEquals((compatUIEvent).getTaskId(), taskId);
-        Assert.assertEquals((compatUIEvent).getState(), state);
-        clearInvocations(mCallback);
-    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
index 3fa21ce..7a64196 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
@@ -16,8 +16,6 @@
 
 package com.android.wm.shell.compatui;
 
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
 
@@ -26,7 +24,6 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.TaskInfo;
 import android.content.ComponentName;
 import android.platform.test.annotations.RequiresFlagsDisabled;
@@ -98,7 +95,7 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+        mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false);
         mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo,
                 mSyncTransactionQueue, mTaskListener, new DisplayLayout(),
                 new CompatUIController.CompatUIHintsState(),
@@ -155,13 +152,10 @@
         verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ false);
     }
 
-    private static TaskInfo createTaskInfo(boolean hasSizeCompat,
-            @CameraCompatControlState int cameraCompatControlState) {
+    private static TaskInfo createTaskInfo(boolean hasSizeCompat) {
         ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
         taskInfo.taskId = TASK_ID;
         taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
-        taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
-                cameraCompatControlState;
         taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity");
         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 0a5672d..7acee78 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
@@ -158,8 +158,8 @@
     }
 
     @Test
-    fun updateVisibleFreeformTasks_singleVisibleNonClosingTask_updatesTasksCorrectly() {
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+    fun updateTaskVisibility_singleVisibleNonClosingTask_updatesTasksCorrectly() {
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
 
         assertThat(repo.isVisibleTask(1)).isTrue()
         assertThat(repo.isClosingTask(1)).isFalse()
@@ -172,7 +172,7 @@
 
     @Test
     fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() {
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
         repo.addClosingTask(DEFAULT_DISPLAY, 1)
 
         // A visible task that's closing
@@ -186,7 +186,7 @@
 
     @Test
     fun isOnlyVisibleNonClosingTask_singleVisibleMinimizedTask() {
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
         repo.minimizeTask(DEFAULT_DISPLAY, 1)
 
         // The visible task that's closing
@@ -200,8 +200,8 @@
 
     @Test
     fun isOnlyVisibleNonClosingTask_multipleVisibleNonClosingTasks() {
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true)
 
         // Not the only task
         assertThat(repo.isVisibleTask(1)).isTrue()
@@ -219,9 +219,9 @@
 
     @Test
     fun isOnlyVisibleNonClosingTask_multipleDisplays() {
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
-        repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 3, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true)
+        repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 3, visible = true)
 
         // Not the only task on DEFAULT_DISPLAY
         assertThat(repo.isVisibleTask(1)).isTrue()
@@ -239,7 +239,7 @@
 
     @Test
     fun addVisibleTasksListener_notifiesVisibleFreeformTask() {
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
         val listener = TestVisibilityListener()
         val executor = TestShellExecutor()
 
@@ -252,7 +252,7 @@
 
     @Test
     fun addListener_tasksOnDifferentDisplay_doesNotNotify() {
-        repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 1, visible = true)
         val listener = TestVisibilityListener()
         val executor = TestShellExecutor()
         repo.addVisibleTasksListener(listener, executor)
@@ -264,13 +264,13 @@
     }
 
     @Test
-    fun updateVisibleFreeformTasks_addVisibleTasksNotifiesListener() {
+    fun updateTaskVisibility_addVisibleTasksNotifiesListener() {
         val listener = TestVisibilityListener()
         val executor = TestShellExecutor()
         repo.addVisibleTasksListener(listener, executor)
 
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true)
         executor.flushAll()
 
         assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
@@ -278,12 +278,12 @@
     }
 
     @Test
-    fun updateVisibleFreeformTasks_addVisibleTaskNotifiesListenerForThatDisplay() {
+    fun updateTaskVisibility_addVisibleTaskNotifiesListenerForThatDisplay() {
         val listener = TestVisibilityListener()
         val executor = TestShellExecutor()
         repo.addVisibleTasksListener(listener, executor)
 
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
         executor.flushAll()
 
         assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
@@ -291,7 +291,7 @@
         assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0)
         assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0)
 
-        repo.updateVisibleFreeformTasks(displayId = 1, taskId = 2, visible = true)
+        repo.updateTaskVisibility(displayId = 1, taskId = 2, visible = true)
         executor.flushAll()
 
         // Listener for secondary display is notified
@@ -302,17 +302,17 @@
     }
 
     @Test
-    fun updateVisibleFreeformTasks_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() {
+    fun updateTaskVisibility_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() {
         val listener = TestVisibilityListener()
         val executor = TestShellExecutor()
         repo.addVisibleTasksListener(listener, executor)
 
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
         executor.flushAll()
         assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
 
         // Mark task 1 visible on secondary display
-        repo.updateVisibleFreeformTasks(displayId = 1, taskId = 1, visible = true)
+        repo.updateTaskVisibility(displayId = 1, taskId = 1, visible = true)
         executor.flushAll()
 
         // Default display should have 2 calls
@@ -327,22 +327,22 @@
     }
 
     @Test
-    fun updateVisibleFreeformTasks_removeVisibleTasksNotifiesListener() {
+    fun updateTaskVisibility_removeVisibleTasksNotifiesListener() {
         val listener = TestVisibilityListener()
         val executor = TestShellExecutor()
         repo.addVisibleTasksListener(listener, executor)
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true)
         executor.flushAll()
 
         assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
 
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = false)
         executor.flushAll()
 
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
 
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = false)
         executor.flushAll()
 
         assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
@@ -354,17 +354,17 @@
      * This tests that task is removed from the last parent display when it vanishes.
      */
     @Test
-    fun updateVisibleFreeformTasks_removeVisibleTasksRemovesTaskWithInvalidDisplay() {
+    fun updateTaskVisibility_removeVisibleTasksRemovesTaskWithInvalidDisplay() {
         val listener = TestVisibilityListener()
         val executor = TestShellExecutor()
         repo.addVisibleTasksListener(listener, executor)
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true)
         executor.flushAll()
 
         assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
 
-        repo.updateVisibleFreeformTasks(INVALID_DISPLAY, taskId = 1, visible = false)
+        repo.updateTaskVisibility(INVALID_DISPLAY, taskId = 1, visible = false)
         executor.flushAll()
 
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
@@ -377,30 +377,30 @@
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
 
         // New task increments count to 1
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
 
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
 
         // Visibility update to same task does not increase count
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
 
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
 
         // Second task visible increments count
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true)
 
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
 
         // Hiding a task decrements count
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = false)
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
 
         // Hiding all tasks leaves count at 0
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = false)
         assertThat(repo.getVisibleTaskCount(displayId = 9)).isEqualTo(0)
 
         // Hiding a not existing task, count remains at 0
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 999, visible = false)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 999, visible = false)
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
     }
 
@@ -410,32 +410,32 @@
         assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0)
 
         // New task on default display increments count for that display only
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true)
 
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
         assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0)
 
         // New task on secondary display, increments count for that display only
-        repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 2, visible = true)
+        repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 2, visible = true)
 
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
         assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
 
         // Marking task visible on another display, updates counts for both displays
-        repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true)
+        repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 1, visible = true)
 
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
         assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2)
 
         // Marking task that is on secondary display, hidden on default display, does not affect
         // secondary display
-        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
+        repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = false)
 
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
         assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2)
 
         // Hiding a task on that display, decrements count
-        repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = false)
+        repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 1, visible = false)
 
         assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
         assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
@@ -468,8 +468,65 @@
     }
 
     @Test
+    fun addOrMoveFreeformTaskToTop_taskIsMinimized_unminimizesTask() {
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7)
+        repo.minimizeTask(displayId = 0, taskId = 6)
+
+        val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+        assertThat(tasks).containsExactly(7, 6, 5).inOrder()
+        assertThat(repo.isMinimizedTask(taskId = 6)).isTrue()
+    }
+
+    @Test
+    fun addOrMoveFreeformTaskToTop_taskIsUnminimized_noop() {
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7)
+
+        val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+        assertThat(tasks).containsExactly(7, 6, 5).inOrder()
+        assertThat(repo.isMinimizedTask(taskId = 6)).isFalse()
+    }
+
+    @Test
+    fun removeFreeformTask_invalidDisplay_removesTaskFromFreeformTasks() {
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1)
+
+        repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1)
+
+        val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY)
+        assertThat(invalidDisplayTasks).isEmpty()
+        val validDisplayTasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+        assertThat(validDisplayTasks).isEmpty()
+    }
+
+    @Test
+    fun removeFreeformTask_validDisplay_removesTaskFromFreeformTasks() {
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1)
+
+        repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1)
+
+        val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+        assertThat(tasks).isEmpty()
+    }
+
+    @Test
+    fun removeFreeformTask_validDisplay_differentDisplay_doesNotRemovesTask() {
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1)
+
+        repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1)
+
+        val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+        assertThat(tasks).containsExactly(1)
+    }
+
+    @Test
     fun removeFreeformTask_removesTaskBoundsBeforeMaximize() {
         val taskId = 1
+        repo.addActiveTask(THIRD_DISPLAY, taskId)
+        repo.addOrMoveFreeformTaskToTop(THIRD_DISPLAY, taskId)
         repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200))
 
         repo.removeFreeformTask(THIRD_DISPLAY, taskId)
@@ -537,9 +594,9 @@
 
 
     @Test
-    fun updateVisibleFreeformTasks_minimizedTaskBecomesVisible_unminimizesTask() {
+    fun updateTaskVisibility_minimizedTaskBecomesVisible_unminimizesTask() {
         repo.minimizeTask(displayId = 10, taskId = 2)
-        repo.updateVisibleFreeformTasks(displayId = 10, taskId = 2, visible = true)
+        repo.updateTaskVisibility(displayId = 10, taskId = 2, visible = true)
 
         val isMinimizedTask = repo.isMinimizedTask(taskId = 2)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index bd39aa6..2dea43b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -61,20 +61,23 @@
 
     @Test
     fun testFullscreenRegionCalculation() {
-        val transitionHeight = context.resources.getDimensionPixelSize(
-            R.dimen.desktop_mode_fullscreen_from_desktop_height)
-        val fromFreeformWidth = mContext.resources.getDimensionPixelSize(
-            R.dimen.desktop_mode_fullscreen_from_desktop_width
-        )
         var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
             WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT)
         assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top))
         testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
             WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT)
+
+        val transitionHeight = context.resources.getDimensionPixelSize(
+            R.dimen.desktop_mode_transition_region_thickness)
+        val toFullscreenScale = mContext.resources.getFloat(
+            R.dimen.desktop_mode_fullscreen_region_scale
+        )
+        val toFullscreenWidth = displayLayout.width() * toFullscreenScale
+
         assertThat(testRegion.bounds).isEqualTo(Rect(
-            DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2,
+            (DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(),
             -50,
-            DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2,
+            (DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(),
             transitionHeight))
         testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
             WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT)
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 e66018f..871f612 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
@@ -43,6 +43,7 @@
 import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
 import android.view.Display.DEFAULT_DISPLAY
+import android.view.Gravity
 import android.view.SurfaceControl
 import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_CHANGE
@@ -70,6 +71,7 @@
 import com.android.window.flags.Flags
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
 import com.android.wm.shell.MockToken
+import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
@@ -174,7 +176,7 @@
   private lateinit var mockitoSession: StaticMockitoSession
   private lateinit var controller: DesktopTasksController
   private lateinit var shellInit: ShellInit
-  private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository
+  private lateinit var taskRepository: DesktopModeTaskRepository
   private lateinit var desktopTasksLimiter: DesktopTasksLimiter
   private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener
 
@@ -185,12 +187,12 @@
 
   private val DISPLAY_DIMENSION_SHORT = 1600
   private val DISPLAY_DIMENSION_LONG = 2560
-  private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400)
-  private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240)
-  private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880)
-  private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400)
-  private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861)
-  private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400)
+  private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 75, 2240, 1275)
+  private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 165, 1400, 2085)
+  private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 435, 1575, 1635)
+  private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 75, 1880, 1275)
+  private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611)
+  private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275)
 
   @Before
   fun setUp() {
@@ -202,11 +204,11 @@
     doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
 
     shellInit = spy(ShellInit(testExecutor))
-    desktopModeTaskRepository = DesktopModeTaskRepository()
+    taskRepository = DesktopModeTaskRepository()
     desktopTasksLimiter =
         DesktopTasksLimiter(
             transitions,
-            desktopModeTaskRepository,
+            taskRepository,
             shellTaskOrganizer,
             MAX_TASK_LIMIT,
         )
@@ -250,7 +252,7 @@
         exitDesktopTransitionHandler,
         toggleResizeDesktopTaskTransitionHandler,
         dragToDesktopTransitionHandler,
-        desktopModeTaskRepository,
+        taskRepository,
         desktopModeLoggerTransitionObserver,
         launchAdjacentController,
         recentsTransitionHandler,
@@ -528,7 +530,7 @@
 
     markTaskHidden(freeformTask)
     markTaskHidden(minimizedTask)
-    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
+    taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
     controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
     val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
@@ -547,7 +549,7 @@
 
     markTaskHidden(freeformTask)
     markTaskHidden(minimizedTask)
-    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
+    taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
     controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
     val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
@@ -590,6 +592,161 @@
   }
 
   @Test
+  fun addMoveToDesktopChanges_gravityLeft_noBoundsApplied() {
+    setUpLandscapeDisplay()
+    val task = setUpFullscreenTask(gravity = Gravity.LEFT)
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(finalBounds).isEqualTo(Rect())
+  }
+
+  @Test
+  fun addMoveToDesktopChanges_gravityRight_noBoundsApplied() {
+    setUpLandscapeDisplay()
+    val task = setUpFullscreenTask(gravity = Gravity.RIGHT)
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(finalBounds).isEqualTo(Rect())
+  }
+
+  @Test
+  fun addMoveToDesktopChanges_gravityTop_noBoundsApplied() {
+    setUpLandscapeDisplay()
+    val task = setUpFullscreenTask(gravity = Gravity.TOP)
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(finalBounds).isEqualTo(Rect())
+  }
+
+  @Test
+  fun addMoveToDesktopChanges_gravityBottom_noBoundsApplied() {
+    setUpLandscapeDisplay()
+    val task = setUpFullscreenTask(gravity = Gravity.BOTTOM)
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(finalBounds).isEqualTo(Rect())
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+  fun addMoveToDesktopChanges_positionBottomRight() {
+    setUpLandscapeDisplay()
+    val stableBounds = Rect()
+    displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+    setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
+
+    val task = setUpFullscreenTask()
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+      .isEqualTo(DesktopTaskPosition.BottomRight)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+  fun addMoveToDesktopChanges_positionTopLeft() {
+    setUpLandscapeDisplay()
+    val stableBounds = Rect()
+    displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+    addFreeformTaskAtPosition(DesktopTaskPosition.BottomRight, stableBounds)
+
+    val task = setUpFullscreenTask()
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+      .isEqualTo(DesktopTaskPosition.TopLeft)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+  fun addMoveToDesktopChanges_positionBottomLeft() {
+    setUpLandscapeDisplay()
+    val stableBounds = Rect()
+    displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+    addFreeformTaskAtPosition(DesktopTaskPosition.TopLeft, stableBounds)
+
+    val task = setUpFullscreenTask()
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+      .isEqualTo(DesktopTaskPosition.BottomLeft)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+  fun addMoveToDesktopChanges_positionTopRight() {
+    setUpLandscapeDisplay()
+    val stableBounds = Rect()
+    displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+    addFreeformTaskAtPosition(DesktopTaskPosition.BottomLeft, stableBounds)
+
+    val task = setUpFullscreenTask()
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+      .isEqualTo(DesktopTaskPosition.TopRight)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+  fun addMoveToDesktopChanges_positionResetsToCenter() {
+    setUpLandscapeDisplay()
+    val stableBounds = Rect()
+    displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+    addFreeformTaskAtPosition(DesktopTaskPosition.TopRight, stableBounds)
+
+    val task = setUpFullscreenTask()
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+      .isEqualTo(DesktopTaskPosition.Center)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+  fun addMoveToDesktopChanges_defaultToCenterIfFree() {
+    setUpLandscapeDisplay()
+    val stableBounds = Rect()
+    displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+    val minTouchTarget = context.resources.getDimensionPixelSize(
+      R.dimen.freeform_required_visible_empty_space_in_header)
+    addFreeformTaskAtPosition(DesktopTaskPosition.Center, stableBounds,
+      Rect(0, 0, 1600, 1200), Point(0, minTouchTarget + 1))
+
+    val task = setUpFullscreenTask()
+    val wct = WindowContainerTransaction()
+    controller.addMoveToDesktopChanges(wct, task)
+
+    val finalBounds = findBoundsChange(wct, task)
+    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+      .isEqualTo(DesktopTaskPosition.Center)
+  }
+
+  @Test
   fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
     val task = setUpFullscreenTask()
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -838,7 +995,7 @@
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
     assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
       .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
 
@@ -867,7 +1024,7 @@
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
     assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
       .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
 
@@ -887,7 +1044,7 @@
     setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
     assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
       .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
 
@@ -1028,7 +1185,7 @@
   fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
 
     val wct = WindowContainerTransaction()
     controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
@@ -1040,8 +1197,8 @@
   fun onDesktopWindowClose_singleActiveTask_isClosing() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId)
 
     val wct = WindowContainerTransaction()
     controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
@@ -1053,8 +1210,8 @@
   fun onDesktopWindowClose_singleActiveTask_isMinimized() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
 
     val wct = WindowContainerTransaction()
     controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
@@ -1067,7 +1224,7 @@
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
     val wallpaperToken = MockToken().token()
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
 
     val wct = WindowContainerTransaction()
     controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId)
@@ -1080,8 +1237,8 @@
     val task1 = setUpFreeformTask()
     val task2 = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId)
 
     val wct = WindowContainerTransaction()
     controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId)
@@ -1094,8 +1251,8 @@
     val task1 = setUpFreeformTask()
     val task2 = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
 
     val wct = WindowContainerTransaction()
     controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId)
@@ -1501,7 +1658,7 @@
   fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
     val task = setUpFreeformTask()
 
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    taskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
 
     assertNull(result, "Should not handle request")
@@ -1516,7 +1673,7 @@
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
 
     // Should create remove wallpaper transaction
@@ -1531,7 +1688,7 @@
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
 
     // Should create remove wallpaper transaction
@@ -1547,7 +1704,7 @@
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    taskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
 
     assertNull(result, "Should not handle request")
@@ -1562,7 +1719,7 @@
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    taskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
 
     assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token)
@@ -1575,7 +1732,7 @@
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    taskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
 
     assertNull(result, "Should not handle request")
@@ -1591,8 +1748,8 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
 
     // Should create remove wallpaper transaction
@@ -1608,8 +1765,8 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
 
     // Should create remove wallpaper transaction
@@ -1626,8 +1783,8 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
 
     // Should create remove wallpaper transaction
@@ -1643,8 +1800,8 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
 
     // Should create remove wallpaper transaction
@@ -1661,11 +1818,11 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     // Task is being minimized so mark it as not visible.
-    desktopModeTaskRepository
-      .updateVisibleFreeformTasks(displayId = DEFAULT_DISPLAY, task2.taskId, false)
+    taskRepository
+      .updateTaskVisibility(displayId = DEFAULT_DISPLAY, task2.taskId, false)
     val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK))
 
     assertNull(result, "Should not handle request")
@@ -1716,7 +1873,7 @@
   fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
     val task = setUpFreeformTask()
 
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    taskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
 
     assertNull(result, "Should not handle request")
@@ -1731,7 +1888,7 @@
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
 
     // Should create remove wallpaper transaction
@@ -1746,7 +1903,7 @@
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
 
     // Should create remove wallpaper transaction
@@ -1762,7 +1919,7 @@
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    taskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
 
     assertNull(result, "Should not handle request")
@@ -1777,7 +1934,7 @@
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    taskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
 
     assertNotNull(result, "Should handle request")
@@ -1791,7 +1948,7 @@
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    taskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
 
     assertNull(result, "Should not handle request")
@@ -1807,8 +1964,8 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
 
     // Should create remove wallpaper transaction
@@ -1824,8 +1981,8 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
 
     // Should create remove wallpaper transaction
@@ -1842,8 +1999,8 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
 
     // Should create remove wallpaper transaction
@@ -1859,8 +2016,8 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
 
     // Should create remove wallpaper transaction
@@ -1877,11 +2034,11 @@
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
 
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     // Task is being minimized so mark it as not visible.
-    desktopModeTaskRepository
-      .updateVisibleFreeformTasks(displayId = DEFAULT_DISPLAY, task2.taskId, false)
+    taskRepository
+      .updateTaskVisibility(displayId = DEFAULT_DISPLAY, task2.taskId, false)
     val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK))
 
     assertNull(result, "Should not handle request")
@@ -1975,9 +2132,9 @@
     task1.isFocused = false
     task2.isFocused = true
     task3.isFocused = false
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
-    desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId,
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
+    taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task3.taskId,
       visible = false)
 
     controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
@@ -1998,7 +2155,7 @@
     task1.isFocused = false
     task2.isFocused = true
     task3.isFocused = false
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
     controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
 
     val wct = getLatestExitDesktopWct()
@@ -2247,9 +2404,9 @@
     task1.isFocused = false
     task2.isFocused = true
     task3.isFocused = false
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
-    desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId,
+    taskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
+    taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task3.taskId,
       visible = false)
 
     controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
@@ -2275,7 +2432,7 @@
     task1.isFocused = false
     task2.isFocused = true
     task3.isFocused = false
-    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    taskRepository.wallpaperActivityToken = wallpaperToken
 
     controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
 
@@ -2327,7 +2484,7 @@
     val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
 
     controller.toggleDesktopTaskSize(task)
-    assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds)
+    assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds)
   }
 
   @Test
@@ -2400,12 +2557,24 @@
     controller.toggleDesktopTaskSize(task)
 
     // Assert last bounds before maximize removed after use
-    assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
+    assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
   }
 
   private val desktopWallpaperIntent: Intent
     get() = Intent(context, DesktopWallpaperActivity::class.java)
 
+  private fun addFreeformTaskAtPosition(
+    pos: DesktopTaskPosition,
+    stableBounds: Rect,
+    bounds: Rect = DEFAULT_LANDSCAPE_BOUNDS,
+    offsetPos: Point = Point(0, 0)
+  ): RunningTaskInfo {
+    val offset = pos.getTopLeftCoordinates(stableBounds, bounds)
+    val prevTaskBounds = Rect(bounds)
+    prevTaskBounds.offsetTo(offset.x + offsetPos.x, offset.y + offsetPos.y)
+    return setUpFreeformTask(bounds = prevTaskBounds)
+  }
+
   private fun setUpFreeformTask(
       displayId: Int = DEFAULT_DISPLAY,
       bounds: Rect? = null
@@ -2414,9 +2583,9 @@
     val activityInfo = ActivityInfo()
     task.topActivityInfo = activityInfo
     whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
-    desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
-    desktopModeTaskRepository.updateVisibleFreeformTasks(displayId, task.taskId, visible = true)
-    desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
+    taskRepository.addActiveTask(displayId, task.taskId)
+    taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true)
+    taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
     runningTasks.add(task)
     return task
   }
@@ -2434,11 +2603,13 @@
       windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
       deviceOrientation: Int = ORIENTATION_LANDSCAPE,
       screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED,
-      shouldLetterbox: Boolean = false
+      shouldLetterbox: Boolean = false,
+      gravity: Int = Gravity.NO_GRAVITY
   ): RunningTaskInfo {
     val task = createFullscreenTask(displayId)
     val activityInfo = ActivityInfo()
     activityInfo.screenOrientation = screenOrientation
+    activityInfo.windowLayout = ActivityInfo.WindowLayout(0, 0F, 0, 0F, gravity, 0, 0)
     with(task) {
       topActivityInfo = activityInfo
       isResizeable = isResizable
@@ -2479,11 +2650,23 @@
   private fun setUpLandscapeDisplay() {
     whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG)
     whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT)
+    val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG,
+      DISPLAY_DIMENSION_SHORT - Companion.TASKBAR_FRAME_HEIGHT
+    )
+    whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
+      (i.arguments.first() as Rect).set(stableBounds)
+    }
   }
 
   private fun setUpPortraitDisplay() {
     whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
     whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
+    val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_SHORT,
+      DISPLAY_DIMENSION_LONG - Companion.TASKBAR_FRAME_HEIGHT
+    )
+    whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
+      (i.arguments.first() as Rect).set(stableBounds)
+    }
   }
 
   private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
@@ -2495,12 +2678,12 @@
   }
 
   private fun markTaskVisible(task: RunningTaskInfo) {
-    desktopModeTaskRepository.updateVisibleFreeformTasks(
+    taskRepository.updateTaskVisibility(
         task.displayId, task.taskId, visible = true)
   }
 
   private fun markTaskHidden(task: RunningTaskInfo) {
-    desktopModeTaskRepository.updateVisibleFreeformTasks(
+    taskRepository.updateTaskVisibility(
         task.displayId, task.taskId, visible = false)
   }
 
@@ -2601,6 +2784,7 @@
     const val SECOND_DISPLAY = 2
     val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
     const val MAX_TASK_LIMIT = 6
+    private const val TASKBAR_FRAME_HEIGHT = 200
   }
 }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 70f3bf8..d4a749c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -358,7 +358,7 @@
     }
 
     private fun markTaskVisible(task: RunningTaskInfo) {
-        desktopTaskRepo.updateVisibleFreeformTasks(
+        desktopTaskRepo.updateTaskVisibility(
                 task.displayId,
                 task.taskId,
                 visible = true
@@ -366,7 +366,7 @@
     }
 
     private fun markTaskHidden(task: RunningTaskInfo) {
-        desktopTaskRepo.updateVisibleFreeformTasks(
+        desktopTaskRepo.updateTaskVisibility(
                 task.displayId,
                 task.taskId,
                 visible = false
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index e302fa8..d71f3b6 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -736,6 +736,7 @@
 
 cc_test {
     name: "hwui_unit_tests",
+    test_config: "tests/unit/AndroidTest.xml",
     defaults: [
         "hwui_test_defaults",
         "android_graphics_apex",
@@ -803,6 +804,7 @@
 
 cc_benchmark {
     name: "hwuimacro",
+    test_config: "tests/macrobench/AndroidTest.xml",
     defaults: ["hwui_test_defaults"],
 
     static_libs: ["libhwui"],
@@ -822,6 +824,7 @@
 
 cc_benchmark {
     name: "hwuimicro",
+    test_config: "tests/microbench/AndroidTest.xml",
     defaults: ["hwui_test_defaults"],
 
     static_libs: ["libhwui_static"],
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 5d3bc89..d184f64 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -101,6 +101,8 @@
 bool Properties::clipSurfaceViews = false;
 bool Properties::hdr10bitPlus = false;
 
+int Properties::timeoutMultiplier = 1;
+
 StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI;
 
 DrawingEnabled Properties::drawingEnabled = DrawingEnabled::NotInitialized;
@@ -174,6 +176,8 @@
             base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews());
     hdr10bitPlus = hwui_flags::hdr_10bit_plus();
 
+    timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1);
+
     return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw);
 }
 
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index d3176f6..e264642 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -343,6 +343,8 @@
     static bool clipSurfaceViews;
     static bool hdr10bitPlus;
 
+    static int timeoutMultiplier;
+
     static StretchEffectBehavior getStretchEffectBehavior() {
         return stretchEffectBehavior;
     }
diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp
index afe4c38..2f15722 100644
--- a/libs/hwui/Readback.cpp
+++ b/libs/hwui/Readback.cpp
@@ -91,8 +91,10 @@
 
     {
         ATRACE_NAME("sync_wait");
-        if (sourceFence != -1 && sync_wait(sourceFence.get(), 500 /* ms */) != NO_ERROR) {
-            ALOGE("Timeout (500ms) exceeded waiting for buffer fence, abandoning readback attempt");
+        int syncWaitTimeoutMs = 500 * Properties::timeoutMultiplier;
+        if (sourceFence != -1 && sync_wait(sourceFence.get(), syncWaitTimeoutMs) != NO_ERROR) {
+            ALOGE("Timeout (%dms) exceeded waiting for buffer fence, abandoning readback attempt",
+                  syncWaitTimeoutMs);
             return request->onCopyFinished(CopyResult::Timeout);
         }
     }
@@ -109,9 +111,8 @@
 
     sk_sp<SkColorSpace> colorSpace =
             DataSpaceToColorSpace(static_cast<android_dataspace>(dataspace));
-    sk_sp<SkImage> image =
-            SkImages::DeferredFromAHardwareBuffer(sourceBuffer.get(), kPremul_SkAlphaType, 
-                                                  colorSpace);
+    sk_sp<SkImage> image = SkImages::DeferredFromAHardwareBuffer(sourceBuffer.get(),
+                                                                 kPremul_SkAlphaType, colorSpace);
 
     if (!image.get()) {
         return request->onCopyFinished(CopyResult::UnknownError);
diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/tests/macrobench/AndroidTest.xml
similarity index 60%
copy from libs/hwui/AndroidTest.xml
copy to libs/hwui/tests/macrobench/AndroidTest.xml
index 75f61f5..5b8576d 100644
--- a/libs/hwui/AndroidTest.xml
+++ b/libs/hwui/tests/macrobench/AndroidTest.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- Copyright 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.
@@ -13,24 +13,13 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<configuration description="Config for hwuimicro">
+<configuration description="Config for hwuimacro">
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
-        <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
-        <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" />
         <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" />
     </target_preparer>
     <option name="test-suite-tag" value="apct" />
     <option name="not-shardable" value="true" />
-    <test class="com.android.tradefed.testtype.GTest" >
-        <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
-        <option name="module-name" value="hwui_unit_tests" />
-    </test>
-    <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
-        <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
-        <option name="benchmark-module-name" value="hwuimicro" />
-        <option name="file-exclusion-filter-regex" value=".*\.config$" />
-    </test>
     <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
         <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
         <option name="benchmark-module-name" value="hwuimacro" />
diff --git a/libs/hwui/tests/macrobench/how_to_run.txt b/libs/hwui/tests/macrobench/how_to_run.txt
index 3c3d36a..59ef25a 100644
--- a/libs/hwui/tests/macrobench/how_to_run.txt
+++ b/libs/hwui/tests/macrobench/how_to_run.txt
@@ -3,3 +3,7 @@
 adb shell /data/benchmarktest/hwuimacro/hwuimacro shadowgrid2 --onscreen
 
 Pass --help to get help
+
+OR (if you don't need to pass arguments)
+
+atest hwuimacro
diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/tests/microbench/AndroidTest.xml
similarity index 63%
rename from libs/hwui/AndroidTest.xml
rename to libs/hwui/tests/microbench/AndroidTest.xml
index 75f61f5..d67305df 100644
--- a/libs/hwui/AndroidTest.xml
+++ b/libs/hwui/tests/microbench/AndroidTest.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- Copyright 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.
@@ -16,24 +16,13 @@
 <configuration description="Config for hwuimicro">
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
-        <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
         <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" />
-        <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" />
     </target_preparer>
     <option name="test-suite-tag" value="apct" />
     <option name="not-shardable" value="true" />
-    <test class="com.android.tradefed.testtype.GTest" >
-        <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
-        <option name="module-name" value="hwui_unit_tests" />
-    </test>
     <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
         <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
         <option name="benchmark-module-name" value="hwuimicro" />
         <option name="file-exclusion-filter-regex" value=".*\.config$" />
     </test>
-    <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
-        <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
-        <option name="benchmark-module-name" value="hwuimacro" />
-        <option name="file-exclusion-filter-regex" value=".*\.config$" />
-    </test>
 </configuration>
diff --git a/libs/hwui/tests/microbench/how_to_run.txt b/libs/hwui/tests/microbench/how_to_run.txt
index 915fe5d..c7ddc1a 100755
--- a/libs/hwui/tests/microbench/how_to_run.txt
+++ b/libs/hwui/tests/microbench/how_to_run.txt
@@ -1,3 +1,7 @@
 mmm -j8 frameworks/base/libs/hwui &&
 adb push $OUT/data/benchmarktest/hwuimicro/hwuimicro /data/benchmarktest/hwuimicro/hwuimicro &&
 adb shell /data/benchmarktest/hwuimicro/hwuimicro
+
+OR
+
+atest hwuimicro
diff --git a/libs/hwui/tests/unit/AndroidTest.xml b/libs/hwui/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..dc586c9
--- /dev/null
+++ b/libs/hwui/tests/unit/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 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.
+-->
+<configuration description="Config for hwui_unit_tests">
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
+    </target_preparer>
+    <option name="test-suite-tag" value="apct" />
+    <option name="not-shardable" value="true" />
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
+        <option name="module-name" value="hwui_unit_tests" />
+    </test>
+</configuration>
diff --git a/libs/hwui/tests/unit/how_to_run.txt b/libs/hwui/tests/unit/how_to_run.txt
index c11d6eb3..1a35adf 100755
--- a/libs/hwui/tests/unit/how_to_run.txt
+++ b/libs/hwui/tests/unit/how_to_run.txt
@@ -2,3 +2,11 @@
 adb push $ANDROID_PRODUCT_OUT/data/nativetest/hwui_unit_tests/hwui_unit_tests \
     /data/nativetest/hwui_unit_tests/hwui_unit_tests &&
 adb shell /data/nativetest/hwui_unit_tests/hwui_unit_tests
+
+OR
+
+atest hwui_unit_tests
+
+OR, if you need arguments, they can be passed as native-test-flags, as in:
+
+atest hwui_unit_tests -- --test-arg com.android.tradefed.testtype.GTest:native-test-flag:"--renderer=skiavk"
diff --git a/libs/hwui/tests/unit/main.cpp b/libs/hwui/tests/unit/main.cpp
index 76cbc8a..3fd15c4 100644
--- a/libs/hwui/tests/unit/main.cpp
+++ b/libs/hwui/tests/unit/main.cpp
@@ -15,6 +15,7 @@
  */
 
 #include <getopt.h>
+#include <log/log.h>
 #include <signal.h>
 
 #include "Properties.h"
@@ -65,6 +66,19 @@
     return RenderPipelineType::SkiaGL;
 }
 
+static constexpr const char* renderPipelineTypeName(const RenderPipelineType renderPipelineType) {
+    switch (renderPipelineType) {
+        case RenderPipelineType::SkiaGL:
+            return "SkiaGL";
+        case RenderPipelineType::SkiaVulkan:
+            return "SkiaVulkan";
+        case RenderPipelineType::SkiaCpu:
+            return "SkiaCpu";
+        case RenderPipelineType::NotInitialized:
+            return "NotInitialized";
+    }
+}
+
 struct Options {
     RenderPipelineType renderer = RenderPipelineType::SkiaGL;
 };
@@ -118,6 +132,7 @@
 
     auto opts = parseOptions(argc, argv);
     Properties::overrideRenderPipelineType(opts.renderer);
+    ALOGI("Starting HWUI unit tests with %s pipeline", renderPipelineTypeName(opts.renderer));
 
     // Run the tests
     testing::InitGoogleTest(&argc, argv);
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index 4217562..1024a55 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -578,6 +578,8 @@
     });
 
     private AudioAttributes() {
+        mBundle = null;
+        mFormattedTags = "";
     }
 
     /**
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 395f81d..0ffab4b 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -1166,10 +1166,11 @@
 
 
     /**
-     * Returns whether the device supports observer mode or not. When observe
-     * mode is enabled, the NFC hardware will listen for NFC readers, but not
-     * respond to them. When observe mode is disabled, the NFC hardware will
-     * resoond to the reader and proceed with the transaction.
+     * Returns whether the device supports observe mode or not. When observe mode is enabled, the
+     * NFC hardware will listen to NFC readers, but not respond to them. While enabled, observed
+     * polling frames will be sent to the APDU service (see {@link #setObserveModeEnabled(boolean)}.
+     * When observe mode is disabled (or if it's not supported), the NFC hardware will automatically
+     * respond to the reader and proceed with the transaction.
      * @return true if the mode is supported, false otherwise.
      */
     @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
@@ -1193,9 +1194,10 @@
      * and simply observe and notify the APDU service of polling loop frames. See
      * {@link #isObserveModeSupported()} for a description of observe mode. Only the package of the
      * currently preferred service (the service set as preferred by the current foreground
-     * application via {@link CardEmulation#setPreferredService(Activity, ComponentName)} or the
-     * current Default Wallet Role Holder {@link android.app.role.RoleManager#ROLE_WALLET}),
-     * otherwise a call to this method will fail and return false.
+     * application via {@link android.nfc.cardemulation.CardEmulation#setPreferredService(Activity,
+     * android.content.ComponentName)} or the current Default Wallet Role Holder
+     * {@link android.app.role.RoleManager#ROLE_WALLET}), otherwise a call to this method will fail
+     * and return false.
      *
      * @param enabled false disables observe mode to allow the transaction to proceed while true
      *                enables observe mode and does not allow transactions to proceed.
diff --git a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
index 9c2064c..8c6880b 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
@@ -58,7 +58,8 @@
     label:                              '5'
     base:                               '\u0665'
     capslock:                           '5'
-    shift:                              '%'
+    shift:                              '\u066a'
+    shift+capslock:                     '%'
 }
 
 key 6 {
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
index b4a9172..ce02404 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
@@ -129,7 +129,7 @@
             }
         }
 
-        override fun onChanged(reason: Int) = onKeyChanged(null, reason)
+        override fun onChanged(observable: Observable, reason: Int) = onKeyChanged(null, reason)
 
         override fun onKeyChanged(key: Any?, reason: Int) {
             notifyBackupManager(key, reason)
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
index ede7c63..4ce1d37 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
@@ -103,7 +103,7 @@
 }
 
 /** A thread safe implementation of [KeyedObservable]. */
-class KeyedDataObservable<K> : KeyedObservable<K> {
+open class KeyedDataObservable<K> : KeyedObservable<K> {
     // Instead of @GuardedBy("this"), guarded by itself because KeyedDataObservable object could be
     // synchronized outside by the holder
     @GuardedBy("itself") private val observers = WeakHashMap<KeyedObserver<K?>, Executor>()
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/ObservableBackupRestoreStorage.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/ObservableBackupRestoreStorage.kt
index 0e399c0..300d240 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/ObservableBackupRestoreStorage.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/ObservableBackupRestoreStorage.kt
@@ -21,5 +21,7 @@
  *
  * This class provides the [Observable] implementations on top of [DataObservable] by delegation.
  */
-abstract class ObservableBackupRestoreStorage :
-    BackupRestoreStorage(), Observable by DataObservable()
+abstract class ObservableBackupRestoreStorage : BackupRestoreStorage(), ObservableDelegation {
+
+    final override val observableDelegate: Observable = DataObservable(this)
+}
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt
index 98d0f6e..6af3d1c 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt
@@ -32,10 +32,11 @@
      *
      * This callback will run in the given [Executor] when observer is added.
      *
+     * @param observable observable of the change
      * @param reason the reason of change
      * @see [Observable.addObserver] for the notices.
      */
-    fun onChanged(reason: Int)
+    fun onChanged(observable: Observable, reason: Int)
 }
 
 /** An observable object allows to observe change with [Observer]. */
@@ -68,8 +69,21 @@
     fun notifyChange(reason: Int)
 }
 
+/** Delegation of [Observable]. */
+interface ObservableDelegation : Observable {
+    /** [Observable] to delegate. */
+    val observableDelegate: Observable
+
+    override fun addObserver(observer: Observer, executor: Executor) =
+        observableDelegate.addObserver(observer, executor)
+
+    override fun removeObserver(observer: Observer) = observableDelegate.removeObserver(observer)
+
+    override fun notifyChange(reason: Int) = observableDelegate.notifyChange(reason)
+}
+
 /** A thread safe implementation of [Observable]. */
-class DataObservable : Observable {
+class DataObservable(private val observable: Observable) : Observable {
     // Instead of @GuardedBy("this"), guarded by itself because DataObservable object could be
     // synchronized outside by the holder
     @GuardedBy("itself") private val observers = WeakHashMap<Observer, Executor>()
@@ -90,7 +104,7 @@
         val entries = synchronized(observers) { observers.entries.toTypedArray() }
         for (entry in entries) {
             val observer = entry.key // avoid reference "entry"
-            entry.value.execute { observer.onChanged(reason) }
+            entry.value.execute { observer.onChanged(observable, reason) }
         }
     }
 }
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesObservable.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesObservable.kt
new file mode 100644
index 0000000..e70ec5b
--- /dev/null
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesObservable.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.settingslib.datastore
+
+import android.content.SharedPreferences
+
+/** [SharedPreferences] based [KeyedDataObservable]. */
+class SharedPreferencesObservable(private val sharedPreferences: SharedPreferences) :
+    KeyedDataObservable<String>(), AutoCloseable {
+
+    private val listener = createSharedPreferenceListener()
+
+    init {
+        sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
+    }
+
+    override fun close() {
+        sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
+    }
+}
+
+/** Creates [SharedPreferences.OnSharedPreferenceChangeListener] for [KeyedObservable]. */
+internal fun KeyedObservable<String>.createSharedPreferenceListener() =
+    SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+        if (key != null) {
+            notifyChange(key, DataChangeReason.UPDATE)
+        } else {
+            // On Android >= R, SharedPreferences.Editor.clear() will trigger this case
+            notifyChange(DataChangeReason.DELETE)
+        }
+    }
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt
index 20a95d7..0ca91cd 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt
@@ -80,15 +80,7 @@
             return context.getSharedPreferences(intermediateName, Context.MODE_MULTI_PROCESS)
         }
 
-    private val sharedPreferencesListener =
-        SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
-            if (key != null) {
-                notifyChange(key, DataChangeReason.UPDATE)
-            } else {
-                // On Android >= R, SharedPreferences.Editor.clear() will trigger this case
-                notifyChange(DataChangeReason.DELETE)
-            }
-        }
+    private val sharedPreferencesListener = createSharedPreferenceListener()
 
     init {
         // listener is weakly referenced, so unregister is optional
@@ -191,8 +183,7 @@
                 else -> {
                     Log.e(
                         LOG_TAG,
-                        "[$name] $operation $key=$value, unknown type: ${value?.javaClass}"
-                    )
+                        "[$name] $operation $key=$value, unknown type: ${value?.javaClass}")
                 }
             }
         }
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt
index 19c574a..97b473c 100644
--- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt
@@ -159,7 +159,7 @@
 
         verify(keyedObserver).onKeyChanged("key", DataChangeReason.RESTORE)
         verify(anyKeyObserver).onKeyChanged(null, DataChangeReason.RESTORE)
-        verify(observer).onChanged(DataChangeReason.RESTORE)
+        verify(observer).onChanged(fileStorage, DataChangeReason.RESTORE)
         if (isRobolectric()) {
             Shadows.shadowOf(BackupManager(application)).apply {
                 assertThat(isDataChanged).isFalse()
@@ -187,7 +187,7 @@
         }
 
         fileStorage.notifyChange(DataChangeReason.UPDATE)
-        verify(observer).onChanged(DataChangeReason.UPDATE)
+        verify(observer).onChanged(fileStorage, DataChangeReason.UPDATE)
         verify(keyedObserver, never()).onKeyChanged(any(), any())
         verify(anyKeyObserver, never()).onKeyChanged(any(), any())
         reset(observer)
@@ -197,7 +197,7 @@
         }
 
         keyedStorage.notifyChange("key", DataChangeReason.DELETE)
-        verify(observer, never()).onChanged(any())
+        verify(observer, never()).onChanged(any(), any())
         verify(keyedObserver).onKeyChanged("key", DataChangeReason.DELETE)
         verify(anyKeyObserver).onKeyChanged("key", DataChangeReason.DELETE)
         backupManager?.apply {
@@ -209,7 +209,7 @@
         // backup manager is not notified for restore event
         fileStorage.notifyChange(DataChangeReason.RESTORE)
         keyedStorage.notifyChange("key", DataChangeReason.RESTORE)
-        verify(observer).onChanged(DataChangeReason.RESTORE)
+        verify(observer).onChanged(fileStorage, DataChangeReason.RESTORE)
         verify(keyedObserver).onKeyChanged("key", DataChangeReason.RESTORE)
         verify(anyKeyObserver).onKeyChanged("key", DataChangeReason.RESTORE)
         backupManager?.apply {
@@ -225,7 +225,10 @@
     }
 
     private class FileStorage(override val name: String) :
-        BackupRestoreFileStorage(getApplicationContext(), "file"), Observable by DataObservable()
+        BackupRestoreFileStorage(getApplicationContext(), "file"), ObservableDelegation {
+
+        override val observableDelegate: Observable = DataObservable(this)
+    }
 
     private class DummyBackupAgentHelper : BackupAgentHelper() {
         val backupHelpers = mutableMapOf<String, BackupHelper>()
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
index 5d0303c..bd114d1 100644
--- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
@@ -24,6 +24,7 @@
 import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
 import org.mockito.kotlin.reset
@@ -33,10 +34,11 @@
 class ObserverTest {
     private val observer1 = mock<Observer>()
     private val observer2 = mock<Observer>()
+    private val originalObservable = mock<Observable>()
 
     private val executor1: Executor = MoreExecutors.directExecutor()
     private val executor2: Executor = MoreExecutors.newDirectExecutorService()
-    private val observable = DataObservable()
+    private val observable = DataObservable(originalObservable)
 
     @Test
     fun addObserver_sameExecutor() {
@@ -55,7 +57,7 @@
     @Test
     fun addObserver_weaklyReferenced() {
         val counter = AtomicInteger()
-        var observer: Observer? = Observer { counter.incrementAndGet() }
+        var observer: Observer? = Observer { _, _ -> counter.incrementAndGet() }
         observable.addObserver(observer!!, executor1)
 
         observable.notifyChange(DataChangeReason.UPDATE)
@@ -77,21 +79,21 @@
 
         observable.notifyChange(DataChangeReason.DELETE)
 
-        verify(observer1).onChanged(DataChangeReason.DELETE)
-        verify(observer2).onChanged(DataChangeReason.DELETE)
+        verify(observer1).onChanged(originalObservable, DataChangeReason.DELETE)
+        verify(observer2).onChanged(originalObservable, DataChangeReason.DELETE)
 
         reset(observer1, observer2)
         observable.removeObserver(observer2)
 
         observable.notifyChange(DataChangeReason.UPDATE)
-        verify(observer1).onChanged(DataChangeReason.UPDATE)
-        verify(observer2, never()).onChanged(DataChangeReason.UPDATE)
+        verify(observer1).onChanged(originalObservable, DataChangeReason.UPDATE)
+        verify(observer2, never()).onChanged(any(), any())
     }
 
     @Test
     fun notifyChange_addObserverWithinCallback() {
         // ConcurrentModificationException is raised if it is not implemented correctly
-        val observer = Observer { observable.addObserver(observer1, executor1) }
+        val observer = Observer { _, _ -> observable.addObserver(observer1, executor1) }
         observable.addObserver(observer, executor1)
         observable.notifyChange(DataChangeReason.UPDATE)
         observable.removeObserver(observer)
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index ce997bf..5c4cdb2 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1010,8 +1010,8 @@
     <!-- UI debug setting: force allow on external summary [CHAR LIMIT=150] -->
     <string name="force_resizable_activities_summary">Make all activities resizable for multi-window, regardless of manifest values.</string>
 
-    <!-- Title for a toggle that enables support for windows to be in freeform (apps run in resizable windows). [CHAR LIMIT=50] -->
-    <string name="enable_freeform_support">Enable freeform window support</string>
+    <!-- Title for a toggle that enables support for windows to be in freeform. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] -->
+    <string name="enable_freeform_support">Enable freeform windows</string>
 
     <!-- Local (desktop) backup password menu title [CHAR LIMIT=25] -->
     <string name="local_backup_password_title">Desktop backup password</string>
@@ -1164,7 +1164,7 @@
     <!-- [CHAR_LIMIT=40] Label for battery level chart when charging with duration -->
     <string name="power_charging_duration"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="time">%2$s</xliff:g> left until full</string>
     <!-- [CHAR_LIMIT=80] Label for battery level chart when charge been limited -->
-    <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging optimized</string>
+    <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging on hold to protect battery</string>
     <!-- [CHAR_LIMIT=80] Label for battery charging future pause -->
     <string name="power_charging_future_paused"><xliff:g id="level">%1$s</xliff:g> - Charging</string>
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt
new file mode 100644
index 0000000..db78280
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingModel.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings.shared.model
+
+import android.content.Intent
+import android.graphics.Bitmap
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
+
+/** Models a device setting. */
+sealed interface DeviceSettingModel {
+    val cachedDevice: CachedBluetoothDevice
+    @DeviceSettingId val id: Int
+
+    /** Models a device setting which should be displayed as an action/switch preference. */
+    data class ActionSwitchPreference(
+        override val cachedDevice: CachedBluetoothDevice,
+        @DeviceSettingId override val id: Int,
+        val title: String,
+        val summary: String? = null,
+        val icon: Bitmap? = null,
+        val intent: Intent? = null,
+        val switchState: DeviceSettingStateModel.ActionSwitchPreferenceState? = null,
+        val isAllowedChangingState: Boolean = true,
+        val updateState: ((DeviceSettingStateModel.ActionSwitchPreferenceState) -> Unit)? = null,
+    ) : DeviceSettingModel
+
+    /** Models a device setting which should be displayed as a multi-toggle preference. */
+    data class MultiTogglePreference(
+        override val cachedDevice: CachedBluetoothDevice,
+        @DeviceSettingId override val id: Int,
+        val title: String,
+        val toggles: List<ToggleModel>,
+        val isActive: Boolean,
+        val state: DeviceSettingStateModel.MultiTogglePreferenceState,
+        val isAllowedChangingState: Boolean,
+        val updateState: (DeviceSettingStateModel.MultiTogglePreferenceState) -> Unit
+    ) : DeviceSettingModel
+
+    /** Models an unknown preference. */
+    data class Unknown(
+        override val cachedDevice: CachedBluetoothDevice,
+        @DeviceSettingId override val id: Int
+    ) : DeviceSettingModel
+}
+
+/** Models a toggle in [DeviceSettingModel.MultiTogglePreference]. */
+data class ToggleModel(val label: String, val icon: Bitmap)
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingStateModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingStateModel.kt
new file mode 100644
index 0000000..b404bb9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingStateModel.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings.shared.model
+
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState
+
+/** Models a device setting state. */
+sealed interface DeviceSettingStateModel {
+    fun toParcelable(): DeviceSettingPreferenceState
+
+    /** Models a device setting state for action/switch preference. */
+    data class ActionSwitchPreferenceState(val checked: Boolean) : DeviceSettingStateModel {
+        override fun toParcelable() =
+            com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreferenceState.Builder()
+                .setChecked(checked)
+                .build()
+    }
+
+    /** Models a device setting state for multi-toggle preference. */
+    data class MultiTogglePreferenceState(val selectedIndex: Int) : DeviceSettingStateModel {
+        override fun toParcelable() =
+            com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState.Builder()
+                .setState(selectedIndex)
+                .build()
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
index 87ab6b3..e5d79a1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
@@ -40,6 +40,8 @@
     override val modes: Flow<List<ZenMode>>
         get() = mutableModesFlow.asStateFlow()
 
+    private val activeModesDurations = mutableMapOf<String, Duration?>()
+
     init {
         updateNotificationPolicy()
     }
@@ -64,8 +66,22 @@
         mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id }
     }
 
+    fun getMode(id: String): ZenMode? {
+        return mutableModesFlow.value.find { it.id == id }
+    }
+
     override fun activateMode(zenMode: ZenMode, duration: Duration?) {
         activateMode(zenMode.id)
+        activeModesDurations[zenMode.id] = duration
+    }
+
+    fun getModeActiveDuration(id: String): Duration? {
+        if (!activeModesDurations.containsKey(id)) {
+            throw IllegalArgumentException(
+                "mode $id not manually activated, you need to call activateMode"
+            )
+        }
+        return activeModesDurations[id]
     }
 
     override fun deactivateMode(zenMode: ZenMode) {
@@ -78,6 +94,7 @@
 
     fun deactivateMode(id: String) {
         updateModeActiveState(id = id, isActive = false)
+        activeModesDurations.remove(id)
     }
 
     // Update the active state while maintaining the mode's position in the list
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
index 990a2d4..88af7ee 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -16,6 +16,8 @@
 
 package com.android.settingslib.notification.modes;
 
+import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
+import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
@@ -156,6 +158,11 @@
         mIsManualDnd = isManualDnd;
     }
 
+    /** Creates a deep copy of this object. */
+    public ZenMode copy() {
+        return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mStatus, mIsManualDnd);
+    }
+
     @NonNull
     public String getId() {
         return mId;
@@ -298,6 +305,17 @@
         return mIsManualDnd;
     }
 
+    /**
+     * A <em>custom manual</em> mode is a mode created by the user, and not yet assigned an
+     * automatic trigger condition (neither time schedule nor a calendar).
+     */
+    public boolean isCustomManual() {
+        return isSystemOwned()
+                && getType() != TYPE_SCHEDULE_TIME
+                && getType() != TYPE_SCHEDULE_CALENDAR
+                && !isManualDnd();
+    }
+
     public boolean isEnabled() {
         return mRule.isEnabled();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt b/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt
index d69c87b..2dc2650 100644
--- a/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/satellite/SatelliteDialogUtils.kt
@@ -21,6 +21,7 @@
 import android.content.Intent
 import android.os.OutcomeReceiver
 import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteModemStateCallback
 import android.util.Log
 import android.view.WindowManager
 import androidx.lifecycle.LifecycleOwner
@@ -31,12 +32,19 @@
 import kotlinx.coroutines.Dispatchers.Default
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 import java.util.concurrent.ExecutionException
 import java.util.concurrent.TimeoutException
 import kotlin.coroutines.resume
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOn
 
 /** A util for Satellite dialog */
 object SatelliteDialogUtils {
@@ -70,7 +78,7 @@
             coroutineScope.launch {
                 var isSatelliteModeOn = false
                 try {
-                    isSatelliteModeOn = requestIsEnabled(context)
+                    isSatelliteModeOn = requestIsSessionStarted(context)
                 } catch (e: InterruptedException) {
                     Log.w(TAG, "Error to get satellite status : $e")
                 } catch (e: ExecutionException) {
@@ -134,6 +142,70 @@
         }
     }
 
+    private suspend fun requestIsSessionStarted(
+            context: Context
+    ): Boolean = withContext(Default) {
+        getIsSessionStartedFlow(context).conflate().first()
+    }
+
+    /**
+     * Provides a Flow that emits the session state of the satellite modem. Updates are triggered
+     * when the modem state changes.
+     *
+     * @param defaultDispatcher The CoroutineDispatcher to use (Defaults to `Dispatchers.Default`).
+     * @return A Flow emitting `true` when the session is started and `false` otherwise.
+     */
+    private fun getIsSessionStartedFlow(
+            context: Context
+    ): Flow<Boolean> {
+        val satelliteManager: SatelliteManager? =
+                context.getSystemService(SatelliteManager::class.java)
+        if (satelliteManager == null) {
+            Log.w(TAG, "SatelliteManager is null")
+            return flowOf(false)
+        }
+
+        return callbackFlow {
+            val callback = SatelliteModemStateCallback { state ->
+                val isSessionStarted = isSatelliteSessionStarted(state)
+                Log.i(TAG, "Satellite modem state changed: state=$state"
+                        + ", isSessionStarted=$isSessionStarted")
+                trySend(isSessionStarted)
+            }
+
+            val registerResult = satelliteManager.registerForModemStateChanged(
+                    Default.asExecutor(),
+                    callback
+            )
+
+            if (registerResult != SatelliteManager.SATELLITE_RESULT_SUCCESS) {
+                // If the registration failed (e.g., device doesn't support satellite),
+                // SatelliteManager will not emit the current state by callback.
+                // We send `false` value by ourself to make sure the flow has initial value.
+                Log.w(TAG, "Failed to register for satellite modem state change: $registerResult")
+                trySend(false)
+            }
+
+            awaitClose { satelliteManager.unregisterForModemStateChanged(callback) }
+        }.flowOn(Default)
+    }
+
+
+    /**
+     * Check if the modem is in a satellite session.
+     *
+     * @param state The SatelliteModemState provided by the SatelliteManager.
+     * @return `true` if the modem is in a satellite session, `false` otherwise.
+     */
+    fun isSatelliteSessionStarted(@SatelliteManager.SatelliteModemState state: Int): Boolean {
+        return when (state) {
+            SatelliteManager.SATELLITE_MODEM_STATE_OFF,
+            SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE,
+            SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN -> false
+            else -> true
+        }
+    }
+
     const val TAG = "SatelliteDialogUtils"
 
     const val EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG: String =
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
index e705f97..651e57c 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -27,6 +27,7 @@
 import android.net.Uri;
 import android.os.Parcel;
 import android.service.notification.Condition;
+import android.service.notification.SystemZenRules;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenPolicy;
 
@@ -109,6 +110,61 @@
     }
 
     @Test
+    public void isCustomManual_customManualMode() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .setType(AutomaticZenRule.TYPE_OTHER)
+                .build();
+        ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false));
+
+        assertThat(mode.isCustomManual()).isTrue();
+    }
+
+    @Test
+    public void isCustomManual_scheduleTime_false() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .setType(AutomaticZenRule.TYPE_SCHEDULE_TIME)
+                .build();
+        ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false));
+
+        assertThat(mode.isCustomManual()).isFalse();
+    }
+
+    @Test
+    public void isCustomManual_scheduleCalendar_false() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR)
+                .build();
+        ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false));
+
+        assertThat(mode.isCustomManual()).isFalse();
+    }
+
+    @Test
+    public void isCustomManual_appProvidedMode_false() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+                .setPackage("com.some.package")
+                .setType(AutomaticZenRule.TYPE_OTHER)
+                .build();
+        ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false));
+
+        assertThat(mode.isCustomManual()).isFalse();
+    }
+
+    @Test
+    public void isCustomManual_manualDnd_false() {
+        AutomaticZenRule dndRule = new AutomaticZenRule.Builder("Mode", Uri.parse("x"))
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .setType(AutomaticZenRule.TYPE_OTHER)
+                .build();
+        ZenMode mode = ZenMode.manualDndMode(dndRule, false);
+
+        assertThat(mode.isCustomManual()).isFalse();
+    }
+
+    @Test
     public void getPolicy_interruptionFilterPriority_returnsZenPolicy() {
         AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                 .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt
index aeda1ed6..31d7130 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/satellite/SatelliteDialogUtilsTest.kt
@@ -17,11 +17,12 @@
 package com.android.settingslib.satellite
 
 import android.content.Context
-import android.content.Intent
-import android.os.OutcomeReceiver
 import android.platform.test.annotations.RequiresFlagsEnabled
 import android.telephony.satellite.SatelliteManager
-import android.telephony.satellite.SatelliteManager.SatelliteException
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_ENABLING_SATELLITE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
+import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_MODEM_ERROR
+import android.telephony.satellite.SatelliteModemStateCallback
 import android.util.AndroidRuntimeException
 import androidx.test.core.app.ApplicationProvider
 import com.android.internal.telephony.flags.Flags
@@ -67,26 +68,21 @@
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun mayStartSatelliteWarningDialog_satelliteIsOn_showWarningDialog() = runBlocking {
-        `when`(
-                satelliteManager.requestIsEnabled(
-                        any(), any<OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>()
-                )
-        )
+        `when`(satelliteManager.registerForModemStateChanged(any(), any()))
                 .thenAnswer { invocation ->
-                    val receiver = invocation
-                            .getArgument<
-                                    OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>(
+                    val callback = invocation
+                            .getArgument<SatelliteModemStateCallback>(
                                     1
                             )
-                    receiver.onResult(true)
+                    callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_ENABLING_SATELLITE)
                     null
                 }
 
         try {
             SatelliteDialogUtils.mayStartSatelliteWarningDialog(
                     context, coroutineScope, TYPE_IS_WIFI, allowClick = {
-                        assertTrue(it)
-                })
+                assertTrue(it)
+            })
         } catch (e: AndroidRuntimeException) {
             // Catch exception of starting activity .
         }
@@ -95,68 +91,49 @@
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun mayStartSatelliteWarningDialog_satelliteIsOff_notShowWarningDialog() = runBlocking {
-        `when`(
-                satelliteManager.requestIsEnabled(
-                        any(), any<OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>()
-                )
-        )
+        `when`(satelliteManager.registerForModemStateChanged(any(), any()))
                 .thenAnswer { invocation ->
-                    val receiver = invocation
-                            .getArgument<
-                                    OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>(
+                    val callback = invocation
+                            .getArgument<SatelliteModemStateCallback>(
                                     1
                             )
-                    receiver.onResult(false)
+                    callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_OFF)
                     null
                 }
 
 
         SatelliteDialogUtils.mayStartSatelliteWarningDialog(
-            context, coroutineScope, TYPE_IS_WIFI, allowClick = {
-                assertFalse(it)
-            })
+                context, coroutineScope, TYPE_IS_WIFI, allowClick = {
+            assertFalse(it)
+        })
 
-        verify(context, Times(0)).startActivity(any<Intent>())
+        verify(context, Times(0)).startActivity(any())
     }
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun mayStartSatelliteWarningDialog_noSatelliteManager_notShowWarningDialog() = runBlocking {
-        `when`(context.getSystemService(SatelliteManager::class.java))
-                .thenReturn(null)
+        `when`(context.getSystemService(SatelliteManager::class.java)).thenReturn(null)
 
         SatelliteDialogUtils.mayStartSatelliteWarningDialog(
-            context, coroutineScope, TYPE_IS_WIFI, allowClick = {
-                assertFalse(it)
-            })
+                context, coroutineScope, TYPE_IS_WIFI, allowClick = {
+            assertFalse(it)
+        })
 
-        verify(context, Times(0)).startActivity(any<Intent>())
+        verify(context, Times(0)).startActivity(any())
     }
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun mayStartSatelliteWarningDialog_satelliteErrorResult_notShowWarningDialog() = runBlocking {
-        `when`(
-                satelliteManager.requestIsEnabled(
-                        any(), any<OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>()
-                )
-        )
-                .thenAnswer { invocation ->
-                    val receiver = invocation
-                            .getArgument<
-                                    OutcomeReceiver<Boolean, SatelliteManager.SatelliteException>>(
-                                    1
-                            )
-                    receiver.onError(SatelliteException(SatelliteManager.SATELLITE_RESULT_ERROR))
-                    null
-                }
-
+        `when`(satelliteManager.registerForModemStateChanged(any(), any()))
+                .thenReturn(SATELLITE_RESULT_MODEM_ERROR)
 
         SatelliteDialogUtils.mayStartSatelliteWarningDialog(
-            context, coroutineScope, TYPE_IS_WIFI, allowClick = {
-                assertFalse(it)
-            })
+                context, coroutineScope, TYPE_IS_WIFI, allowClick = {
+            assertFalse(it)
+        })
 
-        verify(context, Times(0)).startActivity(any<Intent>())
+        verify(context, Times(0)).startActivity(any())
     }
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 2b8b23e..40a8199 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -253,6 +253,7 @@
         Settings.Secure.CUSTOM_BUGREPORT_HANDLER_APP,
         Settings.Secure.CUSTOM_BUGREPORT_HANDLER_USER,
         Settings.Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED,
+        Settings.Secure.HINGE_ANGLE_LIDEVENT_ENABLED,
         Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED,
         Settings.Secure.HEARING_AID_RINGTONE_ROUTING,
         Settings.Secure.HEARING_AID_CALL_ROUTING,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index cc5302b..3b9c683 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -406,6 +406,7 @@
         VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_USER, ANY_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.LOCK_SCREEN_WEATHER_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.HINGE_ANGLE_LIDEVENT_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.HEARING_AID_RINGTONE_ROUTING,
                 new DiscreteValueValidator(new String[] {"0", "1", "2"}));
         VALIDATORS.put(Secure.HEARING_AID_CALL_ROUTING,
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 666d939..9f3c2bf 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -482,7 +482,7 @@
             android:exported="true"
             android:theme="@style/Theme.AppCompat.NoActionBar">
             <intent-filter>
-                <action android:name="com.android.systemui.action.TOUCHPAD_TUTORIAL"/>
+                <action android:name="com.android.systemui.action.TOUCHPAD_KEYBOARD_TUTORIAL"/>
                 <category android:name="android.intent.category.DEFAULT"/>
             </intent-filter>
         </activity>
@@ -1062,6 +1062,14 @@
             </intent-filter>
         </receiver>
 
+        <receiver android:name=".accessibility.extradim.ExtraDimDialogReceiver"
+            android:singleUser="true"
+            android:exported="false">
+            <intent-filter android:priority="1">
+                <action android:name="com.android.systemui.action.LAUNCH_REMOVE_EXTRA_DIM_DIALOG" />
+            </intent-filter>
+        </receiver>
+
         <activity android:name=".logcat.LogAccessDialogActivity"
                   android:theme="@android:style/Theme.Translucent.NoTitleBar"
                   android:excludeFromRecents="true"
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 0861454..8860452 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -90,6 +90,16 @@
 }
 
 flag {
+    name: "update_corner_radius_on_display_changed"
+    namespace: "accessibility"
+    description: "Updates the corner radius to the magnification fullscreen border when the display changes."
+    bug: "335113174"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "hearing_devices_dialog_related_tools"
     namespace: "accessibility"
     description: "Shows the related tools for hearing devices dialog."
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 27f4305..0d337eb 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1005,6 +1005,16 @@
 }
 
 flag {
+  name: "communal_timer_flicker_fix"
+  namespace: "systemui"
+  description: "fixes timers on the hub flickering when pausing"
+  bug: "353801573"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "app_clips_backlinks"
   namespace: "systemui"
   description: "Enables Backlinks improvement feature in App Clips"
@@ -1253,6 +1263,16 @@
 }
 
 flag {
+   name: "use_transitions_for_keyguard_occluded"
+   namespace: "systemui"
+   description: "Use Keyguard Transitions to set Notification Shade occlusion state"
+   bug: "344716537"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
    name: "lockscreen_preview_renderer_create_on_main_thread"
    namespace: "systemui"
    description: "Force preview renderer to be created on the main thread"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 8245cc5..368085f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -7,6 +7,7 @@
 import androidx.compose.animation.core.rememberInfiniteTransition
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.layout.Box
@@ -25,9 +26,13 @@
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
@@ -41,6 +46,7 @@
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.compose.animation.scene.transitions
 import com.android.compose.theme.LocalAndroidColorScheme
+import com.android.internal.R.attr.focusable
 import com.android.systemui.Flags.glanceableHubBackGesture
 import com.android.systemui.communal.shared.model.CommunalBackgroundType
 import com.android.systemui.communal.shared.model.CommunalScenes
@@ -65,7 +71,7 @@
 }
 
 object AllElements : ElementMatcher {
-    override fun matches(key: ElementKey, scene: SceneKey) = true
+    override fun matches(key: ElementKey, content: ContentKey) = true
 }
 
 private object TransitionDuration {
@@ -207,6 +213,8 @@
                 backgroundType = backgroundType,
                 colors = colors,
                 content = content,
+                viewModel = viewModel,
+                modifier = Modifier.horizontalNestedScrollToScene(),
             )
         }
     }
@@ -222,17 +230,41 @@
     backgroundType: CommunalBackgroundType,
     colors: CommunalColors,
     content: CommunalContent,
+    viewModel: CommunalViewModel,
     modifier: Modifier = Modifier,
 ) {
-    Box(modifier = Modifier.element(Communal.Elements.Scrim).fillMaxSize()) {
+    val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false)
+
+    Box(
+        modifier =
+            Modifier.element(Communal.Elements.Scrim)
+                .fillMaxSize()
+                .then(
+                    if (isFocusable) {
+                        Modifier.focusable()
+                    } else {
+                        Modifier.semantics { contentDescription = "" }.clearAndSetSemantics {}
+                    }
+                )
+    ) {
         when (backgroundType) {
             CommunalBackgroundType.STATIC -> DefaultBackground(colors = colors)
             CommunalBackgroundType.STATIC_GRADIENT -> StaticLinearGradient()
             CommunalBackgroundType.ANIMATED -> AnimatedLinearGradient()
             CommunalBackgroundType.NONE -> BackgroundTopScrim()
         }
+
+        with(content) {
+            Content(
+                modifier =
+                    modifier.focusable(isFocusable).semantics {
+                        if (!isFocusable) {
+                            contentDescription = ""
+                        }
+                    }
+            )
+        }
     }
-    with(content) { Content(modifier = modifier) }
 }
 
 /** Default background of the hub, a single color */
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index e29e0fd..69f1174 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -152,6 +152,7 @@
 import androidx.compose.ui.unit.times
 import androidx.compose.ui.util.fastAll
 import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.viewinterop.NoOpUpdate
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.window.layout.WindowMetricsCalculator
 import com.android.compose.animation.Easings.Emphasized
@@ -159,6 +160,7 @@
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.compose.ui.graphics.painter.rememberDrawablePainter
 import com.android.internal.R.dimen.system_app_widget_background_radius
+import com.android.systemui.Flags.communalTimerFlickerFix
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.shared.model.CommunalScenes
@@ -221,7 +223,7 @@
     val layoutDirection = LocalLayoutDirection.current
 
     if (viewModel.isEditMode) {
-        ScrollOnNewWidgetAddedEffect(communalContent, gridState)
+        ObserveNewWidgetAddedEffect(communalContent, gridState, viewModel)
     } else {
         ScrollOnUpdatedLiveContentEffect(communalContent, gridState)
     }
@@ -242,48 +244,59 @@
                 .semantics { testTagsAsResourceId = true }
                 .testTag(COMMUNAL_HUB_TEST_TAG)
                 .fillMaxSize()
-                .nestedScroll(nestedScrollConnection)
-                .pointerInput(layoutDirection, gridState, contentOffset, contentListState) {
-                    awaitPointerEventScope {
-                        while (true) {
-                            var event = awaitFirstDown(requireUnconsumed = false)
-                            // Reset touch on first event.
-                            viewModel.onResetTouchState()
-
-                            // Process down event in case it's consumed immediately
-                            if (event.isConsumed) {
-                                viewModel.onHubTouchConsumed()
-                            }
-
-                            do {
-                                var event = awaitPointerEvent()
-                                for (change in event.changes) {
-                                    if (change.isConsumed) {
-                                        // Signal touch consumption on any consumed event.
-                                        viewModel.onHubTouchConsumed()
-                                    }
-                                }
-                            } while (
-                                !event.changes.fastAll {
-                                    it.changedToUp() || it.changedToUpIgnoreConsumed()
-                                }
-                            )
+                // Observe taps for selecting items
+                .thenIf(viewModel.isEditMode) {
+                    Modifier.pointerInput(
+                        layoutDirection,
+                        gridState,
+                        contentOffset,
+                        contentListState,
+                    ) {
+                        observeTaps { offset ->
+                            // if RTL, flip offset direction from Left side to Right
+                            val adjustedOffset =
+                                Offset(
+                                    if (layoutDirection == LayoutDirection.Rtl)
+                                        screenWidth - offset.x
+                                    else offset.x,
+                                    offset.y
+                                ) - contentOffset
+                            val index = firstIndexAtOffset(gridState, adjustedOffset)
+                            val key =
+                                index?.let { keyAtIndexIfEditable(contentListState.list, index) }
+                            viewModel.setSelectedKey(key)
                         }
                     }
+                }
+                // Nested scroll for full screen swipe to get to shade and bouncer
+                .thenIf(!viewModel.isEditMode) {
+                    Modifier.nestedScroll(nestedScrollConnection).pointerInput(viewModel) {
+                        awaitPointerEventScope {
+                            while (true) {
+                                val firstDownEvent = awaitFirstDown(requireUnconsumed = false)
+                                // Reset touch on first event.
+                                viewModel.onResetTouchState()
 
-                    // If not in edit mode, don't allow selecting items.
-                    if (!viewModel.isEditMode) return@pointerInput
-                    observeTaps { offset ->
-                        // if RTL, flip offset direction from Left side to Right
-                        val adjustedOffset =
-                            Offset(
-                                if (layoutDirection == LayoutDirection.Rtl) screenWidth - offset.x
-                                else offset.x,
-                                offset.y
-                            ) - contentOffset
-                        val index = firstIndexAtOffset(gridState, adjustedOffset)
-                        val key = index?.let { keyAtIndexIfEditable(contentListState.list, index) }
-                        viewModel.setSelectedKey(key)
+                                // Process down event in case it's consumed immediately
+                                if (firstDownEvent.isConsumed) {
+                                    viewModel.onHubTouchConsumed()
+                                }
+
+                                do {
+                                    val event = awaitPointerEvent()
+                                    for (change in event.changes) {
+                                        if (change.isConsumed) {
+                                            // Signal touch consumption on any consumed event.
+                                            viewModel.onHubTouchConsumed()
+                                        }
+                                    }
+                                } while (
+                                    !event.changes.fastAll {
+                                        it.changedToUp() || it.changedToUpIgnoreConsumed()
+                                    }
+                                )
+                            }
+                        }
                     }
                 }
                 .thenIf(!viewModel.isEditMode && !isEmptyState) {
@@ -291,7 +304,7 @@
                         gridState,
                         contentOffset,
                         communalContent,
-                        gridCoordinates
+                        gridCoordinates,
                     ) {
                         detectLongPressGesture { offset ->
                             // Deduct both grid offset relative to its container and content
@@ -553,19 +566,37 @@
     }
 }
 
-/** Observes communal content and scrolls to a newly added widget if any. */
+/**
+ * Observes communal content and determines whether a new widget has been added, upon which case:
+ * - Announce for accessibility
+ * - Scroll if the new widget is not visible
+ */
 @Composable
-private fun ScrollOnNewWidgetAddedEffect(
+private fun ObserveNewWidgetAddedEffect(
     communalContent: List<CommunalContentModel>,
     gridState: LazyGridState,
+    viewModel: BaseCommunalViewModel,
 ) {
     val coroutineScope = rememberCoroutineScope()
     val widgetKeys = remember { mutableListOf<String>() }
+    var communalContentPending by remember { mutableStateOf(true) }
 
     LaunchedEffect(communalContent) {
+        // Do nothing until any communal content comes in
+        if (communalContentPending && communalContent.isEmpty()) {
+            return@LaunchedEffect
+        }
+
         val oldWidgetKeys = widgetKeys.toList()
+        val widgets = communalContent.filterIsInstance<CommunalContentModel.WidgetContent.Widget>()
         widgetKeys.clear()
-        widgetKeys.addAll(communalContent.filter { it.isWidgetContent() }.map { it.key })
+        widgetKeys.addAll(widgets.map { it.key })
+
+        // Do nothing on first communal content since we don't have a delta
+        if (communalContentPending) {
+            communalContentPending = false
+            return@LaunchedEffect
+        }
 
         // Do nothing if there is no new widget
         val indexOfFirstNewWidget = widgetKeys.indexOfFirst { !oldWidgetKeys.contains(it) }
@@ -573,6 +604,8 @@
             return@LaunchedEffect
         }
 
+        viewModel.onNewWidgetAdded(widgets[indexOfFirstNewWidget].providerInfo)
+
         // Scroll if the new widget is not visible
         val lastVisibleItemIndex = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
         if (lastVisibleItemIndex != null && indexOfFirstNewWidget > lastVisibleItemIndex) {
@@ -1336,9 +1369,15 @@
         factory = { context ->
             SmartspaceAppWidgetHostView(context).apply {
                 interactionHandler?.let { setInteractionHandler(it) }
-                updateAppWidget(model.remoteViews)
+                if (!communalTimerFlickerFix()) {
+                    updateAppWidget(model.remoteViews)
+                }
             }
         },
+        update =
+            if (communalTimerFlickerFix()) {
+                { view: SmartspaceAppWidgetHostView -> view.updateAppWidget(model.remoteViews) }
+            } else NoOpUpdate,
         // For reusing composition in lazy lists.
         onReset = {},
     )
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 859c036..df068c4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -92,7 +92,7 @@
     fun SceneScope.Notifications(burnInParams: BurnInParameters?, modifier: Modifier = Modifier) {
         val areNotificationsVisible by
             lockscreenContentViewModel
-                .areNotificationsVisible(sceneKey)
+                .areNotificationsVisible(contentKey)
                 .collectAsStateWithLifecycle(initialValue = false)
         if (!areNotificationsVisible) {
             return
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
new file mode 100644
index 0000000..4b3a39b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.notifications.ui.composable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.IntOffset
+import com.android.compose.nestedscroll.PriorityNestedScrollConnection
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun Modifier.stackVerticalOverscroll(
+    coroutineScope: CoroutineScope,
+    canScrollForward: () -> Boolean
+): Modifier {
+    val overscrollOffset = remember { Animatable(0f) }
+    val stackNestedScrollConnection = remember {
+        NotificationStackNestedScrollConnection(
+            stackOffset = { overscrollOffset.value },
+            canScrollForward = canScrollForward,
+            onScroll = { offsetAvailable ->
+                coroutineScope.launch {
+                    overscrollOffset.snapTo(overscrollOffset.value + offsetAvailable * 0.3f)
+                }
+            },
+            onStop = { velocityAvailable ->
+                coroutineScope.launch {
+                    overscrollOffset.animateTo(
+                        targetValue = 0f,
+                        initialVelocity = velocityAvailable,
+                        animationSpec = tween()
+                    )
+                }
+            }
+        )
+    }
+
+    return this.then(
+        Modifier.nestedScroll(stackNestedScrollConnection).offset {
+            IntOffset(x = 0, y = overscrollOffset.value.roundToInt())
+        }
+    )
+}
+
+fun NotificationStackNestedScrollConnection(
+    stackOffset: () -> Float,
+    canScrollForward: () -> Boolean,
+    onStart: (Float) -> Unit = {},
+    onScroll: (Float) -> Unit,
+    onStop: (Float) -> Unit = {},
+): PriorityNestedScrollConnection {
+    return PriorityNestedScrollConnection(
+        orientation = Orientation.Vertical,
+        canStartPreScroll = { _, _ -> false },
+        canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
+            offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward()
+        },
+        canStartPostFling = { velocityAvailable -> velocityAvailable < 0f && !canScrollForward() },
+        canContinueScroll = { source ->
+            if (source == NestedScrollSource.SideEffect) {
+                stackOffset() > STACK_OVERSCROLL_FLING_MIN_OFFSET
+            } else {
+                true
+            }
+        },
+        canScrollOnFling = true,
+        onStart = { offsetAvailable -> onStart(offsetAvailable) },
+        onScroll = { offsetAvailable ->
+            onScroll(offsetAvailable)
+            offsetAvailable
+        },
+        onStop = { velocityAvailable ->
+            onStop(velocityAvailable)
+            velocityAvailable
+        },
+    )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 76a7a10..2eb7b3f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -474,6 +474,7 @@
                         .thenIf(shadeMode == ShadeMode.Single) {
                             Modifier.nestedScroll(scrimNestedScrollConnection)
                         }
+                        .stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward }
                         .verticalScroll(scrollState)
                         .padding(top = topPadding)
                         .fillMaxWidth()
@@ -671,3 +672,4 @@
 private val DEBUG_BOX_COLOR = Color(0f, 1f, 0f, 0.2f)
 private const val HUN_SNOOZE_POSITIONAL_THRESHOLD_FRACTION = 0.25f
 private const val HUN_SNOOZE_VELOCITY_THRESHOLD = -70f
+internal const val STACK_OVERSCROLL_FLING_MIN_OFFSET = -100f
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 114dcf4..afbc8e7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -67,15 +67,15 @@
 /**
  * Animate a scene Int value.
  *
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
  */
 @Composable
-fun SceneScope.animateSceneIntAsState(
+fun ContentScope.animateContentIntAsState(
     value: Int,
     key: ValueKey,
     canOverflow: Boolean = true,
 ): AnimatedState<Int> {
-    return animateSceneValueAsState(value, key, SharedIntType, canOverflow)
+    return animateContentValueAsState(value, key, SharedIntType, canOverflow)
 }
 
 /**
@@ -107,17 +107,28 @@
 /**
  * Animate a scene Float value.
  *
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
  */
 @Composable
-fun SceneScope.animateSceneFloatAsState(
+fun ContentScope.animateContentFloatAsState(
     value: Float,
     key: ValueKey,
     canOverflow: Boolean = true,
 ): AnimatedState<Float> {
-    return animateSceneValueAsState(value, key, SharedFloatType, canOverflow)
+    return animateContentValueAsState(value, key, SharedFloatType, canOverflow)
 }
 
+@Deprecated(
+    "Use animateSceneFloatAsState() instead",
+    replaceWith = ReplaceWith("animateContentFloatAsState(value, key, canOverflow)")
+)
+@Composable
+fun ContentScope.animateSceneFloatAsState(
+    value: Float,
+    key: ValueKey,
+    canOverflow: Boolean = true,
+) = animateContentFloatAsState(value, key, canOverflow)
+
 /**
  * Animate a shared element Float value.
  *
@@ -147,17 +158,28 @@
 /**
  * Animate a scene Dp value.
  *
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
  */
 @Composable
-fun SceneScope.animateSceneDpAsState(
+fun ContentScope.animateContentDpAsState(
     value: Dp,
     key: ValueKey,
     canOverflow: Boolean = true,
 ): AnimatedState<Dp> {
-    return animateSceneValueAsState(value, key, SharedDpType, canOverflow)
+    return animateContentValueAsState(value, key, SharedDpType, canOverflow)
 }
 
+@Deprecated(
+    "Use animateSceneDpAsState() instead",
+    replaceWith = ReplaceWith("animateContentDpAsState(value, key, canOverflow)")
+)
+@Composable
+fun ContentScope.animateSceneDpAsState(
+    value: Dp,
+    key: ValueKey,
+    canOverflow: Boolean = true,
+) = animateContentDpAsState(value, key, canOverflow)
+
 /**
  * Animate a shared element Dp value.
  *
@@ -188,14 +210,14 @@
 /**
  * Animate a scene Color value.
  *
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
  */
 @Composable
-fun SceneScope.animateSceneColorAsState(
+fun ContentScope.animateContentColorAsState(
     value: Color,
     key: ValueKey,
 ): AnimatedState<Color> {
-    return animateSceneValueAsState(value, key, SharedColorType, canOverflow = false)
+    return animateContentValueAsState(value, key, SharedColorType, canOverflow = false)
 }
 
 /**
@@ -261,24 +283,24 @@
 @Composable
 internal fun <T> animateSharedValueAsState(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: ElementKey?,
     key: ValueKey,
     value: T,
     type: SharedValueType<T, *>,
     canOverflow: Boolean,
 ): AnimatedState<T> {
-    DisposableEffect(layoutImpl, scene, element, key) {
-        // Create the associated maps that hold the current value for each (element, scene) pair.
+    DisposableEffect(layoutImpl, content, element, key) {
+        // Create the associated maps that hold the current value for each (element, content) pair.
         val valueMap = layoutImpl.sharedValues.getOrPut(key) { mutableMapOf() }
         val sharedValue = valueMap.getOrPut(element) { SharedValue(type) } as SharedValue<T, *>
         val targetValues = sharedValue.targetValues
-        targetValues[scene] = value
+        targetValues[content] = value
 
         onDispose {
             // Remove the value associated to the current scene, and eventually remove the maps if
             // they are empty.
-            targetValues.remove(scene)
+            targetValues.remove(content)
 
             if (targetValues.isEmpty() && valueMap[element] === sharedValue) {
                 valueMap.remove(element)
@@ -297,11 +319,11 @@
             error("value is equal to $value, which is the undefined value for this type.")
         }
 
-        sharedValue<T, Any>(layoutImpl, key, element).targetValues[scene] = value
+        sharedValue<T, Any>(layoutImpl, key, element).targetValues[content] = value
     }
 
-    return remember(layoutImpl, scene, element, canOverflow) {
-        AnimatedStateImpl<T, Any>(layoutImpl, scene, element, key, canOverflow)
+    return remember(layoutImpl, content, element, canOverflow) {
+        AnimatedStateImpl<T, Any>(layoutImpl, content, element, key, canOverflow)
     }
 }
 
@@ -322,8 +344,8 @@
 internal class SharedValue<T, Delta>(
     val type: SharedValueType<T, Delta>,
 ) {
-    /** The target value of this shared value for each scene. */
-    val targetValues = SnapshotStateMap<SceneKey, T>()
+    /** The target value of this shared value for each content. */
+    val targetValues = SnapshotStateMap<ContentKey, T>()
 
     /** The last value of this shared value. */
     var lastValue: T = type.unspecifiedValue
@@ -340,7 +362,7 @@
 
 private class AnimatedStateImpl<T, Delta>(
     private val layoutImpl: SceneTransitionLayoutImpl,
-    private val scene: SceneKey,
+    private val content: ContentKey,
     private val element: ElementKey?,
     private val key: ValueKey,
     private val canOverflow: Boolean,
@@ -356,14 +378,14 @@
                 // TODO(b/311600838): Remove this. We should not have to fallback to the current
                 // scene value, but we have to because code of removed nodes can still run if they
                 // are placed with a graphics layer.
-                ?: sharedValue[scene]
+                ?: sharedValue[content]
                 ?: error(valueReadTooEarlyMessage(key))
         val interruptedValue = computeInterruptedValue(sharedValue, transition, value)
         sharedValue.lastValue = interruptedValue
         return interruptedValue
     }
 
-    private operator fun SharedValue<T, *>.get(scene: SceneKey): T? = targetValues[scene]
+    private operator fun SharedValue<T, *>.get(content: ContentKey): T? = targetValues[content]
 
     private fun valueOrNull(
         sharedValue: SharedValue<T, *>,
@@ -401,7 +423,7 @@
         val targetValues = sharedValue.targetValues
         val transition =
             if (element != null) {
-                layoutImpl.elements[element]?.sceneStates?.let { sceneStates ->
+                layoutImpl.elements[element]?.stateByContent?.let { sceneStates ->
                     layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
                         transition.fromScene in sceneStates || transition.toScene in sceneStates
                     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index fb13b57..67d1b59 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
 import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
+import com.android.compose.animation.scene.content.Scene
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
 import kotlin.math.absoluteValue
 import kotlinx.coroutines.CoroutineScope
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 3ad07d0..0b5e58f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -48,6 +48,7 @@
 import androidx.compose.ui.util.fastCoerceIn
 import androidx.compose.ui.util.fastLastOrNull
 import androidx.compose.ui.util.lerp
+import com.android.compose.animation.scene.content.Content
 import com.android.compose.animation.scene.transformation.PropertyTransformation
 import com.android.compose.animation.scene.transformation.SharedElementTransformation
 import com.android.compose.ui.util.lerp
@@ -57,30 +58,30 @@
 /** An element on screen, that can be composed in one or more scenes. */
 @Stable
 internal class Element(val key: ElementKey) {
-    /** The mapping between a scene and the state this element has in that scene, if any. */
+    /** The mapping between a content and the state this element has in that content, if any. */
     // TODO(b/316901148): Make this a normal map instead once we can make sure that new transitions
     // are first seen by composition then layout/drawing code. See b/316901148#comment2 for details.
-    val sceneStates = SnapshotStateMap<SceneKey, SceneState>()
+    val stateByContent = SnapshotStateMap<ContentKey, State>()
 
     /**
      * The last transition that was used when computing the state (size, position and alpha) of this
-     * element in any scene, or `null` if it was last laid out when idle.
+     * element in any content, or `null` if it was last laid out when idle.
      */
     var lastTransition: TransitionState.Transition? = null
 
-    /** Whether this element was ever drawn in a scene. */
-    var wasDrawnInAnyScene = false
+    /** Whether this element was ever drawn in a content. */
+    var wasDrawnInAnyContent = false
 
     override fun toString(): String {
         return "Element(key=$key)"
     }
 
-    /** The last and target state of this element in a given scene. */
+    /** The last and target state of this element in a given content. */
     @Stable
-    class SceneState(val scene: SceneKey) {
+    class State(val content: ContentKey) {
         /**
-         * The *target* state of this element in this scene, i.e. the state of this element when we
-         * are idle on this scene.
+         * The *target* state of this element in this content, i.e. the state of this element when
+         * we are idle on this content.
          */
         var targetSize by mutableStateOf(SizeUnspecified)
         var targetOffset by mutableStateOf(Offset.Unspecified)
@@ -91,7 +92,9 @@
         var lastScale = Scale.Unspecified
         var lastAlpha = AlphaUnspecified
 
-        /** The state of this element in this scene right before the last interruption (if any). */
+        /**
+         * The state of this element in this content right before the last interruption (if any).
+         */
         var offsetBeforeInterruption = Offset.Unspecified
         var sizeBeforeInterruption = SizeUnspecified
         var scaleBeforeInterruption = Scale.Unspecified
@@ -109,7 +112,7 @@
         var alphaInterruptionDelta = 0f
 
         /**
-         * The attached [ElementNode] a Modifier.element() for a given element and scene. During
+         * The attached [ElementNode] a Modifier.element() for a given element and content. During
          * composition, this set could have 0 to 2 elements. After composition and after all
          * modifier nodes have been attached/detached, this set should contain exactly 1 element.
          */
@@ -130,19 +133,19 @@
     }
 }
 
-/** The implementation of [SceneScope.element]. */
+/** The implementation of [ContentScope.element]. */
 @Stable
 internal fun Modifier.element(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
+    content: Content,
     key: ElementKey,
 ): Modifier {
     // Make sure that we read the current transitions during composition and not during
     // layout/drawing.
     // TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once
-    // we can ensure that SceneTransitionLayoutImpl will compose new scenes first.
+    // we can ensure that SceneTransitionLayoutImpl will compose new contents first.
     val currentTransitions = layoutImpl.state.currentTransitions
-    return then(ElementModifier(layoutImpl, currentTransitions, scene, key)).testTag(key.testTag)
+    return then(ElementModifier(layoutImpl, currentTransitions, content, key)).testTag(key.testTag)
 }
 
 /**
@@ -152,92 +155,92 @@
 private data class ElementModifier(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val currentTransitions: List<TransitionState.Transition>,
-    private val scene: Scene,
+    private val content: Content,
     private val key: ElementKey,
 ) : ModifierNodeElement<ElementNode>() {
-    override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, scene, key)
+    override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, content, key)
 
     override fun update(node: ElementNode) {
-        node.update(layoutImpl, currentTransitions, scene, key)
+        node.update(layoutImpl, currentTransitions, content, key)
     }
 }
 
 internal class ElementNode(
     private var layoutImpl: SceneTransitionLayoutImpl,
     private var currentTransitions: List<TransitionState.Transition>,
-    private var scene: Scene,
+    private var content: Content,
     private var key: ElementKey,
 ) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
     private var _element: Element? = null
     private val element: Element
         get() = _element!!
 
-    private var _sceneState: Element.SceneState? = null
-    private val sceneState: Element.SceneState
-        get() = _sceneState!!
+    private var _stateInContent: Element.State? = null
+    private val stateInContent: Element.State
+        get() = _stateInContent!!
 
     override val traverseKey: Any = ElementTraverseKey
 
     override fun onAttach() {
         super.onAttach()
-        updateElementAndSceneValues()
-        addNodeToSceneState()
+        updateElementAndContentValues()
+        addNodeToContentState()
     }
 
-    private fun updateElementAndSceneValues() {
+    private fun updateElementAndContentValues() {
         val element =
             layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
         _element = element
-        _sceneState =
-            element.sceneStates[scene.key]
-                ?: Element.SceneState(scene.key).also { element.sceneStates[scene.key] = it }
+        _stateInContent =
+            element.stateByContent[content.key]
+                ?: Element.State(content.key).also { element.stateByContent[content.key] = it }
     }
 
-    private fun addNodeToSceneState() {
-        sceneState.nodes.add(this)
+    private fun addNodeToContentState() {
+        stateInContent.nodes.add(this)
 
         coroutineScope.launch {
             // At this point all [CodeLocationNode] have been attached or detached, which means that
-            // [sceneState.codeLocations] should have exactly 1 element, otherwise this means that
-            // this element was composed multiple times in the same scene.
-            val nCodeLocations = sceneState.nodes.size
-            if (nCodeLocations != 1 || !sceneState.nodes.contains(this@ElementNode)) {
-                error("$key was composed $nCodeLocations times in ${sceneState.scene}")
+            // [elementState.codeLocations] should have exactly 1 element, otherwise this means that
+            // this element was composed multiple times in the same content.
+            val nCodeLocations = stateInContent.nodes.size
+            if (nCodeLocations != 1 || !stateInContent.nodes.contains(this@ElementNode)) {
+                error("$key was composed $nCodeLocations times in ${stateInContent.content}")
             }
         }
     }
 
     override fun onDetach() {
         super.onDetach()
-        removeNodeFromSceneState()
-        maybePruneMaps(layoutImpl, element, sceneState)
+        removeNodeFromContentState()
+        maybePruneMaps(layoutImpl, element, stateInContent)
 
         _element = null
-        _sceneState = null
+        _stateInContent = null
     }
 
-    private fun removeNodeFromSceneState() {
-        sceneState.nodes.remove(this)
+    private fun removeNodeFromContentState() {
+        stateInContent.nodes.remove(this)
     }
 
     fun update(
         layoutImpl: SceneTransitionLayoutImpl,
         currentTransitions: List<TransitionState.Transition>,
-        scene: Scene,
+        content: Content,
         key: ElementKey,
     ) {
-        check(layoutImpl == this.layoutImpl && scene == this.scene)
+        check(layoutImpl == this.layoutImpl && content == this.content)
         this.currentTransitions = currentTransitions
 
-        removeNodeFromSceneState()
+        removeNodeFromContentState()
 
         val prevElement = this.element
-        val prevSceneState = this.sceneState
+        val prevElementState = this.stateInContent
         this.key = key
-        updateElementAndSceneValues()
+        updateElementAndContentValues()
 
-        addNodeToSceneState()
-        maybePruneMaps(layoutImpl, prevElement, prevSceneState)
+        addNodeToContentState()
+        maybePruneMaps(layoutImpl, prevElement, prevElementState)
     }
 
     override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
@@ -262,15 +265,15 @@
         check(isLookingAhead)
 
         return measurable.measure(constraints).run {
-            // Update the size this element has in this scene when idle.
-            sceneState.targetSize = size()
+            // Update the size this element has in this content when idle.
+            stateInContent.targetSize = size()
 
             layout(width, height) {
                 // Update the offset (relative to the SceneTransitionLayout) this element has in
-                // this scene when idle.
+                // this content when idle.
                 coordinates?.let { coords ->
                     with(layoutImpl.lookaheadScope) {
-                        sceneState.targetOffset =
+                        stateInContent.targetOffset =
                             lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
                     }
                 }
@@ -287,22 +290,22 @@
         val transition = elementTransition(layoutImpl, element, transitions)
 
         // If this element is not supposed to be laid out now, either because it is not part of any
-        // ongoing transition or the other scene of its transition is overscrolling, then lay out
+        // ongoing transition or the other content of its transition is overscrolling, then lay out
         // the element normally and don't place it.
         val overscrollScene = transition?.currentOverscrollSpec?.scene
-        val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != scene.key
+        val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != content.key
         val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null
         if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) {
             recursivelyClearPlacementValues()
-            sceneState.lastSize = Element.SizeUnspecified
+            stateInContent.lastSize = Element.SizeUnspecified
 
             val placeable = measurable.measure(constraints)
             return layout(placeable.width, placeable.height) { /* Do not place */ }
         }
 
         val placeable =
-            measure(layoutImpl, element, transition, sceneState, measurable, constraints)
-        sceneState.lastSize = placeable.size()
+            measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
+        stateInContent.lastSize = placeable.size()
         return layout(placeable.width, placeable.height) { place(transition, placeable) }
     }
 
@@ -312,12 +315,12 @@
     ) {
         with(layoutImpl.lookaheadScope) {
             // Update the offset (relative to the SceneTransitionLayout) this element has in this
-            // scene when idle.
+            // content when idle.
             val coords =
                 coordinates ?: error("Element ${element.key} does not have any coordinates")
 
-            // No need to place the element in this scene if we don't want to draw it anyways.
-            if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) {
+            // No need to place the element in this content if we don't want to draw it anyways.
+            if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
                 recursivelyClearPlacementValues()
                 return
             }
@@ -326,10 +329,10 @@
             val targetOffset =
                 computeValue(
                     layoutImpl,
-                    sceneState,
+                    stateInContent,
                     element,
                     transition,
-                    sceneValue = { it.targetOffset },
+                    contentValue = { it.targetOffset },
                     transformation = { it.offset },
                     currentValue = { currentOffset },
                     isSpecified = { it != Offset.Unspecified },
@@ -343,17 +346,17 @@
                     value = targetOffset,
                     unspecifiedValue = Offset.Unspecified,
                     zeroValue = Offset.Zero,
-                    getValueBeforeInterruption = { sceneState.offsetBeforeInterruption },
-                    setValueBeforeInterruption = { sceneState.offsetBeforeInterruption = it },
-                    getInterruptionDelta = { sceneState.offsetInterruptionDelta },
+                    getValueBeforeInterruption = { stateInContent.offsetBeforeInterruption },
+                    setValueBeforeInterruption = { stateInContent.offsetBeforeInterruption = it },
+                    getInterruptionDelta = { stateInContent.offsetInterruptionDelta },
                     setInterruptionDelta = { delta ->
                         setPlacementInterruptionDelta(
                             element = element,
-                            sceneState = sceneState,
+                            stateInContent = stateInContent,
                             transition = transition,
                             delta = delta,
-                            setter = { sceneState, delta ->
-                                sceneState.offsetInterruptionDelta = delta
+                            setter = { stateInContent, delta ->
+                                stateInContent.offsetInterruptionDelta = delta
                             },
                         )
                     },
@@ -361,14 +364,15 @@
                     add = { a, b, bProgress -> a + b * bProgress },
                 )
 
-            sceneState.lastOffset = interruptedOffset
+            stateInContent.lastOffset = interruptedOffset
 
             val offset = (interruptedOffset - currentOffset).round()
             if (
-                isElementOpaque(scene, element, transition) &&
-                    interruptedAlpha(layoutImpl, element, transition, sceneState, alpha = 1f) == 1f
+                isElementOpaque(content, element, transition) &&
+                    interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha = 1f) ==
+                        1f
             ) {
-                sceneState.lastAlpha = 1f
+                stateInContent.lastAlpha = 1f
 
                 // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is
                 // not animated once b/305195729 is fixed. Test that drawing is not invalidated in
@@ -387,11 +391,11 @@
                     }
 
                     val transition = elementTransition(layoutImpl, element, currentTransitions)
-                    if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) {
+                    if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
                         return@placeWithLayer
                     }
 
-                    alpha = elementAlpha(layoutImpl, element, transition, sceneState)
+                    alpha = elementAlpha(layoutImpl, element, transition, stateInContent)
                     compositingStrategy = CompositingStrategy.ModulateAlpha
                 }
             }
@@ -404,24 +408,24 @@
      * for the descendants for which approachMeasure() won't be called.
      */
     private fun recursivelyClearPlacementValues() {
-        fun Element.SceneState.clearLastPlacementValues() {
+        fun Element.State.clearLastPlacementValues() {
             lastOffset = Offset.Unspecified
             lastScale = Scale.Unspecified
             lastAlpha = Element.AlphaUnspecified
         }
 
-        sceneState.clearLastPlacementValues()
+        stateInContent.clearLastPlacementValues()
         traverseDescendants(ElementTraverseKey) { node ->
-            (node as ElementNode)._sceneState?.clearLastPlacementValues()
+            (node as ElementNode)._stateInContent?.clearLastPlacementValues()
             TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal
         }
     }
 
     override fun ContentDrawScope.draw() {
-        element.wasDrawnInAnyScene = true
+        element.wasDrawnInAnyContent = true
 
         val transition = elementTransition(layoutImpl, element, currentTransitions)
-        val drawScale = getDrawScale(layoutImpl, element, transition, sceneState)
+        val drawScale = getDrawScale(layoutImpl, element, transition, stateInContent)
         if (drawScale == Scale.Default) {
             drawContent()
         } else {
@@ -441,16 +445,21 @@
         private fun maybePruneMaps(
             layoutImpl: SceneTransitionLayoutImpl,
             element: Element,
-            sceneState: Element.SceneState,
+            stateInContent: Element.State,
         ) {
-            // If element is not composed from this scene anymore, remove the scene values. This
+            // If element is not composed in this content anymore, remove the content values. This
             // works because [onAttach] is called before [onDetach], so if an element is moved from
             // the UI tree we will first add the new code location then remove the old one.
-            if (sceneState.nodes.isEmpty() && element.sceneStates[sceneState.scene] == sceneState) {
-                element.sceneStates.remove(sceneState.scene)
+            if (
+                stateInContent.nodes.isEmpty() &&
+                    element.stateByContent[stateInContent.content] == stateInContent
+            ) {
+                element.stateByContent.remove(stateInContent.content)
 
-                // If the element is not composed in any scene, remove it from the elements map.
-                if (element.sceneStates.isEmpty() && layoutImpl.elements[element.key] == element) {
+                // If the element is not composed in any content, remove it from the elements map.
+                if (
+                    element.stateByContent.isEmpty() && layoutImpl.elements[element.key] == element
+                ) {
                     layoutImpl.elements.remove(element.key)
                 }
             }
@@ -460,7 +469,7 @@
 
 /**
  * The transition that we should consider for [element]. This is the last transition where one of
- * its scenes contains the element.
+ * its contents contains the element.
  */
 private fun elementTransition(
     layoutImpl: SceneTransitionLayoutImpl,
@@ -469,7 +478,8 @@
 ): TransitionState.Transition? {
     val transition =
         transitions.fastLastOrNull { transition ->
-            transition.fromScene in element.sceneStates || transition.toScene in element.sceneStates
+            transition.fromScene in element.stateByContent ||
+                transition.toScene in element.stateByContent
         }
 
     val previousTransition = element.lastTransition
@@ -480,7 +490,7 @@
         prepareInterruption(layoutImpl, element, transition, previousTransition)
     } else if (transition == null && previousTransition != null) {
         // The transition was just finished.
-        element.sceneStates.values.forEach {
+        element.stateByContent.values.forEach {
             it.clearValuesBeforeInterruption()
             it.clearInterruptionDeltas()
         }
@@ -499,32 +509,32 @@
         return
     }
 
-    val sceneStates = element.sceneStates
-    fun updatedSceneState(key: SceneKey): Element.SceneState? {
-        return sceneStates[key]?.also { it.selfUpdateValuesBeforeInterruption() }
+    val stateByContent = element.stateByContent
+    fun updateStateInContent(key: ContentKey): Element.State? {
+        return stateByContent[key]?.also { it.selfUpdateValuesBeforeInterruption() }
     }
 
-    val previousFromState = updatedSceneState(previousTransition.fromScene)
-    val previousToState = updatedSceneState(previousTransition.toScene)
-    val fromState = updatedSceneState(transition.fromScene)
-    val toState = updatedSceneState(transition.toScene)
+    val previousFromState = updateStateInContent(previousTransition.fromScene)
+    val previousToState = updateStateInContent(previousTransition.toScene)
+    val fromState = updateStateInContent(transition.fromScene)
+    val toState = updateStateInContent(transition.toScene)
 
     reconcileStates(element, previousTransition)
     reconcileStates(element, transition)
 
-    // Remove the interruption values to all scenes but the scene(s) where the element will be
+    // Remove the interruption values to all contents but the content(s) where the element will be
     // placed, to make sure that interruption deltas are computed only right after this interruption
     // is prepared.
-    fun cleanInterruptionValues(sceneState: Element.SceneState) {
-        sceneState.sizeInterruptionDelta = IntSize.Zero
-        sceneState.offsetInterruptionDelta = Offset.Zero
-        sceneState.alphaInterruptionDelta = 0f
-        sceneState.scaleInterruptionDelta = Scale.Zero
+    fun cleanInterruptionValues(stateInContent: Element.State) {
+        stateInContent.sizeInterruptionDelta = IntSize.Zero
+        stateInContent.offsetInterruptionDelta = Offset.Zero
+        stateInContent.alphaInterruptionDelta = 0f
+        stateInContent.scaleInterruptionDelta = Scale.Zero
 
-        if (!shouldPlaceElement(layoutImpl, sceneState.scene, element, transition)) {
-            sceneState.offsetBeforeInterruption = Offset.Unspecified
-            sceneState.alphaBeforeInterruption = Element.AlphaUnspecified
-            sceneState.scaleBeforeInterruption = Scale.Unspecified
+        if (!shouldPlaceElement(layoutImpl, stateInContent.content, element, transition)) {
+            stateInContent.offsetBeforeInterruption = Offset.Unspecified
+            stateInContent.alphaBeforeInterruption = Element.AlphaUnspecified
+            stateInContent.scaleBeforeInterruption = Scale.Unspecified
         }
     }
 
@@ -542,8 +552,8 @@
     element: Element,
     transition: TransitionState.Transition,
 ) {
-    val fromSceneState = element.sceneStates[transition.fromScene] ?: return
-    val toSceneState = element.sceneStates[transition.toScene] ?: return
+    val fromSceneState = element.stateByContent[transition.fromScene] ?: return
+    val toSceneState = element.stateByContent[transition.toScene] ?: return
     if (!isSharedElementEnabled(element.key, transition)) {
         return
     }
@@ -563,7 +573,7 @@
     }
 }
 
-private fun Element.SceneState.selfUpdateValuesBeforeInterruption() {
+private fun Element.State.selfUpdateValuesBeforeInterruption() {
     sizeBeforeInterruption = lastSize
 
     if (lastAlpha > 0f) {
@@ -571,7 +581,7 @@
         scaleBeforeInterruption = lastScale
         alphaBeforeInterruption = lastAlpha
     } else {
-        // Consider the element as not placed in this scene if it was fully transparent.
+        // Consider the element as not placed in this content if it was fully transparent.
         // TODO(b/290930950): Look into using derived state inside place() instead to not even place
         // the element at all when alpha == 0f.
         offsetBeforeInterruption = Offset.Unspecified
@@ -580,7 +590,7 @@
     }
 }
 
-private fun Element.SceneState.updateValuesBeforeInterruption(lastState: Element.SceneState) {
+private fun Element.State.updateValuesBeforeInterruption(lastState: Element.State) {
     offsetBeforeInterruption = lastState.offsetBeforeInterruption
     sizeBeforeInterruption = lastState.sizeBeforeInterruption
     scaleBeforeInterruption = lastState.scaleBeforeInterruption
@@ -589,14 +599,14 @@
     clearInterruptionDeltas()
 }
 
-private fun Element.SceneState.clearInterruptionDeltas() {
+private fun Element.State.clearInterruptionDeltas() {
     offsetInterruptionDelta = Offset.Zero
     sizeInterruptionDelta = IntSize.Zero
     scaleInterruptionDelta = Scale.Zero
     alphaInterruptionDelta = 0f
 }
 
-private fun Element.SceneState.clearValuesBeforeInterruption() {
+private fun Element.State.clearValuesBeforeInterruption() {
     offsetBeforeInterruption = Offset.Unspecified
     scaleBeforeInterruption = Scale.Unspecified
     alphaBeforeInterruption = Element.AlphaUnspecified
@@ -655,13 +665,13 @@
  */
 private inline fun <T> setPlacementInterruptionDelta(
     element: Element,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
     transition: TransitionState.Transition?,
     delta: T,
-    setter: (Element.SceneState, T) -> Unit,
+    setter: (Element.State, T) -> Unit,
 ) {
-    // Set the interruption delta on the current scene.
-    setter(sceneState, delta)
+    // Set the interruption delta on the current content.
+    setter(stateInContent, delta)
 
     if (transition == null) {
         return
@@ -670,8 +680,9 @@
     // If the element is shared, also set the delta on the other scene so that it is used by that
     // scene if we start overscrolling it and change the scene where the element is placed.
     val otherScene =
-        if (sceneState.scene == transition.fromScene) transition.toScene else transition.fromScene
-    val otherSceneState = element.sceneStates[otherScene] ?: return
+        if (stateInContent.content == transition.fromScene) transition.toScene
+        else transition.fromScene
+    val otherSceneState = element.stateByContent[otherScene] ?: return
     if (isSharedElementEnabled(element.key, transition)) {
         setter(otherSceneState, delta)
     }
@@ -679,7 +690,7 @@
 
 private fun shouldPlaceElement(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: Element,
     transition: TransitionState.Transition?,
 ): Boolean {
@@ -688,15 +699,16 @@
         return true
     }
 
-    // Don't place the element in this scene if this scene is not part of the current element
+    // Don't place the element in this content if this content is not part of the current element
     // transition.
-    if (scene != transition.fromScene && scene != transition.toScene) {
+    if (content != transition.fromScene && content != transition.toScene) {
         return false
     }
 
     // Place the element if it is not shared.
     if (
-        transition.fromScene !in element.sceneStates || transition.toScene !in element.sceneStates
+        transition.fromScene !in element.stateByContent ||
+            transition.toScene !in element.stateByContent
     ) {
         return true
     }
@@ -708,7 +720,7 @@
 
     return shouldPlaceOrComposeSharedElement(
         layoutImpl,
-        scene,
+        content,
         element.key,
         transition,
     )
@@ -716,14 +728,14 @@
 
 internal fun shouldPlaceOrComposeSharedElement(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: ElementKey,
     transition: TransitionState.Transition,
 ): Boolean {
     // If we are overscrolling, only place/compose the element in the overscrolling scene.
     val overscrollScene = transition.currentOverscrollSpec?.scene
     if (overscrollScene != null) {
-        return scene == overscrollScene
+        return content == overscrollScene
     }
 
     val scenePicker = element.scenePicker
@@ -738,7 +750,7 @@
             toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex,
         ) ?: return false
 
-    return pickedScene == scene
+    return pickedScene == content
 }
 
 private fun isSharedElementEnabled(
@@ -775,7 +787,7 @@
  * placement and we don't want to read the transition progress in that phase.
  */
 private fun isElementOpaque(
-    scene: Scene,
+    content: Content,
     element: Element,
     transition: TransitionState.Transition?,
 ): Boolean {
@@ -785,8 +797,8 @@
 
     val fromScene = transition.fromScene
     val toScene = transition.toScene
-    val fromState = element.sceneStates[fromScene]
-    val toState = element.sceneStates[toScene]
+    val fromState = element.stateByContent[fromScene]
+    val toState = element.stateByContent[toScene]
 
     if (fromState == null && toState == null) {
         // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
@@ -799,7 +811,7 @@
         return true
     }
 
-    return transition.transformationSpec.transformations(element.key, scene.key).alpha == null
+    return transition.transformationSpec.transformations(element.key, content.key).alpha == null
 }
 
 /**
@@ -814,15 +826,15 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
 ): Float {
     val alpha =
         computeValue(
                 layoutImpl,
-                sceneState,
+                stateInContent,
                 element,
                 transition,
-                sceneValue = { 1f },
+                contentValue = { 1f },
                 transformation = { it.alpha },
                 currentValue = { 1f },
                 isSpecified = { true },
@@ -832,12 +844,12 @@
 
     // If the element is fading during this transition and that it is drawn for the first time, make
     // sure that it doesn't instantly appear on screen.
-    if (!element.wasDrawnInAnyScene && alpha > 0f) {
-        element.sceneStates.forEach { it.value.alphaBeforeInterruption = 0f }
+    if (!element.wasDrawnInAnyContent && alpha > 0f) {
+        element.stateByContent.forEach { it.value.alphaBeforeInterruption = 0f }
     }
 
-    val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, sceneState, alpha)
-    sceneState.lastAlpha = interruptedAlpha
+    val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha)
+    stateInContent.lastAlpha = interruptedAlpha
     return interruptedAlpha
 }
 
@@ -845,7 +857,7 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
     alpha: Float,
 ): Float {
     return computeInterruptedValue(
@@ -854,16 +866,16 @@
         value = alpha,
         unspecifiedValue = Element.AlphaUnspecified,
         zeroValue = 0f,
-        getValueBeforeInterruption = { sceneState.alphaBeforeInterruption },
-        setValueBeforeInterruption = { sceneState.alphaBeforeInterruption = it },
-        getInterruptionDelta = { sceneState.alphaInterruptionDelta },
+        getValueBeforeInterruption = { stateInContent.alphaBeforeInterruption },
+        setValueBeforeInterruption = { stateInContent.alphaBeforeInterruption = it },
+        getInterruptionDelta = { stateInContent.alphaInterruptionDelta },
         setInterruptionDelta = { delta ->
             setPlacementInterruptionDelta(
                 element = element,
-                sceneState = sceneState,
+                stateInContent = stateInContent,
                 transition = transition,
                 delta = delta,
-                setter = { sceneState, delta -> sceneState.alphaInterruptionDelta = delta },
+                setter = { stateInContent, delta -> stateInContent.alphaInterruptionDelta = delta },
             )
         },
         diff = { a, b -> a - b },
@@ -875,7 +887,7 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
     measurable: Measurable,
     constraints: Constraints,
 ): Placeable {
@@ -887,10 +899,10 @@
     val targetSize =
         computeValue(
             layoutImpl,
-            sceneState,
+            stateInContent,
             element,
             transition,
-            sceneValue = { it.targetSize },
+            contentValue = { it.targetSize },
             transformation = { it.size },
             currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
             isSpecified = { it != Element.SizeUnspecified },
@@ -900,8 +912,8 @@
     // The measurable was already measured, so we can't take interruptions into account here given
     // that we are not allowed to measure the same measurable twice.
     maybePlaceable?.let { placeable ->
-        sceneState.sizeBeforeInterruption = Element.SizeUnspecified
-        sceneState.sizeInterruptionDelta = IntSize.Zero
+        stateInContent.sizeBeforeInterruption = Element.SizeUnspecified
+        stateInContent.sizeInterruptionDelta = IntSize.Zero
         return placeable
     }
 
@@ -912,10 +924,10 @@
             value = targetSize,
             unspecifiedValue = Element.SizeUnspecified,
             zeroValue = IntSize.Zero,
-            getValueBeforeInterruption = { sceneState.sizeBeforeInterruption },
-            setValueBeforeInterruption = { sceneState.sizeBeforeInterruption = it },
-            getInterruptionDelta = { sceneState.sizeInterruptionDelta },
-            setInterruptionDelta = { sceneState.sizeInterruptionDelta = it },
+            getValueBeforeInterruption = { stateInContent.sizeBeforeInterruption },
+            setValueBeforeInterruption = { stateInContent.sizeBeforeInterruption = it },
+            getInterruptionDelta = { stateInContent.sizeInterruptionDelta },
+            setInterruptionDelta = { stateInContent.sizeInterruptionDelta = it },
             diff = { a, b -> IntSize(a.width - b.width, a.height - b.height) },
             add = { a, b, bProgress ->
                 IntSize(
@@ -939,15 +951,15 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
 ): Scale {
     val scale =
         computeValue(
             layoutImpl,
-            sceneState,
+            stateInContent,
             element,
             transition,
-            sceneValue = { Scale.Default },
+            contentValue = { Scale.Default },
             transformation = { it.drawScale },
             currentValue = { Scale.Default },
             isSpecified = { true },
@@ -965,16 +977,18 @@
             value = scale,
             unspecifiedValue = Scale.Unspecified,
             zeroValue = Scale.Zero,
-            getValueBeforeInterruption = { sceneState.scaleBeforeInterruption },
-            setValueBeforeInterruption = { sceneState.scaleBeforeInterruption = it },
-            getInterruptionDelta = { sceneState.scaleInterruptionDelta },
+            getValueBeforeInterruption = { stateInContent.scaleBeforeInterruption },
+            setValueBeforeInterruption = { stateInContent.scaleBeforeInterruption = it },
+            getInterruptionDelta = { stateInContent.scaleInterruptionDelta },
             setInterruptionDelta = { delta ->
                 setPlacementInterruptionDelta(
                     element = element,
-                    sceneState = sceneState,
+                    stateInContent = stateInContent,
                     transition = transition,
                     delta = delta,
-                    setter = { sceneState, delta -> sceneState.scaleInterruptionDelta = delta },
+                    setter = { stateInContent, delta ->
+                        stateInContent.scaleInterruptionDelta = delta
+                    },
                 )
             },
             diff = { a, b ->
@@ -1003,7 +1017,7 @@
             }
         )
 
-    sceneState.lastScale = interruptedScale
+    stateInContent.lastScale = interruptedScale
     return interruptedScale
 }
 
@@ -1015,11 +1029,11 @@
  * Measurable.
  *
  * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
- * @param currentSceneState the scene state of the scene for which we are computing the value. Note
- *   that during interruptions, this could be the state of a scene that is neither
+ * @param currentContentState the content state of the content for which we are computing the value.
+ *   Note that during interruptions, this could be the state of a content that is neither
  *   [transition.toScene] nor [transition.fromScene].
  * @param element the element being animated.
- * @param sceneValue the value being animated.
+ * @param contentValue the value being animated.
  * @param transformation the transformation associated to the value being animated.
  * @param currentValue the value that would be used if it is not transformed. Note that this is
  *   different than [idleValue] even if the value is not transformed directly because it could be
@@ -1030,10 +1044,10 @@
  */
 private inline fun <T> computeValue(
     layoutImpl: SceneTransitionLayoutImpl,
-    currentSceneState: Element.SceneState,
+    currentContentState: Element.State,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneValue: (Element.SceneState) -> T,
+    contentValue: (Element.State) -> T,
     transformation: (ElementTransformations) -> PropertyTransformation<T>?,
     currentValue: () -> T,
     isSpecified: (T) -> Boolean,
@@ -1050,16 +1064,16 @@
     val fromScene = transition.fromScene
     val toScene = transition.toScene
 
-    val fromState = element.sceneStates[fromScene]
-    val toState = element.sceneStates[toScene]
+    val fromState = element.stateByContent[fromScene]
+    val toState = element.stateByContent[toScene]
 
     if (fromState == null && toState == null) {
         // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
         // run anymore.
-        return sceneValue(currentSceneState)
+        return contentValue(currentContentState)
     }
 
-    val currentScene = currentSceneState.scene
+    val currentScene = currentContentState.content
     if (transition is TransitionState.HasOverscrollProperties) {
         val overscroll = transition.currentOverscrollSpec
         if (overscroll?.scene == currentScene) {
@@ -1067,7 +1081,7 @@
                 overscroll.transformationSpec.transformations(element.key, currentScene)
             val propertySpec = transformation(elementSpec) ?: return currentValue()
             val overscrollState = checkNotNull(if (currentScene == toScene) toState else fromState)
-            val idleValue = sceneValue(overscrollState)
+            val idleValue = contentValue(overscrollState)
             val targetValue =
                 propertySpec.transform(
                     layoutImpl,
@@ -1102,8 +1116,8 @@
     // elements follow the finger direction.
     val isSharedElement = fromState != null && toState != null
     if (isSharedElement && isSharedElementEnabled(element.key, transition)) {
-        val start = sceneValue(fromState!!)
-        val end = sceneValue(toState!!)
+        val start = contentValue(fromState!!)
+        val end = contentValue(toState!!)
 
         // TODO(b/316901148): Remove checks to isSpecified() once the lookahead pass runs for all
         // nodes before the intermediate layout pass.
@@ -1117,7 +1131,7 @@
 
     // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
     // end (for leaving elements) of the transition.
-    val sceneState =
+    val contentState =
         checkNotNull(
             when {
                 isSharedElement && currentScene == fromScene -> fromState
@@ -1129,26 +1143,26 @@
     // The scene for which we compute the transformation. Note that this is not necessarily
     // [currentScene] because [currentScene] could be a different scene than the transition
     // fromScene or toScene during interruptions.
-    val scene = sceneState.scene
+    val content = contentState.content
 
     val transformation =
-        transformation(transition.transformationSpec.transformations(element.key, scene))
+        transformation(transition.transformationSpec.transformations(element.key, content))
 
     val previewTransformation =
         transition.previewTransformationSpec?.let {
-            transformation(it.transformations(element.key, scene))
+            transformation(it.transformations(element.key, content))
         }
     if (previewTransformation != null) {
         val isInPreviewStage = transition.isInPreviewStage
 
-        val idleValue = sceneValue(sceneState)
-        val isEntering = scene == toScene
+        val idleValue = contentValue(contentState)
+        val isEntering = content == toScene
         val previewTargetValue =
             previewTransformation.transform(
                 layoutImpl,
-                scene,
+                content,
                 element,
-                sceneState,
+                contentState,
                 transition,
                 idleValue,
             )
@@ -1156,9 +1170,9 @@
         val targetValueOrNull =
             transformation?.transform(
                 layoutImpl,
-                scene,
+                content,
                 element,
-                sceneState,
+                contentState,
                 transition,
                 idleValue,
             )
@@ -1226,13 +1240,13 @@
         return currentValue()
     }
 
-    val idleValue = sceneValue(sceneState)
+    val idleValue = contentValue(contentState)
     val targetValue =
         transformation.transform(
             layoutImpl,
-            scene,
+            content,
             element,
-            sceneState,
+            contentState,
             transition,
             idleValue,
         )
@@ -1248,7 +1262,7 @@
     val rangeProgress = transformation.range?.progress(progress) ?: progress
 
     // Interpolate between the value at rest and the value before entering/after leaving.
-    val isEntering = scene == toScene
+    val isEntering = content == toScene
     return if (isEntering) {
         lerp(targetValue, idleValue, rangeProgress)
     } else {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
index 98dbb67..ca68c25 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
@@ -18,20 +18,23 @@
 
 /** An interface to match one or more elements. */
 interface ElementMatcher {
-    /** Whether the element with key [key] in scene [scene] matches this matcher. */
-    fun matches(key: ElementKey, scene: SceneKey): Boolean
+    /** Whether the element with key [key] in scene [content] matches this matcher. */
+    fun matches(key: ElementKey, content: ContentKey): Boolean
 }
 
 /**
- * Returns an [ElementMatcher] that matches elements in [scene] also matching [this]
+ * Returns an [ElementMatcher] that matches elements in [content] also matching [this]
  * [ElementMatcher].
  */
-fun ElementMatcher.inScene(scene: SceneKey): ElementMatcher {
+fun ElementMatcher.inContent(content: ContentKey): ElementMatcher {
     val delegate = this
-    val matcherScene = scene
+    val matcherScene = content
     return object : ElementMatcher {
-        override fun matches(key: ElementKey, scene: SceneKey): Boolean {
-            return scene == matcherScene && delegate.matches(key, scene)
+        override fun matches(key: ElementKey, content: ContentKey): Boolean {
+            return content == matcherScene && delegate.matches(key, content)
         }
     }
 }
+
+@Deprecated("Use inContent() instead", replaceWith = ReplaceWith("inContent(scene)"))
+fun ElementMatcher.inScene(scene: SceneKey) = inContent(scene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index 9770399..a9edf0a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -40,15 +40,20 @@
     }
 }
 
+/** The key for a content (scene or overlay). */
+sealed class ContentKey(debugName: String, identity: Any) : Key(debugName, identity) {
+    @VisibleForTesting
+    // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
+    // access internal members.
+    abstract val testTag: String
+}
+
 /** Key for a scene. */
 class SceneKey(
     debugName: String,
     identity: Any = Object(),
-) : Key(debugName, identity) {
-    @VisibleForTesting
-    // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
-    // access internal members.
-    val testTag: String = "scene:$debugName"
+) : ContentKey(debugName, identity) {
+    override val testTag: String = "scene:$debugName"
 
     /** The unique [ElementKey] identifying this scene's root element. */
     val rootElementKey = ElementKey(debugName, identity)
@@ -74,7 +79,7 @@
     // access internal members.
     val testTag: String = "element:$debugName"
 
-    override fun matches(key: ElementKey, scene: SceneKey): Boolean {
+    override fun matches(key: ElementKey, content: ContentKey): Boolean {
         return key == this
     }
 
@@ -86,7 +91,7 @@
         /** Matches any element whose [key identity][ElementKey.identity] matches [predicate]. */
         fun withIdentity(predicate: (Any) -> Boolean): ElementMatcher {
             return object : ElementMatcher {
-                override fun matches(key: ElementKey, scene: SceneKey): Boolean {
+                override fun matches(key: ElementKey, content: ContentKey): Boolean {
                     return predicate(key.identity)
                 }
             }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
index 32eadde..e556f6f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
@@ -27,21 +27,22 @@
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastLastOrNull
+import com.android.compose.animation.scene.content.Content
 
 @Composable
 internal fun Element(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
+    sceneOrOverlay: Content,
     key: ElementKey,
     modifier: Modifier,
     content: @Composable ElementScope<ElementContentScope>.() -> Unit,
 ) {
-    Box(modifier.element(layoutImpl, scene, key)) {
-        val sceneScope = scene.scope
+    Box(modifier.element(layoutImpl, sceneOrOverlay, key)) {
+        val contentScope = sceneOrOverlay.scope
         val boxScope = this
         val elementScope =
-            remember(layoutImpl, key, scene, sceneScope, boxScope) {
-                ElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope)
+            remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) {
+                ElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope)
             }
 
         content(elementScope)
@@ -51,17 +52,17 @@
 @Composable
 internal fun MovableElement(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
+    sceneOrOverlay: Content,
     key: ElementKey,
     modifier: Modifier,
     content: @Composable ElementScope<MovableElementContentScope>.() -> Unit,
 ) {
-    Box(modifier.element(layoutImpl, scene, key)) {
-        val sceneScope = scene.scope
+    Box(modifier.element(layoutImpl, sceneOrOverlay, key)) {
+        val contentScope = sceneOrOverlay.scope
         val boxScope = this
         val elementScope =
-            remember(layoutImpl, key, scene, sceneScope, boxScope) {
-                MovableElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope)
+            remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) {
+                MovableElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope)
             }
 
         content(elementScope)
@@ -71,7 +72,7 @@
 private abstract class BaseElementScope<ContentScope>(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val element: ElementKey,
-    private val scene: Scene,
+    private val sceneOrOverlay: Content,
 ) : ElementScope<ContentScope> {
     @Composable
     override fun <T> animateElementValueAsState(
@@ -82,7 +83,7 @@
     ): AnimatedState<T> {
         return animateSharedValueAsState(
             layoutImpl,
-            scene.key,
+            sceneOrOverlay.key,
             element,
             key,
             value,
@@ -95,12 +96,12 @@
 private class ElementScopeImpl(
     layoutImpl: SceneTransitionLayoutImpl,
     element: ElementKey,
-    scene: Scene,
-    private val sceneScope: SceneScope,
+    content: Content,
+    private val delegateContentScope: ContentScope,
     private val boxScope: BoxScope,
-) : BaseElementScope<ElementContentScope>(layoutImpl, element, scene) {
+) : BaseElementScope<ElementContentScope>(layoutImpl, element, content) {
     private val contentScope =
-        object : ElementContentScope, SceneScope by sceneScope, BoxScope by boxScope {}
+        object : ElementContentScope, ContentScope by delegateContentScope, BoxScope by boxScope {}
 
     @Composable
     override fun content(content: @Composable ElementContentScope.() -> Unit) {
@@ -111,12 +112,15 @@
 private class MovableElementScopeImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val element: ElementKey,
-    private val scene: Scene,
-    private val sceneScope: BaseSceneScope,
+    private val content: Content,
+    private val baseContentScope: BaseContentScope,
     private val boxScope: BoxScope,
-) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, scene) {
+) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, content) {
     private val contentScope =
-        object : MovableElementContentScope, BaseSceneScope by sceneScope, BoxScope by boxScope {}
+        object :
+            MovableElementContentScope,
+            BaseContentScope by baseContentScope,
+            BoxScope by boxScope {}
 
     @Composable
     override fun content(content: @Composable MovableElementContentScope.() -> Unit) {
@@ -126,9 +130,10 @@
         // during the transition.
         // TODO(b/317026105): Use derivedStateOf only if the scene picker reads the progress in its
         // logic.
+        val contentKey = this@MovableElementScopeImpl.content.key
         val shouldComposeMovableElement by
-            remember(layoutImpl, scene.key, element) {
-                derivedStateOf { shouldComposeMovableElement(layoutImpl, scene.key, element) }
+            remember(layoutImpl, contentKey, element) {
+                derivedStateOf { shouldComposeMovableElement(layoutImpl, contentKey, element) }
             }
 
         if (shouldComposeMovableElement) {
@@ -152,7 +157,7 @@
                 val size =
                     placeholderContentSize(
                         layoutImpl,
-                        scene.key,
+                        contentKey,
                         layoutImpl.elements.getValue(element),
                     )
                 layout(size.width, size.height) {}
@@ -163,7 +168,7 @@
 
 private fun shouldComposeMovableElement(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: ElementKey,
 ): Boolean {
     val transitions = layoutImpl.state.currentTransitions
@@ -171,7 +176,7 @@
         // If we are idle, there is only one [scene] that is composed so we can compose our
         // movable content here. We still check that [scene] is equal to the current idle scene, to
         // make sure we only compose it there.
-        return layoutImpl.state.transitionState.currentScene == scene
+        return layoutImpl.state.transitionState.currentScene == content
     }
 
     // The current transition for this element is the last transition in which either fromScene or
@@ -189,7 +194,7 @@
     // Always compose movable elements in the scene picked by their scene picker.
     return shouldPlaceOrComposeSharedElement(
         layoutImpl,
-        scene,
+        content,
         element,
         transition,
     )
@@ -201,12 +206,12 @@
  */
 private fun placeholderContentSize(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: Element,
 ): IntSize {
     // If the content of the movable element was already composed in this scene before, use that
     // target size.
-    val targetValueInScene = element.sceneStates.getValue(scene).targetSize
+    val targetValueInScene = element.stateByContent.getValue(content).targetSize
     if (targetValueInScene != Element.SizeUnspecified) {
         return targetValueInScene
     }
@@ -219,8 +224,9 @@
     // doesn't change between scenes.
     // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is not
     // true.
-    val otherScene = if (transition.fromScene == scene) transition.toScene else transition.fromScene
-    val targetValueInOtherScene = element.sceneStates[otherScene]?.targetSize
+    val otherScene =
+        if (transition.fromScene == content) transition.toScene else transition.fromScene
+    val targetValueInOtherScene = element.stateByContent[otherScene]?.targetSize
     if (targetValueInOtherScene != null && targetValueInOtherScene != Element.SizeUnspecified) {
         return targetValueInOtherScene
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 2fc4526..3401af8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -34,7 +34,6 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
-import com.android.compose.animation.scene.UserAction.Resolved
 
 /**
  * [SceneTransitionLayout] is a container that automatically animates its content whenever its state
@@ -85,7 +84,7 @@
     fun scene(
         key: SceneKey,
         userActions: Map<UserAction, UserActionResult> = emptyMap(),
-        content: @Composable SceneScope.() -> Unit,
+        content: @Composable ContentScope.() -> Unit,
     )
 }
 
@@ -118,25 +117,25 @@
 
 @Stable
 @ElementDsl
-interface BaseSceneScope : ElementStateScope {
-    /** The key of this scene. */
-    val sceneKey: SceneKey
+interface BaseContentScope : ElementStateScope {
+    /** The key of this content. */
+    val contentKey: ContentKey
 
-    /** The state of the [SceneTransitionLayout] in which this scene is contained. */
+    /** The state of the [SceneTransitionLayout] in which this content is contained. */
     val layoutState: SceneTransitionLayoutState
 
     /**
      * Tag an element identified by [key].
      *
      * Tagging an element will allow you to reference that element when defining transitions, so
-     * that the element can be transformed and animated when the scene transitions in or out.
+     * that the element can be transformed and animated when the content transitions in or out.
      *
-     * Additionally, this [key] will be used to detect elements that are shared between scenes to
+     * Additionally, this [key] will be used to detect elements that are shared between contents to
      * automatically interpolate their size and offset. If you need to animate shared element values
-     * (i.e. values associated to this element that change depending on which scene it is composed
+     * (i.e. values associated to this element that change depending on which content it is composed
      * in), use [Element] instead.
      *
-     * Note that shared elements tagged using this function will be duplicated in each scene they
+     * Note that shared elements tagged using this function will be duplicated in each content they
      * are part of, so any **internal** state (e.g. state created using `remember {
      * mutableStateOf(...) }`) will be lost. If you need to preserve internal state, you should use
      * [MovableElement] instead.
@@ -150,7 +149,7 @@
      * Create an element identified by [key].
      *
      * Similar to [element], this creates an element that will be automatically shared when present
-     * in multiple scenes and that can be transformed during transitions, the same way that
+     * in multiple contents and that can be transformed during transitions, the same way that
      * [element] does.
      *
      * The only difference with [element] is that the provided [ElementScope] allows you to
@@ -177,7 +176,7 @@
      * Create a *movable* element identified by [key].
      *
      * Similar to [Element], this creates an element that will be automatically shared when present
-     * in multiple scenes and that can be transformed during transitions, and you can also use the
+     * in multiple contents and that can be transformed during transitions, and you can also use the
      * provided [ElementScope] to [animate element values][ElementScope.animateElementValueAsState].
      *
      * The important difference with [element] and [Element] is that this element
@@ -232,24 +231,26 @@
     fun Modifier.noResizeDuringTransitions(): Modifier
 }
 
+typealias SceneScope = ContentScope
+
 @Stable
 @ElementDsl
-interface SceneScope : BaseSceneScope {
+interface ContentScope : BaseContentScope {
     /**
-     * Animate some value at the scene level.
+     * Animate some value at the content level.
      *
      * @param value the value of this shared value in the current scene.
      * @param key the key of this shared value.
      * @param type the [SharedValueType] of this animated value.
      * @param canOverflow whether this value can overflow past the values it is interpolated
      *   between, for instance because the transition is animated using a bouncy spring.
-     * @see animateSceneIntAsState
-     * @see animateSceneFloatAsState
-     * @see animateSceneDpAsState
-     * @see animateSceneColorAsState
+     * @see animateContentIntAsState
+     * @see animateContentFloatAsState
+     * @see animateContentDpAsState
+     * @see animateContentColorAsState
      */
     @Composable
-    fun <T> animateSceneValueAsState(
+    fun <T> animateContentValueAsState(
         value: T,
         key: ValueKey,
         type: SharedValueType<T, *>,
@@ -259,7 +260,7 @@
 
 /**
  * The type of a shared value animated using [ElementScope.animateElementValueAsState] or
- * [SceneScope.animateSceneValueAsState].
+ * [ContentScope.animateContentValueAsState].
  */
 @Stable
 interface SharedValueType<T, Delta> {
@@ -321,8 +322,9 @@
  * The exact same scope as [androidx.compose.foundation.layout.BoxScope].
  *
  * We can't reuse BoxScope directly because of the @LayoutScopeMarker annotation on it, which would
- * prevent us from calling Modifier.element() and other methods of [SceneScope] inside any Box {} in
- * the [content][ElementScope.content] of a [SceneScope.Element] or a [SceneScope.MovableElement].
+ * prevent us from calling Modifier.element() and other methods of [ContentScope] inside any Box {}
+ * in the [content][ElementScope.content] of a [ContentScope.Element] or a
+ * [ContentScope.MovableElement].
  */
 @Stable
 @ElementDsl
@@ -335,16 +337,16 @@
 }
 
 /** The scope for "normal" (not movable) elements. */
-@Stable @ElementDsl interface ElementContentScope : SceneScope, ElementBoxScope
+@Stable @ElementDsl interface ElementContentScope : ContentScope, ElementBoxScope
 
 /**
  * The scope for the content of movable elements.
  *
- * Note that it extends [BaseSceneScope] and not [SceneScope] because movable elements should not
- * call [SceneScope.animateSceneValueAsState], given that their content is not composed in all
- * scenes.
+ * Note that it extends [BaseContentScope] and not [ContentScope] because movable elements should
+ * not call [ContentScope.animateContentValueAsState], given that their content is not composed in
+ * all scenes.
  */
-@Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope
+@Stable @ElementDsl interface MovableElementContentScope : BaseContentScope, ElementBoxScope
 
 /** An action performed by the user. */
 sealed class UserAction {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 32db0b7..062d553 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -36,6 +36,8 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachReversed
+import com.android.compose.animation.scene.content.Content
+import com.android.compose.animation.scene.content.Scene
 import com.android.compose.ui.util.lerp
 import kotlinx.coroutines.CoroutineScope
 
@@ -84,7 +86,7 @@
 
     /**
      * The different values of a shared value keyed by a a [ValueKey] and the different elements and
-     * scenes it is associated to.
+     * contents it is associated to.
      */
     private var _sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>? =
         null
@@ -149,6 +151,12 @@
         return scenes[key] ?: error("Scene $key is not configured")
     }
 
+    internal fun content(key: ContentKey): Content {
+        return when (key) {
+            is SceneKey -> scene(key)
+        }
+    }
+
     internal fun updateScenes(
         builder: SceneTransitionLayoutScope.() -> Unit,
         layoutDirection: LayoutDirection,
@@ -164,7 +172,7 @@
                 override fun scene(
                     key: SceneKey,
                     userActions: Map<UserAction, UserActionResult>,
-                    content: @Composable SceneScope.() -> Unit,
+                    content: @Composable ContentScope.() -> Unit,
                 ) {
                     scenesToRemove.remove(key)
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 06b093d..cfa4c70 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -302,18 +302,18 @@
     override val distance: UserActionDistance?,
     override val transformations: List<Transformation>,
 ) : TransformationSpec {
-    private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>()
+    private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations>>()
 
-    internal fun transformations(element: ElementKey, scene: SceneKey): ElementTransformations {
+    internal fun transformations(element: ElementKey, content: ContentKey): ElementTransformations {
         return cache
             .getOrPut(element) { mutableMapOf() }
-            .getOrPut(scene) { computeTransformations(element, scene) }
+            .getOrPut(content) { computeTransformations(element, content) }
     }
 
     /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
     private fun computeTransformations(
         element: ElementKey,
-        scene: SceneKey,
+        content: ContentKey,
     ): ElementTransformations {
         var shared: SharedElementTransformation? = null
         var offset: PropertyTransformation<Offset>? = null
@@ -351,7 +351,7 @@
         }
 
         transformations.fastForEach { transformation ->
-            if (!transformation.matcher.matches(element, scene)) {
+            if (!transformation.matcher.matches(element, content)) {
                 return@fastForEach
             }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index a2118b2..f062146 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.node.TraversableNode
 import androidx.compose.ui.node.findNearestAncestor
 import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.content.Scene
 
 /**
  * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 3a87d41..06be86d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -239,10 +239,11 @@
      * should not be drawn or composed in neither [transition.fromScene] nor [transition.toScene],
      * return `null`.
      *
-     * Important: For [MovableElements][SceneScope.MovableElement], this scene picker will *always*
-     * be used during transitions to decide whether we should compose that element in a given scene
-     * or not. Therefore, you should make sure that the returned [SceneKey] contains the movable
-     * element, otherwise that element will not be composed in any scene during the transition.
+     * Important: For [MovableElements][ContentScope.MovableElement], this scene picker will
+     * *always* be used during transitions to decide whether we should compose that element in a
+     * given scene or not. Therefore, you should make sure that the returned [SceneKey] contains the
+     * movable element, otherwise that element will not be composed in any scene during the
+     * transition.
      */
     fun sceneDuringTransition(
         element: ElementKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index b7abb33..0f66804 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -23,13 +23,13 @@
     private val layoutImpl: SceneTransitionLayoutImpl,
 ) : ElementStateScope {
     override fun ElementKey.targetSize(scene: SceneKey): IntSize? {
-        return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf {
+        return layoutImpl.elements[this]?.stateByContent?.get(scene)?.targetSize.takeIf {
             it != Element.SizeUnspecified
         }
     }
 
     override fun ElementKey.targetOffset(scene: SceneKey): Offset? {
-        return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetOffset.takeIf {
+        return layoutImpl.elements[this]?.stateByContent?.get(scene)?.targetOffset.takeIf {
             it != Offset.Unspecified
         }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
similarity index 63%
rename from packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
rename to packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index a49f1af..492d211 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.compose.animation.scene
+package com.android.compose.animation.scene.content
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
@@ -29,24 +29,44 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.zIndex
+import com.android.compose.animation.scene.AnimatedState
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementContentScope
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementScope
+import com.android.compose.animation.scene.ElementStateScope
+import com.android.compose.animation.scene.MovableElement
+import com.android.compose.animation.scene.MovableElementContentScope
+import com.android.compose.animation.scene.NestedScrollBehavior
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.SceneTransitionLayoutState
+import com.android.compose.animation.scene.SharedValueType
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.ValueKey
+import com.android.compose.animation.scene.animateSharedValueAsState
+import com.android.compose.animation.scene.element
 import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions
+import com.android.compose.animation.scene.nestedScrollToScene
 
-/** A scene in a [SceneTransitionLayout]. */
+/** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */
 @Stable
-internal class Scene(
-    val key: SceneKey,
-    layoutImpl: SceneTransitionLayoutImpl,
-    content: @Composable SceneScope.() -> Unit,
+internal sealed class Content(
+    open val key: ContentKey,
+    val layoutImpl: SceneTransitionLayoutImpl,
+    content: @Composable ContentScope.() -> Unit,
     actions: Map<UserAction.Resolved, UserActionResult>,
     zIndex: Float,
 ) {
-    internal val scope = SceneScopeImpl(layoutImpl, this)
+    internal val scope = ContentScopeImpl(layoutImpl, content = this)
 
     var content by mutableStateOf(content)
-    private var _userActions by mutableStateOf(checkValid(actions))
     var zIndex by mutableFloatStateOf(zIndex)
     var targetSize by mutableStateOf(IntSize.Zero)
 
+    private var _userActions by mutableStateOf(checkValid(actions))
     var userActions
         get() = _userActions
         set(value) {
@@ -59,8 +79,8 @@
         userActions.forEach { (action, result) ->
             if (key == result.toScene) {
                 error(
-                    "Transition to the same scene is not supported. Scene $key, action $action," +
-                        " result $result"
+                    "Transition to the same content (scene/overlay) is not supported. Content " +
+                        "$key, action $action, result $result"
                 )
             }
         }
@@ -73,7 +93,7 @@
             modifier
                 .zIndex(zIndex)
                 .approachLayout(
-                    isMeasurementApproachInProgress = { scope.layoutState.isTransitioning() }
+                    isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() }
                 ) { measurable, constraints ->
                     targetSize = lookaheadSize
                     val placeable = measurable.measure(constraints)
@@ -84,21 +104,19 @@
             scope.content()
         }
     }
-
-    override fun toString(): String {
-        return "Scene(key=$key)"
-    }
 }
 
-internal class SceneScopeImpl(
+internal class ContentScopeImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
-    private val scene: Scene,
-) : SceneScope, ElementStateScope by layoutImpl.elementStateScope {
-    override val sceneKey: SceneKey = scene.key
+    private val content: Content,
+) : ContentScope, ElementStateScope by layoutImpl.elementStateScope {
+    override val contentKey: ContentKey
+        get() = content.key
+
     override val layoutState: SceneTransitionLayoutState = layoutImpl.state
 
     override fun Modifier.element(key: ElementKey): Modifier {
-        return element(layoutImpl, scene, key)
+        return element(layoutImpl, content, key)
     }
 
     @Composable
@@ -107,7 +125,7 @@
         modifier: Modifier,
         content: @Composable (ElementScope<ElementContentScope>.() -> Unit)
     ) {
-        Element(layoutImpl, scene, key, modifier, content)
+        Element(layoutImpl, this@ContentScopeImpl.content, key, modifier, content)
     }
 
     @Composable
@@ -116,19 +134,19 @@
         modifier: Modifier,
         content: @Composable (ElementScope<MovableElementContentScope>.() -> Unit)
     ) {
-        MovableElement(layoutImpl, scene, key, modifier, content)
+        MovableElement(layoutImpl, this@ContentScopeImpl.content, key, modifier, content)
     }
 
     @Composable
-    override fun <T> animateSceneValueAsState(
+    override fun <T> animateContentValueAsState(
         value: T,
         key: ValueKey,
         type: SharedValueType<T, *>,
-        canOverflow: Boolean
+        canOverflow: Boolean,
     ): AnimatedState<T> {
         return animateSharedValueAsState(
             layoutImpl = layoutImpl,
-            scene = scene.key,
+            content = content.key,
             element = null,
             key = key,
             value = value,
@@ -141,27 +159,29 @@
         leftBehavior: NestedScrollBehavior,
         rightBehavior: NestedScrollBehavior,
         isExternalOverscrollGesture: () -> Boolean,
-    ): Modifier =
-        nestedScrollToScene(
+    ): Modifier {
+        return nestedScrollToScene(
             layoutImpl = layoutImpl,
             orientation = Orientation.Horizontal,
             topOrLeftBehavior = leftBehavior,
             bottomOrRightBehavior = rightBehavior,
             isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
+    }
 
     override fun Modifier.verticalNestedScrollToScene(
         topBehavior: NestedScrollBehavior,
         bottomBehavior: NestedScrollBehavior,
         isExternalOverscrollGesture: () -> Boolean,
-    ): Modifier =
-        nestedScrollToScene(
+    ): Modifier {
+        return nestedScrollToScene(
             layoutImpl = layoutImpl,
             orientation = Orientation.Vertical,
             topOrLeftBehavior = topBehavior,
             bottomOrRightBehavior = bottomBehavior,
             isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
+    }
 
     override fun Modifier.noResizeDuringTransitions(): Modifier {
         return noResizeDuringTransitions(layoutState = layoutImpl.state)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt
new file mode 100644
index 0000000..4a7a94d
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.content
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+
+/** A scene defined in a [SceneTransitionLayout]. */
+@Stable
+internal class Scene(
+    override val key: SceneKey,
+    layoutImpl: SceneTransitionLayoutImpl,
+    content: @Composable ContentScope.() -> Unit,
+    actions: Map<UserAction.Resolved, UserActionResult>,
+    zIndex: Float,
+) : Content(key, layoutImpl, content, actions, zIndex) {
+    override fun toString(): String {
+        return "Scene(key=$key)"
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index 73ee451..65d4d2d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -17,6 +17,7 @@
 package com.android.compose.animation.scene.transformation
 
 import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
@@ -33,15 +34,15 @@
 ) : PropertyTransformation<IntSize> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: IntSize,
     ): IntSize {
         fun anchorSizeIn(scene: SceneKey): IntSize {
             val size =
-                layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize?.takeIf {
+                layoutImpl.elements[anchor]?.stateByContent?.get(scene)?.targetSize?.takeIf {
                     it != Element.SizeUnspecified
                 }
                     ?: throwMissingAnchorException(
@@ -59,7 +60,7 @@
         // This simple implementation assumes that the size of [element] is the same as the size of
         // the [anchor] in [scene], so simply transform to the size of the anchor in the other
         // scene.
-        return if (scene == transition.fromScene) {
+        return if (content == transition.fromScene) {
             anchorSizeIn(transition.toScene)
         } else {
             anchorSizeIn(transition.fromScene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index 70dca4c..8d7e1c9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.isSpecified
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
@@ -32,9 +33,9 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
@@ -48,7 +49,7 @@
 
         val anchor = layoutImpl.elements[anchor] ?: throwException(scene = null)
         fun anchorOffsetIn(scene: SceneKey): Offset? {
-            return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified }
+            return anchor.stateByContent[scene]?.targetOffset?.takeIf { it.isSpecified }
         }
 
         // [element] will move the same amount as [anchor] does.
@@ -60,7 +61,7 @@
             anchorOffsetIn(transition.toScene) ?: throwException(transition.toScene)
         val offset = anchorToOffset - anchorFromOffset
 
-        return if (scene == transition.toScene) {
+        return if (content == transition.toScene) {
             Offset(
                 value.x - offset.x,
                 value.y - offset.y,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index 98c2dd3..f010c3b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -17,10 +17,10 @@
 package com.android.compose.animation.scene.transformation
 
 import androidx.compose.ui.geometry.Offset
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.Scale
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -37,9 +37,9 @@
 
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Scale,
     ): Scale {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 7daefd0..dfce997 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -17,10 +17,10 @@
 package com.android.compose.animation.scene.transformation
 
 import androidx.compose.ui.geometry.Offset
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -32,13 +32,13 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Offset
     ): Offset {
-        val sceneSize = layoutImpl.scene(scene).targetSize
+        val sceneSize = layoutImpl.content(content).targetSize
         val elementSize = sceneState.targetSize
         if (elementSize == Element.SizeUnspecified) {
             return value
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index ada814e..c1bb017 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -16,9 +16,9 @@
 
 package com.android.compose.animation.scene.transformation
 
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -28,9 +28,9 @@
 ) : PropertyTransformation<Float> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Float
     ): Float {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index dca8f85..5adbf7e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -17,9 +17,9 @@
 package com.android.compose.animation.scene.transformation
 
 import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 import kotlin.math.roundToInt
@@ -35,9 +35,9 @@
 ) : PropertyTransformation<IntSize> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: IntSize,
     ): IntSize {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 7be9ce1..24b7194 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -19,9 +19,9 @@
 import androidx.compose.ui.util.fastCoerceAtLeast
 import androidx.compose.ui.util.fastCoerceAtMost
 import androidx.compose.ui.util.fastCoerceIn
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -61,9 +61,9 @@
     // to these internal classes.
     fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: T,
     ): T
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index f066511..123756a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -19,10 +19,10 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.OverscrollScope
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -33,9 +33,9 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
@@ -55,9 +55,9 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
index 8e35988..ae3169b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
@@ -57,7 +57,7 @@
             minHeight() < currentHeight && currentHeight < maxHeight()
         },
         canScrollOnFling = true,
-        onStart = { /* do nothing */},
+        onStart = { /* do nothing */ },
         onScroll = { offsetAvailable ->
             val currentHeight = height()
             val amountConsumed =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
index ac11d30..228f7ba 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
@@ -38,7 +38,7 @@
     private val canStartPreScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
     private val canStartPostScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
     private val canStartPostFling: (velocityAvailable: Velocity) -> Boolean,
-    private val canContinueScroll: () -> Boolean,
+    private val canContinueScroll: (source: NestedScrollSource) -> Boolean,
     private val canScrollOnFling: Boolean,
     private val onStart: (offsetAvailable: Offset) -> Unit,
     private val onScroll: (offsetAvailable: Offset) -> Offset,
@@ -61,7 +61,7 @@
 
         if (
             isPriorityMode ||
-                (source == NestedScrollSource.Fling && !canScrollOnFling) ||
+                (source == NestedScrollSource.SideEffect && !canScrollOnFling) ||
                 !canStartPostScroll(available, offsetBeforeStart)
         ) {
             // The priority mode cannot start so we won't consume the available offset.
@@ -73,7 +73,7 @@
 
     override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
         if (!isPriorityMode) {
-            if (source != NestedScrollSource.Fling || canScrollOnFling) {
+            if (source == NestedScrollSource.UserInput || canScrollOnFling) {
                 if (canStartPreScroll(available, offsetScrolledBeforePriorityMode)) {
                     return onPriorityStart(available)
                 }
@@ -84,7 +84,7 @@
             return Offset.Zero
         }
 
-        if (!canContinueScroll()) {
+        if (!canContinueScroll(source)) {
             // Step 3a: We have lost priority and we no longer need to intercept scroll events.
             onPriorityStop(velocity = Velocity.Zero)
 
@@ -170,7 +170,7 @@
     canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
     canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
     canStartPostFling: (velocityAvailable: Float) -> Boolean,
-    canContinueScroll: () -> Boolean,
+    canContinueScroll: (source: NestedScrollSource) -> Boolean,
     canScrollOnFling: Boolean,
     onStart: (offsetAvailable: Float) -> Unit,
     onScroll: (offsetAvailable: Float) -> Float,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
index a7889e2..0f33303 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
@@ -67,7 +67,7 @@
     }
 
     @Composable
-    private fun SceneScope.Foo(
+    private fun ContentScope.Foo(
         targetValues: Values,
         onCurrentValueChanged: (Values) -> Unit,
     ) {
@@ -87,7 +87,7 @@
     }
 
     @Composable
-    private fun SceneScope.MovableFoo(
+    private fun ContentScope.MovableFoo(
         targetValues: Values,
         onCurrentValueChanged: (Values) -> Unit,
     ) {
@@ -105,14 +105,14 @@
     }
 
     @Composable
-    private fun SceneScope.SceneValues(
+    private fun ContentScope.SceneValues(
         targetValues: Values,
         onCurrentValueChanged: (Values) -> Unit,
     ) {
-        val int by animateSceneIntAsState(targetValues.int, key = TestValues.Value1)
-        val float by animateSceneFloatAsState(targetValues.float, key = TestValues.Value2)
-        val dp by animateSceneDpAsState(targetValues.dp, key = TestValues.Value3)
-        val color by animateSceneColorAsState(targetValues.color, key = TestValues.Value4)
+        val int by animateContentIntAsState(targetValues.int, key = TestValues.Value1)
+        val float by animateContentFloatAsState(targetValues.float, key = TestValues.Value2)
+        val dp by animateContentDpAsState(targetValues.dp, key = TestValues.Value3)
+        val color by animateContentColorAsState(targetValues.color, key = TestValues.Value4)
 
         LaunchedEffect(Unit) {
             snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged)
@@ -292,7 +292,7 @@
     fun readingAnimatedStateValueDuringCompositionThrows() {
         assertThrows(IllegalStateException::class.java) {
             rule.testTransition(
-                fromSceneContent = { animateSceneIntAsState(0, TestValues.Value1).value },
+                fromSceneContent = { animateContentIntAsState(0, TestValues.Value1).value },
                 toSceneContent = {},
                 transition = {},
             ) {}
@@ -302,21 +302,21 @@
     @Test
     fun readingAnimatedStateValueDuringCompositionIsStillPossible() {
         @Composable
-        fun SceneScope.SceneValuesDuringComposition(
+        fun ContentScope.SceneValuesDuringComposition(
             targetValues: Values,
             onCurrentValueChanged: (Values) -> Unit,
         ) {
             val int by
-                animateSceneIntAsState(targetValues.int, key = TestValues.Value1)
+                animateContentIntAsState(targetValues.int, key = TestValues.Value1)
                     .unsafeCompositionState(targetValues.int)
             val float by
-                animateSceneFloatAsState(targetValues.float, key = TestValues.Value2)
+                animateContentFloatAsState(targetValues.float, key = TestValues.Value2)
                     .unsafeCompositionState(targetValues.float)
             val dp by
-                animateSceneDpAsState(targetValues.dp, key = TestValues.Value3)
+                animateContentDpAsState(targetValues.dp, key = TestValues.Value3)
                     .unsafeCompositionState(targetValues.dp)
             val color by
-                animateSceneColorAsState(targetValues.color, key = TestValues.Value4)
+                animateContentColorAsState(targetValues.color, key = TestValues.Value4)
                     .unsafeCompositionState(targetValues.color)
 
             val values = Values(int, float, dp, color)
@@ -397,14 +397,14 @@
 
         val foo = ValueKey("foo")
         val bar = ValueKey("bar")
-        val lastValues = mutableMapOf<ValueKey, MutableMap<SceneKey, Float>>()
+        val lastValues = mutableMapOf<ValueKey, MutableMap<ContentKey, Float>>()
 
         @Composable
-        fun SceneScope.animateFloat(value: Float, key: ValueKey) {
-            val animatedValue = animateSceneFloatAsState(value, key)
+        fun ContentScope.animateFloat(value: Float, key: ValueKey) {
+            val animatedValue = animateContentFloatAsState(value, key)
             LaunchedEffect(animatedValue) {
                 snapshotFlow { animatedValue.value }
-                    .collect { lastValues.getOrPut(key) { mutableMapOf() }[sceneKey] = it }
+                    .collect { lastValues.getOrPut(key) { mutableMapOf() }[contentKey] = it }
             }
         }
 
@@ -458,13 +458,13 @@
             }
 
         val key = ValueKey("foo")
-        val lastValues = mutableMapOf<SceneKey, Float>()
+        val lastValues = mutableMapOf<ContentKey, Float>()
 
         @Composable
-        fun SceneScope.animateFloat(value: Float, key: ValueKey) {
-            val animatedValue = animateSceneFloatAsState(value, key)
+        fun ContentScope.animateFloat(value: Float, key: ValueKey) {
+            val animatedValue = animateContentFloatAsState(value, key)
             LaunchedEffect(animatedValue) {
-                snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it }
+                snapshotFlow { animatedValue.value }.collect { lastValues[contentKey] = it }
             }
         }
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 1d9e9b7..329257e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -86,7 +86,7 @@
     @get:Rule val rule = createComposeRule()
 
     @Composable
-    private fun SceneScope.Element(
+    private fun ContentScope.Element(
         key: ElementKey,
         size: Dp,
         offset: Dp,
@@ -380,7 +380,7 @@
 
         assertThat(layoutImpl.elements.keys).containsExactly(key)
         val element = layoutImpl.elements.getValue(key)
-        assertThat(element.sceneStates.keys).containsExactly(SceneB)
+        assertThat(element.stateByContent.keys).containsExactly(SceneB)
 
         // Scene C, state 0: the same element is reused.
         rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) }
@@ -389,13 +389,13 @@
 
         assertThat(layoutImpl.elements.keys).containsExactly(key)
         assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
-        assertThat(element.sceneStates.keys).containsExactly(SceneC)
+        assertThat(element.stateByContent.keys).containsExactly(SceneC)
 
         // Scene C, state 1: the element is removed from the map.
         sceneCState = 1
         rule.waitForIdle()
 
-        assertThat(element.sceneStates).isEmpty()
+        assertThat(element.stateByContent).isEmpty()
         assertThat(layoutImpl.elements).isEmpty()
     }
 
@@ -405,7 +405,7 @@
 
         assertThrows(IllegalStateException::class.java) {
             rule.setContent {
-                TestSceneScope {
+                TestContentScope {
                     Column {
                         Box(Modifier.element(key))
                         Box(Modifier.element(key))
@@ -421,7 +421,7 @@
 
         assertThrows(IllegalStateException::class.java) {
             rule.setContent {
-                TestSceneScope {
+                TestContentScope {
                     Column {
                         val childModifier = Modifier.element(key)
                         Box(childModifier)
@@ -439,7 +439,7 @@
         assertThrows(IllegalStateException::class.java) {
             var nElements by mutableStateOf(1)
             rule.setContent {
-                TestSceneScope {
+                TestContentScope {
                     Column {
                         val childModifier = Modifier.element(key)
                         repeat(nElements) { Box(childModifier) }
@@ -457,7 +457,7 @@
         assertThrows(IllegalStateException::class.java) {
             var key by mutableStateOf(TestElements.Foo)
             rule.setContent {
-                TestSceneScope {
+                TestContentScope {
                     Column {
                         Box(Modifier.element(key))
                         Box(Modifier.element(TestElements.Bar))
@@ -491,7 +491,7 @@
         // There is only Foo in the elements map.
         assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
         val fooElement = layoutImpl.elements.getValue(TestElements.Foo)
-        assertThat(fooElement.sceneStates.keys).containsExactly(SceneA)
+        assertThat(fooElement.stateByContent.keys).containsExactly(SceneA)
 
         key = TestElements.Bar
 
@@ -499,8 +499,8 @@
         rule.waitForIdle()
         assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Bar)
         val barElement = layoutImpl.elements.getValue(TestElements.Bar)
-        assertThat(barElement.sceneStates.keys).containsExactly(SceneA)
-        assertThat(fooElement.sceneStates).isEmpty()
+        assertThat(barElement.stateByContent.keys).containsExactly(SceneA)
+        assertThat(fooElement.stateByContent).isEmpty()
     }
 
     @Test
@@ -553,7 +553,7 @@
         // There is only Foo in the elements map.
         assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
         val element = layoutImpl.elements.getValue(TestElements.Foo)
-        val sceneValues = element.sceneStates
+        val sceneValues = element.stateByContent
         assertThat(sceneValues.keys).containsExactly(SceneA)
 
         // Get the ElementModifier node that should be reused later on when coming back to this
@@ -576,7 +576,7 @@
 
         assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
         val newElement = layoutImpl.elements.getValue(TestElements.Foo)
-        val newSceneValues = newElement.sceneStates
+        val newSceneValues = newElement.stateByContent
         assertThat(newElement).isNotEqualTo(element)
         assertThat(newSceneValues).isNotEqualTo(sceneValues)
         assertThat(newSceneValues.keys).containsExactly(SceneA)
@@ -677,7 +677,7 @@
                 modifier = Modifier.size(layoutWidth, layoutHeight)
             ) {
                 scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
-                    animateSceneFloatAsState(
+                    animateContentFloatAsState(
                         value = animatedFloatRange.start,
                         key = TestValues.Value1,
                         false
@@ -686,7 +686,7 @@
                 }
                 scene(SceneB) {
                     val animatedFloat by
-                        animateSceneFloatAsState(
+                        animateContentFloatAsState(
                             value = animatedFloatRange.endInclusive,
                             key = TestValues.Value1,
                             canOverflow = false
@@ -1215,15 +1215,15 @@
             }
 
         val layoutSize = DpSize(200.dp, 100.dp)
-        val lastValues = mutableMapOf<SceneKey, Float>()
+        val lastValues = mutableMapOf<ContentKey, Float>()
 
         @Composable
-        fun SceneScope.Foo(size: Dp, value: Float, modifier: Modifier = Modifier) {
-            val sceneKey = this.sceneKey
+        fun ContentScope.Foo(size: Dp, value: Float, modifier: Modifier = Modifier) {
+            val contentKey = this.contentKey
             Element(TestElements.Foo, modifier.size(size)) {
                 val animatedValue = animateElementFloatAsState(value, TestValues.Value1)
                 LaunchedEffect(animatedValue) {
-                    snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it }
+                    snapshotFlow { animatedValue.value }.collect { lastValues[contentKey] = it }
                 }
             }
         }
@@ -1388,8 +1388,8 @@
 
         // The interruption values should be unspecified and deltas should be set to zero.
         val foo = layoutImpl.elements.getValue(TestElements.Foo)
-        assertThat(foo.sceneStates.keys).containsExactly(SceneC)
-        val stateInC = foo.sceneStates.getValue(SceneC)
+        assertThat(foo.stateByContent.keys).containsExactly(SceneC)
+        val stateInC = foo.stateByContent.getValue(SceneC)
         assertThat(stateInC.offsetBeforeInterruption).isEqualTo(Offset.Unspecified)
         assertThat(stateInC.sizeBeforeInterruption).isEqualTo(Element.SizeUnspecified)
         assertThat(stateInC.scaleBeforeInterruption).isEqualTo(Scale.Unspecified)
@@ -1423,7 +1423,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo(modifier: Modifier = Modifier) {
+        fun ContentScope.Foo(modifier: Modifier = Modifier) {
             Box(modifier.element(TestElements.Foo).size(fooSize))
         }
 
@@ -1542,8 +1542,8 @@
         assertThat(layoutImpl.elements).containsKey(TestElements.Foo)
         val foo = layoutImpl.elements.getValue(TestElements.Foo)
 
-        assertThat(foo.sceneStates).containsKey(SceneB)
-        val bState = foo.sceneStates.getValue(SceneB)
+        assertThat(foo.stateByContent).containsKey(SceneB)
+        val bState = foo.stateByContent.getValue(SceneB)
 
         assertThat(bState.targetSize).isNotEqualTo(Element.SizeUnspecified)
         assertThat(bState.targetOffset).isNotEqualTo(Offset.Unspecified)
@@ -1583,9 +1583,9 @@
         rule.waitForIdle()
 
         val foo = checkNotNull(layoutImpl.elements[TestElements.Foo])
-        assertThat(foo.sceneStates[SceneA]).isNull()
+        assertThat(foo.stateByContent[SceneA]).isNull()
 
-        val fooInB = foo.sceneStates[SceneB]
+        val fooInB = foo.stateByContent[SceneB]
         assertThat(fooInB).isNotNull()
         assertThat(fooInB!!.lastAlpha).isEqualTo(0.5f)
 
@@ -1599,7 +1599,7 @@
             state.startTransition(transition(from = SceneB, to = SceneC, progress = { 0.3f }))
         }
         rule.waitForIdle()
-        val fooInC = foo.sceneStates[SceneC]
+        val fooInC = foo.stateByContent[SceneC]
         assertThat(fooInC).isNotNull()
         assertThat(fooInC!!.lastAlpha).isEqualTo(1f)
         assertThat(fooInB.lastAlpha).isEqualTo(Element.AlphaUnspecified)
@@ -1645,7 +1645,7 @@
         rule.waitForIdle()
 
         // Alpha of Foo should be 0f at interruption progress 100%.
-        val fooInB = layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB)
+        val fooInB = layoutImpl.elements.getValue(TestElements.Foo).stateByContent.getValue(SceneB)
         assertThat(fooInB.lastAlpha).isEqualTo(0f)
 
         // Alpha of Foo should be 0.6f at interruption progress 0%.
@@ -1673,7 +1673,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo() {
+        fun ContentScope.Foo() {
             Box(Modifier.element(TestElements.Foo).size(10.dp))
         }
 
@@ -1724,7 +1724,7 @@
         val fooInB = "fooInB"
 
         @Composable
-        fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
+        fun ContentScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
             MovableElement(TestElements.Foo, modifier) { content { Text(text) } }
         }
 
@@ -1773,7 +1773,7 @@
             }
 
         @Composable
-        fun SceneScope.SceneWithFoo(offset: DpOffset, modifier: Modifier = Modifier) {
+        fun ContentScope.SceneWithFoo(offset: DpOffset, modifier: Modifier = Modifier) {
             Box(modifier.fillMaxSize()) {
                 Box(Modifier.offset(offset.x, offset.y).element(TestElements.Foo).size(100.dp))
             }
@@ -1856,7 +1856,7 @@
         val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
 
         @Composable
-        fun SceneScope.NestedFooBar() {
+        fun ContentScope.NestedFooBar() {
             Box(Modifier.element(TestElements.Foo)) {
                 Box(Modifier.element(TestElements.Bar).size(10.dp))
             }
@@ -1881,13 +1881,13 @@
         val foo = layoutImpl.elements.getValue(TestElements.Foo)
         val bar = layoutImpl.elements.getValue(TestElements.Bar)
 
-        assertThat(foo.sceneStates).containsKey(SceneA)
-        assertThat(bar.sceneStates).containsKey(SceneA)
-        assertThat(foo.sceneStates).doesNotContainKey(SceneB)
-        assertThat(bar.sceneStates).doesNotContainKey(SceneB)
+        assertThat(foo.stateByContent).containsKey(SceneA)
+        assertThat(bar.stateByContent).containsKey(SceneA)
+        assertThat(foo.stateByContent).doesNotContainKey(SceneB)
+        assertThat(bar.stateByContent).doesNotContainKey(SceneB)
 
-        val fooInA = foo.sceneStates.getValue(SceneA)
-        val barInA = bar.sceneStates.getValue(SceneA)
+        val fooInA = foo.stateByContent.getValue(SceneA)
+        val barInA = bar.stateByContent.getValue(SceneA)
         assertThat(fooInA.lastOffset).isNotEqualTo(Offset.Unspecified)
         assertThat(fooInA.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
         assertThat(fooInA.lastScale).isNotEqualTo(Scale.Unspecified)
@@ -1903,11 +1903,11 @@
         rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsDisplayed()
         rule.onNode(isElement(TestElements.Bar, SceneB)).assertIsDisplayed()
 
-        assertThat(foo.sceneStates).containsKey(SceneB)
-        assertThat(bar.sceneStates).containsKey(SceneB)
+        assertThat(foo.stateByContent).containsKey(SceneB)
+        assertThat(bar.stateByContent).containsKey(SceneB)
 
-        val fooInB = foo.sceneStates.getValue(SceneB)
-        val barInB = bar.sceneStates.getValue(SceneB)
+        val fooInB = foo.stateByContent.getValue(SceneB)
+        val barInB = bar.stateByContent.getValue(SceneB)
         assertThat(fooInA.lastOffset).isEqualTo(Offset.Unspecified)
         assertThat(fooInA.lastAlpha).isEqualTo(Element.AlphaUnspecified)
         assertThat(fooInA.lastScale).isEqualTo(Scale.Unspecified)
@@ -1938,8 +1938,8 @@
             }
 
         @Composable
-        fun SceneScope.Foo() {
-            Box(Modifier.testTag("fooParentIn${sceneKey.debugName}")) {
+        fun ContentScope.Foo() {
+            Box(Modifier.testTag("fooParentIn${contentKey.debugName}")) {
                 Box(Modifier.element(TestElements.Foo).size(20.dp))
             }
         }
@@ -1973,7 +1973,7 @@
         val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
 
         @Composable
-        fun SceneScope.Foo(offset: Dp) {
+        fun ContentScope.Foo(offset: Dp) {
             Box(Modifier.fillMaxSize()) {
                 Box(Modifier.offset(offset, offset).element(TestElements.Foo).size(20.dp))
             }
@@ -2041,7 +2041,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo() {
+        fun ContentScope.Foo() {
             Box(Modifier.element(TestElements.Foo).size(10.dp))
         }
 
@@ -2062,7 +2062,11 @@
         rule.waitForIdle()
 
         assertThat(
-                layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB).lastSize
+                layoutImpl.elements
+                    .getValue(TestElements.Foo)
+                    .stateByContent
+                    .getValue(SceneB)
+                    .lastSize
             )
             .isEqualTo(Element.SizeUnspecified)
     }
@@ -2078,8 +2082,8 @@
                             // In A => B, Foo is not shared and first fades out from A then fades in
                             // B.
                             sharedElement(TestElements.Foo, enabled = false)
-                            fractionRange(end = 0.5f) { fade(TestElements.Foo.inScene(SceneA)) }
-                            fractionRange(start = 0.5f) { fade(TestElements.Foo.inScene(SceneB)) }
+                            fractionRange(end = 0.5f) { fade(TestElements.Foo.inContent(SceneA)) }
+                            fractionRange(start = 0.5f) { fade(TestElements.Foo.inContent(SceneB)) }
                         }
 
                         from(SceneB, to = SceneA) {
@@ -2091,7 +2095,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo(modifier: Modifier = Modifier) {
+        fun ContentScope.Foo(modifier: Modifier = Modifier) {
             Box(modifier.element(TestElements.Foo).size(10.dp))
         }
 
@@ -2149,7 +2153,7 @@
         val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
 
         @Composable
-        fun SceneScope.Foo(modifier: Modifier = Modifier) {
+        fun ContentScope.Foo(modifier: Modifier = Modifier) {
             Box(modifier.element(TestElements.Foo).size(10.dp))
         }
 
@@ -2216,7 +2220,7 @@
 
         // verify that preview transition for exiting elements is halfway played from
         // current-scene-value -> preview-target-value
-        val exiting1InB = layoutImpl.elements.getValue(exiting1).sceneStates.getValue(SceneB)
+        val exiting1InB = layoutImpl.elements.getValue(exiting1).stateByContent.getValue(SceneB)
         // e.g. exiting1 is half scaled...
         assertThat(exiting1InB.lastScale).isEqualTo(Scale(0.9f, 0.9f, Offset.Unspecified))
         // ...and exiting2 is halfway translated from 0.dp to 20.dp...
@@ -2228,7 +2232,7 @@
         // verify that preview transition for entering elements is halfway played from
         // preview-target-value -> transition-target-value (or target-scene-value if no
         // transition-target-value defined).
-        val entering1InA = layoutImpl.elements.getValue(entering1).sceneStates.getValue(SceneA)
+        val entering1InA = layoutImpl.elements.getValue(entering1).stateByContent.getValue(SceneA)
         // e.g. entering1 is half scaled between 0f and 0.5f -> 0.25f...
         assertThat(entering1InA.lastScale).isEqualTo(Scale(0.25f, 0.25f, Offset.Unspecified))
         // ...and entering2 is half way translated between 30.dp and 0.dp
@@ -2272,7 +2276,7 @@
 
         // verify that exiting elements remain in the preview-end state if no further transition is
         // defined for them in the second stage
-        val exiting1InB = layoutImpl.elements.getValue(exiting1).sceneStates.getValue(SceneB)
+        val exiting1InB = layoutImpl.elements.getValue(exiting1).stateByContent.getValue(SceneB)
         // i.e. exiting1 remains half scaled
         assertThat(exiting1InB.lastScale).isEqualTo(Scale(0.9f, 0.9f, Offset.Unspecified))
         // in case there is an additional transition defined for the second stage, verify that the
@@ -2286,7 +2290,7 @@
         rule.onNode(isElement(exiting3)).assertSizeIsEqualTo(90.dp, 90.dp)
 
         // verify that entering elements animate seamlessly to their target state
-        val entering1InA = layoutImpl.elements.getValue(entering1).sceneStates.getValue(SceneA)
+        val entering1InA = layoutImpl.elements.getValue(entering1).stateByContent.getValue(SceneA)
         // e.g. entering1, which was scaled from 0f to 0.25f during the preview phase, should now be
         // half way scaled between 0.25f and its target-state of 1f -> 0.625f
         assertThat(entering1InA.lastScale).isEqualTo(Scale(0.625f, 0.625f, Offset.Unspecified))
@@ -2318,7 +2322,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo(elementKey: ElementKey) {
+        fun ContentScope.Foo(elementKey: ElementKey) {
             Box(Modifier.element(elementKey).size(100.dp))
         }
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
index 9523896..821cc29 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
@@ -62,7 +62,7 @@
     }
 
     @Composable
-    private fun SceneScope.MovableCounter(key: ElementKey, modifier: Modifier) {
+    private fun ContentScope.MovableCounter(key: ElementKey, modifier: Modifier) {
         MovableElement(key, modifier) { content { Counter() } }
     }
 
@@ -264,7 +264,7 @@
     @Test
     fun movableElementContentIsRecomposedIfContentParametersChange() {
         @Composable
-        fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
+        fun ContentScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
             MovableElement(TestElements.Foo, modifier) { content { Text(text) } }
         }
 
@@ -298,7 +298,7 @@
     @Test
     fun elementScopeExtendsBoxScope() {
         rule.setContent {
-            TestSceneScope {
+            TestContentScope {
                 Element(TestElements.Foo, Modifier.size(200.dp)) {
                     content {
                         Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd))
@@ -315,7 +315,7 @@
     @Test
     fun movableElementScopeExtendsBoxScope() {
         rule.setContent {
-            TestSceneScope {
+            TestContentScope {
                 MovableElement(TestElements.Foo, Modifier.size(200.dp)) {
                     content {
                         Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
index 311a580..9ebc426 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
@@ -48,7 +48,7 @@
     private val layoutHeight = 400.dp
 
     private fun setup2ScenesAndScrollTouchSlop(
-        modifierSceneA: @Composable SceneScope.() -> Modifier = { Modifier },
+        modifierSceneA: @Composable ContentScope.() -> Modifier = { Modifier },
     ): MutableSceneTransitionLayoutState {
         val state =
             rule.runOnUiThread {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 1ec1079..32f3bac 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -129,7 +129,7 @@
     }
 
     @Composable
-    private fun SceneScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
+    private fun ContentScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
         Element(TestElements.Foo, modifier.size(size).background(Color.Red)) {
             // Offset the single child of Foo by some animated shared offset.
             val offset by animateElementDpAsState(childOffset, TestValues.Value1)
@@ -479,14 +479,14 @@
     fun sceneKeyInScope() {
         val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
 
-        var keyInA: SceneKey? = null
-        var keyInB: SceneKey? = null
-        var keyInC: SceneKey? = null
+        var keyInA: ContentKey? = null
+        var keyInB: ContentKey? = null
+        var keyInC: ContentKey? = null
         rule.setContent {
             SceneTransitionLayout(state) {
-                scene(SceneA) { keyInA = sceneKey }
-                scene(SceneB) { keyInB = sceneKey }
-                scene(SceneC) { keyInC = sceneKey }
+                scene(SceneA) { keyInA = contentKey }
+                scene(SceneB) { keyInB = contentKey }
+                scene(SceneC) { keyInC = contentKey }
             }
         }
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
index 6233608..c9f71da 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
@@ -25,7 +25,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.ContentScope
 import com.android.compose.animation.scene.TestElements
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.TransitionRecordingSpec
@@ -108,8 +108,8 @@
     }
 
     private fun assertBarSizeMatchesGolden(
-        fromSceneContent: @Composable SceneScope.() -> Unit,
-        toSceneContent: @Composable SceneScope.() -> Unit,
+        fromSceneContent: @Composable ContentScope.() -> Unit,
+        toSceneContent: @Composable ContentScope.() -> Unit,
         transition: TransitionBuilder.() -> Unit,
     ) {
         val recordingSpec =
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
index 8001f41..00acb13 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
@@ -31,7 +31,7 @@
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TestElements
 import com.android.compose.animation.scene.TestScenes
-import com.android.compose.animation.scene.inScene
+import com.android.compose.animation.scene.inContent
 import com.android.compose.animation.scene.testTransition
 import com.android.compose.test.assertSizeIsEqualTo
 import org.junit.Rule
@@ -125,10 +125,10 @@
                 sharedElement(TestElements.Foo, enabled = false)
 
                 // In SceneA, Foo leaves to the left edge.
-                translate(TestElements.Foo.inScene(TestScenes.SceneA), Edge.Left)
+                translate(TestElements.Foo.inContent(TestScenes.SceneA), Edge.Left)
 
                 // In SceneB, Foo comes from the bottom edge.
-                translate(TestElements.Foo.inScene(TestScenes.SceneB), Edge.Bottom)
+                translate(TestElements.Foo.inContent(TestScenes.SceneB), Edge.Bottom)
             },
         ) {
             before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
similarity index 85%
rename from packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
rename to packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
index fbd557f..00adefb 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
@@ -20,11 +20,13 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 
-/** `SceneScope` for tests, which allows a single scene to be drawn in a [SceneTransitionLayout]. */
+/**
+ * [ContentScope] for tests, which allows a single scene to be drawn in a [SceneTransitionLayout].
+ */
 @Composable
-fun TestSceneScope(
+fun TestContentScope(
     modifier: Modifier = Modifier,
-    content: @Composable SceneScope.() -> Unit,
+    content: @Composable ContentScope.() -> Unit,
 ) {
     val currentScene = remember { SceneKey("current") }
     val state = remember { MutableSceneTransitionLayoutState(currentScene) }
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index a37d78e..7f26b98 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -18,9 +18,7 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -87,8 +85,8 @@
  * @sample com.android.compose.animation.scene.transformation.TranslateTest
  */
 fun ComposeContentTestRule.testTransition(
-    fromSceneContent: @Composable SceneScope.() -> Unit,
-    toSceneContent: @Composable SceneScope.() -> Unit,
+    fromSceneContent: @Composable ContentScope.() -> Unit,
+    toSceneContent: @Composable ContentScope.() -> Unit,
     transition: TransitionBuilder.() -> Unit,
     layoutModifier: Modifier = Modifier,
     fromScene: SceneKey = TestScenes.SceneA,
@@ -134,8 +132,8 @@
 
 /** Records the transition between two scenes of [transitionLayout][SceneTransitionLayout]. */
 fun MotionTestRule<ComposeToolkit>.recordTransition(
-    fromSceneContent: @Composable SceneScope.() -> Unit,
-    toSceneContent: @Composable SceneScope.() -> Unit,
+    fromSceneContent: @Composable ContentScope.() -> Unit,
+    toSceneContent: @Composable ContentScope.() -> Unit,
     transition: TransitionBuilder.() -> Unit,
     recordingSpec: TransitionRecordingSpec,
     layoutModifier: Modifier = Modifier,
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
index 9e857deb..362e23d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
@@ -17,10 +17,13 @@
 package com.android.systemui.shared.notifications.data.repository
 
 import android.provider.Settings
+import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
 import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
@@ -30,9 +33,10 @@
 
 /** Provides access to state related to notification settings. */
 class NotificationSettingsRepository(
-    private val scope: CoroutineScope,
+    private val backgroundScope: CoroutineScope,
     private val backgroundDispatcher: CoroutineDispatcher,
     private val secureSettingsRepository: SecureSettingsRepository,
+    systemSettingsRepository: SystemSettingsRepository,
 ) {
     val isNotificationHistoryEnabled: Flow<Boolean> =
         secureSettingsRepository
@@ -48,9 +52,7 @@
             )
             .map { it == 1 }
             .flowOn(backgroundDispatcher)
-            .stateIn(
-                scope = scope,
-            )
+            .stateIn(scope = backgroundScope)
 
     suspend fun setShowNotificationsOnLockscreenEnabled(enabled: Boolean) {
         withContext(backgroundDispatcher) {
@@ -60,4 +62,27 @@
             )
         }
     }
+
+    val isCooldownEnabled: StateFlow<Boolean> =
+        systemSettingsRepository
+            .intSetting(name = Settings.System.NOTIFICATION_COOLDOWN_ENABLED)
+            .map { it == 1 }
+            .flowOn(backgroundDispatcher)
+            .stateIn(
+                scope = backgroundScope,
+                started = SharingStarted.Eagerly,
+                initialValue = false,
+            )
+
+    /** The default duration for DND mode when enabled. See [Settings.Secure.ZEN_DURATION]. */
+    val zenDuration: StateFlow<Int> =
+        secureSettingsRepository
+            .intSetting(name = Settings.Secure.ZEN_DURATION)
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
+            .stateIn(
+                backgroundScope,
+                started = SharingStarted.Eagerly,
+                initialValue = ZEN_DURATION_PROMPT,
+            )
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
index b4105bd..a274f1d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
@@ -38,4 +38,5 @@
         val current = repository.isShowNotificationsOnLockScreenEnabled().value
         repository.setShowNotificationsOnLockscreenEnabled(!current)
     }
-}
+
+    val isCooldownEnabled = repository.isCooldownEnabled}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt
new file mode 100644
index 0000000..afe82fb
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.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.shared.settings.data.repository
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.provider.Settings
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+/**
+ * Defines interface for classes that can provide access to data from [Settings.System]. This
+ * repository doesn't guarantee to provide value across different users. For that see:
+ * [UserAwareSecureSettingsRepository] which does that for secure settings.
+ */
+interface SystemSettingsRepository {
+
+    /** Returns a [Flow] tracking the value of a setting as an [Int]. */
+    fun intSetting(
+        name: String,
+        defaultValue: Int = 0,
+    ): Flow<Int>
+
+    /** Updates the value of the setting with the given name. */
+    suspend fun setInt(
+        name: String,
+        value: Int,
+    )
+
+    suspend fun getInt(
+        name: String,
+        defaultValue: Int = 0,
+    ): Int
+
+    suspend fun getString(name: String): String?
+}
+
+class SystemSettingsRepositoryImpl(
+    private val contentResolver: ContentResolver,
+    private val backgroundDispatcher: CoroutineDispatcher,
+) : SystemSettingsRepository {
+
+    override fun intSetting(
+        name: String,
+        defaultValue: Int,
+    ): Flow<Int> {
+        return callbackFlow {
+                val observer =
+                    object : ContentObserver(null) {
+                        override fun onChange(selfChange: Boolean) {
+                            trySend(Unit)
+                        }
+                    }
+
+                contentResolver.registerContentObserver(
+                    Settings.System.getUriFor(name),
+                    /* notifyForDescendants= */ false,
+                    observer,
+                )
+                send(Unit)
+
+                awaitClose { contentResolver.unregisterContentObserver(observer) }
+            }
+            .map { Settings.System.getInt(contentResolver, name, defaultValue) }
+            // The above work is done on the background thread (which is important for accessing
+            // settings through the content resolver).
+            .flowOn(backgroundDispatcher)
+    }
+
+    override suspend fun setInt(name: String, value: Int) {
+        withContext(backgroundDispatcher) {
+            Settings.System.putInt(
+                contentResolver,
+                name,
+                value,
+            )
+        }
+    }
+
+    override suspend fun getInt(name: String, defaultValue: Int): Int {
+        return withContext(backgroundDispatcher) {
+            Settings.System.getInt(
+                contentResolver,
+                name,
+                defaultValue,
+            )
+        }
+    }
+
+    override suspend fun getString(name: String): String? {
+        return withContext(backgroundDispatcher) {
+            Settings.System.getString(
+                contentResolver,
+                name,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt
new file mode 100644
index 0000000..7da2b40
--- /dev/null
+++ b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.shared.settings.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+
+class FakeSystemSettingsRepository : SystemSettingsRepository {
+
+    private val settings = MutableStateFlow<Map<String, String>>(mutableMapOf())
+
+    override fun intSetting(name: String, defaultValue: Int): Flow<Int> {
+        return settings.map { it.getOrDefault(name, defaultValue.toString()) }.map { it.toInt() }
+    }
+
+    override suspend fun setInt(name: String, value: Int) {
+        settings.value = settings.value.toMutableMap().apply { this[name] = value.toString() }
+    }
+
+    override suspend fun getInt(name: String, defaultValue: Int): Int {
+        return settings.value[name]?.toInt() ?: defaultValue
+    }
+
+    override suspend fun getString(name: String): String? {
+        return settings.value[name]
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt
index c1816ed..d251585 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt
@@ -22,6 +22,7 @@
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_TIMER_FLICKER_FIX
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.smartspace.CommunalSmartspaceController
 import com.android.systemui.concurrency.fakeExecutor
@@ -97,6 +98,7 @@
         }
 
     @EnableFlags(FLAG_REMOTE_VIEWS)
+    @DisableFlags(FLAG_COMMUNAL_TIMER_FLICKER_FIX)
     @Test
     fun communalTimers_onlyShowTimersWithRemoteViews() =
         testScope.runTest {
@@ -138,6 +140,48 @@
             assertThat(communalTimers?.first()?.smartspaceTargetId).isEqualTo("timer-1-started")
         }
 
+    @EnableFlags(FLAG_REMOTE_VIEWS, FLAG_COMMUNAL_TIMER_FLICKER_FIX)
+    @Test
+    fun communalTimers_onlyShowTimersWithRemoteViews_timerFlickerFix() =
+        testScope.runTest {
+            underTest.startListening()
+
+            val communalTimers by collectLastValue(underTest.timers)
+            runCurrent()
+            fakeExecutor.runAllReady()
+
+            with(captureSmartspaceTargetListener()) {
+                onSmartspaceTargetsUpdated(
+                    listOf(
+                        // Invalid. Not a timer
+                        mock<SmartspaceTarget> {
+                            on { smartspaceTargetId }.doReturn("weather")
+                            on { featureType }.doReturn(SmartspaceTarget.FEATURE_WEATHER)
+                        },
+                        // Invalid. RemoteViews absent
+                        mock<SmartspaceTarget> {
+                            on { smartspaceTargetId }.doReturn("timer-0-started")
+                            on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                            on { remoteViews }.doReturn(null)
+                            on { creationTimeMillis }.doReturn(1000)
+                        },
+                        // Valid
+                        mock<SmartspaceTarget> {
+                            on { smartspaceTargetId }.doReturn("timer-1-started")
+                            on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                            on { remoteViews }.doReturn(mock())
+                            on { creationTimeMillis }.doReturn(2000)
+                        },
+                    )
+                )
+            }
+            runCurrent()
+
+            // Verify that only the valid target is listed
+            assertThat(communalTimers).hasSize(1)
+            assertThat(communalTimers?.first()?.smartspaceTargetId).isEqualTo("timer-1")
+        }
+
     @EnableFlags(FLAG_REMOTE_VIEWS)
     @Test
     fun communalTimers_cacheCreationTime() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 61487b0..57ce9de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.view.viewmodel
 
+import android.appwidget.AppWidgetProviderInfo
 import android.content.ActivityNotFoundException
 import android.content.ComponentName
 import android.content.Intent
@@ -24,6 +25,9 @@
 import android.content.pm.ResolveInfo
 import android.content.pm.UserInfo
 import android.provider.Settings
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
+import android.view.accessibility.accessibilityManager
 import android.widget.RemoteViews
 import androidx.activity.result.ActivityResultLauncher
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -42,7 +46,6 @@
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalInteractor
-import com.android.systemui.communal.domain.interactor.communalPrefsInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
@@ -61,8 +64,6 @@
 import com.android.systemui.settings.fakeUserTracker
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
@@ -77,8 +78,12 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -98,6 +103,7 @@
     private lateinit var mediaRepository: FakeCommunalMediaRepository
     private lateinit var communalSceneInteractor: CommunalSceneInteractor
     private lateinit var communalInteractor: CommunalInteractor
+    private lateinit var accessibilityManager: AccessibilityManager
 
     private val testableResources = context.orCreateTestableResources
 
@@ -119,6 +125,7 @@
             selectedUserIndex = 0,
         )
         kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
+        accessibilityManager = kosmos.accessibilityManager
 
         underTest =
             CommunalEditModeViewModel(
@@ -130,8 +137,10 @@
                 uiEventLogger,
                 logcatLogBuffer("CommunalEditModeViewModelTest"),
                 kosmos.testDispatcher,
-                kosmos.communalPrefsInteractor,
                 metricsLogger,
+                context,
+                accessibilityManager,
+                packageManager,
             )
     }
 
@@ -356,6 +365,37 @@
         verify(communalInteractor).setScrollPosition(eq(index), eq(offset))
     }
 
+    @Test
+    fun onNewWidgetAdded_accessibilityDisabled_doNothing() {
+        whenever(accessibilityManager.isEnabled).thenReturn(false)
+
+        val provider =
+            mock<AppWidgetProviderInfo> {
+                on { loadLabel(packageManager) }.thenReturn("Test Clock")
+            }
+        underTest.onNewWidgetAdded(provider)
+
+        verify(accessibilityManager, never()).sendAccessibilityEvent(any())
+    }
+
+    @Test
+    fun onNewWidgetAdded_accessibilityEnabled_sendAccessibilityAnnouncement() {
+        whenever(accessibilityManager.isEnabled).thenReturn(true)
+
+        val provider =
+            mock<AppWidgetProviderInfo> {
+                on { loadLabel(packageManager) }.thenReturn("Test Clock")
+            }
+        underTest.onNewWidgetAdded(provider)
+
+        val captor = argumentCaptor<AccessibilityEvent>()
+        verify(accessibilityManager).sendAccessibilityEvent(captor.capture())
+
+        val event = captor.firstValue
+        assertThat(event.eventType).isEqualTo(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+        assertThat(event.contentDescription).isEqualTo("Test Clock widget added to lock screen")
+    }
+
     private companion object {
         val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
         const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index 4c24ce2..5c09777 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -62,12 +62,13 @@
 import com.android.systemui.dreams.complication.HideComplicationTouchHandler
 import com.android.systemui.dreams.dagger.DreamOverlayComponent
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.gesture.domain.gestureInteractor
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.touch.TouchInsetManager
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
@@ -84,9 +85,11 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.isNull
-import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -166,6 +169,7 @@
     private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
     private lateinit var communalRepository: FakeCommunalSceneRepository
     private var viewCaptureSpy = spy(ViewCaptureFactory.getInstance(context))
+    private lateinit var gestureInteractor: GestureInteractor
 
     @Captor var mViewCaptor: ArgumentCaptor<View>? = null
     private lateinit var mService: DreamOverlayService
@@ -177,6 +181,7 @@
         lifecycleRegistry = FakeLifecycleRegistry(mLifecycleOwner)
         bouncerRepository = kosmos.fakeKeyguardBouncerRepository
         communalRepository = kosmos.fakeCommunalSceneRepository
+        gestureInteractor = spy(kosmos.gestureInteractor)
 
         whenever(mDreamOverlayComponent.getDreamOverlayContainerViewController())
             .thenReturn(mDreamOverlayContainerViewController)
@@ -231,6 +236,7 @@
                 HOME_CONTROL_PANEL_DREAM_COMPONENT,
                 mDreamOverlayCallbackController,
                 kosmos.keyguardInteractor,
+                gestureInteractor,
                 WINDOW_NAME
             )
     }
@@ -955,6 +961,47 @@
         assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
     }
 
+    @Test
+    fun testDreamActivityGesturesBlockedOnStart() {
+        val client = client
+
+        // Inform the overlay service of dream starting.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            false /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+        val captor = argumentCaptor<ComponentName>()
+        verify(gestureInteractor)
+            .addGestureBlockedActivity(captor.capture(), eq(GestureInteractor.Scope.Global))
+        assertThat(captor.firstValue.packageName)
+            .isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName)
+    }
+
+    @Test
+    fun testDreamActivityGesturesUnblockedOnEnd() {
+        val client = client
+
+        // Inform the overlay service of dream starting.
+        client.startDream(
+            mWindowParams,
+            mDreamOverlayCallback,
+            DREAM_COMPONENT,
+            false /*shouldShowComplication*/
+        )
+        mMainExecutor.runAllReady()
+
+        client.endDream()
+        mMainExecutor.runAllReady()
+        val captor = argumentCaptor<ComponentName>()
+        verify(gestureInteractor)
+            .removeGestureBlockedActivity(captor.capture(), eq(GestureInteractor.Scope.Global))
+        assertThat(captor.firstValue.packageName)
+            .isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName)
+    }
+
     internal class FakeLifecycleRegistry(provider: LifecycleOwner) : LifecycleRegistry(provider) {
         val mLifecycles: MutableList<State> = ArrayList()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt
new file mode 100644
index 0000000..91d37cf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.gesture.data
+
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepositoryImpl
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GestureRepositoryTest : SysuiTestCase() {
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+    private val underTest by lazy { GestureRepositoryImpl(testDispatcher) }
+
+    @Test
+    fun addRemoveComponentToBlock_updatesBlockedComponentSet() =
+        testScope.runTest {
+            val component = mock<ComponentName>()
+
+            underTest.addGestureBlockedActivity(component)
+            val addedBlockedComponents by collectLastValue(underTest.gestureBlockedActivities)
+            assertThat(addedBlockedComponents).contains(component)
+
+            underTest.removeGestureBlockedActivity(component)
+            val removedBlockedComponents by collectLastValue(underTest.gestureBlockedActivities)
+            assertThat(removedBlockedComponents).isEmpty()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
new file mode 100644
index 0000000..bc142e6
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.gesture.domain
+
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GestureInteractorTest : SysuiTestCase() {
+    @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    val dispatcher = StandardTestDispatcher()
+    val testScope = TestScope(dispatcher)
+
+    @Mock private lateinit var gestureRepository: GestureRepository
+
+    private val underTest by lazy {
+        GestureInteractor(gestureRepository, testScope.backgroundScope)
+    }
+
+    @Before
+    fun setup() {
+        Dispatchers.setMain(dispatcher)
+        whenever(gestureRepository.gestureBlockedActivities).thenReturn(MutableStateFlow(setOf()))
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+    }
+
+    @Test
+    fun addBlockedActivity_testCombination() =
+        testScope.runTest {
+            val globalComponent = mock<ComponentName>()
+            whenever(gestureRepository.gestureBlockedActivities)
+                .thenReturn(MutableStateFlow(setOf(globalComponent)))
+            val localComponent = mock<ComponentName>()
+            underTest.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local)
+            val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
+            testScope.runCurrent()
+            verify(gestureRepository, never()).addGestureBlockedActivity(any())
+            assertThat(lastSeen).hasSize(2)
+            assertThat(lastSeen).containsExactly(globalComponent, localComponent)
+        }
+
+    @Test
+    fun addBlockedActivityLocally_onlyAffectsLocalInteractor() =
+        testScope.runTest {
+            val component = mock<ComponentName>()
+            underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
+            val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
+            testScope.runCurrent()
+            verify(gestureRepository, never()).addGestureBlockedActivity(any())
+            assertThat(lastSeen).contains(component)
+        }
+
+    @Test
+    fun removeBlockedActivityLocally_onlyAffectsLocalInteractor() =
+        testScope.runTest {
+            val component = mock<ComponentName>()
+            underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
+            val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
+            testScope.runCurrent()
+            underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Local)
+            testScope.runCurrent()
+            verify(gestureRepository, never()).removeGestureBlockedActivity(any())
+            assertThat(lastSeen).isEmpty()
+        }
+
+    @Test
+    fun addBlockedActivity_invokesRepository() =
+        testScope.runTest {
+            val component = mock<ComponentName>()
+            underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Global)
+            runCurrent()
+            val captor = argumentCaptor<ComponentName>()
+            verify(gestureRepository).addGestureBlockedActivity(captor.capture())
+            assertThat(captor.firstValue).isEqualTo(component)
+        }
+
+    @Test
+    fun removeBlockedActivity_invokesRepository() =
+        testScope.runTest {
+            val component = mock<ComponentName>()
+            underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Global)
+            runCurrent()
+            val captor = argumentCaptor<ComponentName>()
+            verify(gestureRepository).removeGestureBlockedActivity(captor.capture())
+            assertThat(captor.firstValue).isEqualTo(component)
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
index 59802ef..273e3cb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.haptics.vibratorHelper
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.qs.qsTileFactory
 import com.android.systemui.statusbar.policy.keyguardStateController
 import com.android.systemui.testKosmos
@@ -72,6 +73,7 @@
             QSLongPressEffect(
                 vibratorHelper,
                 kosmos.keyguardStateController,
+                FakeLogBuffer.Factory.create(),
             )
         longPressEffect.callback = callback
         longPressEffect.qsTile = qsTile
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..f74b74a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AlternateBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() {
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+
+    val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
+
+    val underTest = kosmos.alternateBouncerToLockscreenTransitionViewModel
+
+    @Test
+    fun lockscreenAlpha_zeroInitialAlpha() =
+        testScope.runTest {
+            // ViewState starts at 0 alpha.
+            val viewState = ViewStateAccessor(alpha = { 0f })
+            val alpha by collectValues(underTest.lockscreenAlpha(viewState))
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.ALTERNATE_BOUNCER,
+                to = KeyguardState.LOCKSCREEN,
+                testScope
+            )
+
+            assertThat(alpha[0]).isEqualTo(0f)
+            // alpha duration is 250ms of the 300ms total, so 0.5f of the total is 0.6
+            assertThat(alpha[1]).isEqualTo(0.6f)
+            assertThat(alpha[2]).isEqualTo(1f)
+        }
+
+    @Test
+    fun deviceEntryParentViewAlpha() =
+        testScope.runTest {
+            val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha)
+
+            // immediately 1f
+            keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+            keyguardTransitionRepository.sendTransitionStep(step(0.4f))
+            assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+            keyguardTransitionRepository.sendTransitionStep(step(.85f))
+            assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+
+            keyguardTransitionRepository.sendTransitionStep(step(1f))
+            assertThat(deviceEntryParentViewAlpha).isEqualTo(1f)
+        }
+
+    @Test
+    fun deviceEntryBackgroundViewAlpha_udfpsEnrolled_show() =
+        testScope.runTest {
+            fingerprintPropertyRepository.supportsUdfps()
+            val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
+            runCurrent()
+
+            // immediately 1f
+            keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            assertThat(bgViewAlpha).isEqualTo(1f)
+
+            keyguardTransitionRepository.sendTransitionStep(step(0.1f))
+            assertThat(bgViewAlpha).isEqualTo(1f)
+
+            keyguardTransitionRepository.sendTransitionStep(step(.3f))
+            assertThat(bgViewAlpha).isEqualTo(1f)
+
+            keyguardTransitionRepository.sendTransitionStep(step(.5f))
+            assertThat(bgViewAlpha).isEqualTo(1f)
+
+            keyguardTransitionRepository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(bgViewAlpha).isEqualTo(1f)
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.ALTERNATE_BOUNCER,
+            to = KeyguardState.LOCKSCREEN,
+            value = value,
+            transitionState = state,
+            ownerName = "AlternateBouncerToLockscreenTransitionViewModelTest"
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 6ad4b31..3146318 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -675,6 +675,24 @@
             assertThat(tiles!!.size).isEqualTo(3)
         }
 
+    @Test
+    fun changeInPackagesTiles_doesntTriggerUserChange_logged() =
+        testScope.runTest(USER_INFO_0) {
+            val specs =
+                listOf(
+                    TileSpec.create("a"),
+                )
+            tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+            runCurrent()
+            // Settled on the same list of tiles.
+            assertThat(underTest.currentTilesSpecs).isEqualTo(specs)
+
+            installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
+            runCurrent()
+
+            verify(logger, never()).logTileUserChanged(TileSpec.create("a"), 0)
+        }
+
     private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) {
         this.state = state
         this.label = label
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
index 09dca25..69b8ee1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
@@ -22,11 +22,14 @@
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.settingslib.notification.data.repository.FakeZenModeRepository
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
@@ -40,51 +43,72 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ModesTileDataInteractorTest : SysuiTestCase() {
-    private val zenModeRepository = FakeZenModeRepository()
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val dispatcher = kosmos.testDispatcher
+    private val zenModeRepository = kosmos.fakeZenModeRepository
 
-    private val underTest = ModesTileDataInteractor(zenModeRepository)
+    private val underTest = ModesTileDataInteractor(zenModeRepository, dispatcher)
 
     @EnableFlags(Flags.FLAG_MODES_UI)
     @Test
-    fun availableWhenFlagIsOn() = runTest {
-        val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
+    fun availableWhenFlagIsOn() =
+        testScope.runTest {
+            val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
 
-        assertThat(availability).containsExactly(true)
-    }
+            assertThat(availability).containsExactly(true)
+        }
 
     @DisableFlags(Flags.FLAG_MODES_UI)
     @Test
-    fun unavailableWhenFlagIsOff() = runTest {
-        val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
+    fun unavailableWhenFlagIsOff() =
+        testScope.runTest {
+            val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
 
-        assertThat(availability).containsExactly(false)
-    }
+            assertThat(availability).containsExactly(false)
+        }
 
     @EnableFlags(Flags.FLAG_MODES_UI)
     @Test
-    fun isActivatedWhenModesChange() = runTest {
-        val dataList: List<ModesTileModel> by
-            collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
-        runCurrent()
-        assertThat(dataList.map { it.isActivated }).containsExactly(false).inOrder()
+    fun isActivatedWhenModesChange() =
+        testScope.runTest {
+            val dataList: List<ModesTileModel> by
+                collectValues(
+                    underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            runCurrent()
+            assertThat(dataList.map { it.isActivated }).containsExactly(false).inOrder()
 
-        // Add active mode
-        zenModeRepository.addMode(id = "One", active = true)
-        runCurrent()
-        assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()
+            // Add active mode
+            zenModeRepository.addMode(id = "One", active = true)
+            runCurrent()
+            assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()
+            assertThat(dataList.map { it.activeModes }.last()).containsExactly("Mode One")
 
-        // Add another mode: state hasn't changed, so this shouldn't cause another emission
-        zenModeRepository.addMode(id = "Two", active = true)
-        runCurrent()
-        assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()
+            // Add an inactive mode: state hasn't changed, so this shouldn't cause another emission
+            zenModeRepository.addMode(id = "Two", active = false)
+            runCurrent()
+            assertThat(dataList.map { it.isActivated }).containsExactly(false, true).inOrder()
+            assertThat(dataList.map { it.activeModes }.last()).containsExactly("Mode One")
 
-        // Remove a mode and disable the other
-        zenModeRepository.removeMode("One")
-        runCurrent()
-        zenModeRepository.deactivateMode("Two")
-        runCurrent()
-        assertThat(dataList.map { it.isActivated }).containsExactly(false, true, false).inOrder()
-    }
+            // Add another active mode
+            zenModeRepository.addMode(id = "Three", active = true)
+            runCurrent()
+            assertThat(dataList.map { it.isActivated }).containsExactly(false, true, true).inOrder()
+            assertThat(dataList.map { it.activeModes }.last())
+                .containsExactly("Mode One", "Mode Three")
+                .inOrder()
+
+            // Remove a mode and deactivate the other
+            zenModeRepository.removeMode("One")
+            runCurrent()
+            zenModeRepository.deactivateMode("Three")
+            runCurrent()
+            assertThat(dataList.map { it.isActivated })
+                .containsExactly(false, true, true, true, false)
+                .inOrder()
+            assertThat(dataList.map { it.activeModes }.last()).isEmpty()
+        }
 
     private companion object {
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
index a67e7c6..4b75649 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
@@ -54,7 +54,11 @@
     fun handleClick_active() = runTest {
         val expandable = mock<Expandable>()
         underTest.handleInput(
-            QSTileInputTestKtx.click(data = ModesTileModel(true), expandable = expandable))
+            QSTileInputTestKtx.click(
+                data = ModesTileModel(true, listOf("DND")),
+                expandable = expandable
+            )
+        )
 
         verify(mockDialogDelegate).showDialog(eq(expandable))
     }
@@ -63,14 +67,18 @@
     fun handleClick_inactive() = runTest {
         val expandable = mock<Expandable>()
         underTest.handleInput(
-            QSTileInputTestKtx.click(data = ModesTileModel(false), expandable = expandable))
+            QSTileInputTestKtx.click(
+                data = ModesTileModel(false, emptyList()),
+                expandable = expandable
+            )
+        )
 
         verify(mockDialogDelegate).showDialog(eq(expandable))
     }
 
     @Test
     fun handleLongClick_active() = runTest {
-        underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true)))
+        underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true, listOf("DND"))))
 
         QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
             assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
@@ -79,7 +87,7 @@
 
     @Test
     fun handleLongClick_inactive() = runTest {
-        underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false)))
+        underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false, emptyList())))
 
         QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
             assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
index 3baf2f4..dd9711e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
@@ -54,21 +54,35 @@
 
     @Test
     fun inactiveState() {
-        val model = ModesTileModel(isActivated = false)
+        val model = ModesTileModel(isActivated = false, activeModes = emptyList())
 
         val state = underTest.map(config, model)
 
         assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE)
         assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_off)
+        assertThat(state.secondaryLabel).isEqualTo("No active modes")
     }
 
     @Test
-    fun activeState() {
-        val model = ModesTileModel(isActivated = true)
+    fun activeState_oneMode() {
+        val model = ModesTileModel(isActivated = true, activeModes = listOf("DND"))
 
         val state = underTest.map(config, model)
 
         assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
         assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_on)
+        assertThat(state.secondaryLabel).isEqualTo("DND is active")
+    }
+
+    @Test
+    fun activeState_multipleModes() {
+        val model =
+            ModesTileModel(isActivated = true, activeModes = listOf("Mode 1", "Mode 2", "Mode 3"))
+
+        val state = underTest.map(config, model)
+
+        assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
+        assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_on)
+        assertThat(state.secondaryLabel).isEqualTo("3 modes are active")
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index 09580c5..d472d98 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -24,9 +24,11 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.displayStateRepository
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testCase
@@ -117,6 +119,7 @@
         }
 
     private val shadeInteractor = kosmos.shadeInteractor
+    private val displayStateInteractor = kosmos.displayStateInteractor
     private val dumpManager = mock<DumpManager>()
 
     private val underTest =
@@ -124,6 +127,7 @@
             qsSceneComponentFactory,
             qsImplProvider,
             shadeInteractor,
+            displayStateInteractor,
             dumpManager,
             testDispatcher,
             testScope.backgroundScope,
@@ -583,6 +587,25 @@
         }
 
     @Test
+    fun setIsNotificationPanelFullWidth() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            kosmos.displayStateRepository.setIsLargeScreen(true)
+            runCurrent()
+
+            verify(qsImpl!!).setIsNotificationPanelFullWidth(false)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            verify(qsImpl!!).setIsNotificationPanelFullWidth(false)
+        }
+
+    @Test
     fun setBrightnessMirrorController() =
         testScope.runTest {
             val qsImpl by collectLastValue(underTest.qsImpl)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 4a7b887..34fbcac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -60,6 +60,7 @@
 import com.android.systemui.res.R;
 import com.android.systemui.scene.FakeWindowRootViewComponent;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.DozeParameters;
@@ -106,6 +107,7 @@
     @Mock private ShadeWindowLogger mShadeWindowLogger;
     @Mock private SelectedUserInteractor mSelectedUserInteractor;
     @Mock private UserTracker mUserTracker;
+    @Mock private NotificationShadeWindowModel mNotificationShadeWindowModel;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
 
@@ -161,6 +163,7 @@
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
                 mUserTracker,
+                mNotificationShadeWindowModel,
                 mKosmos::getCommunalInteractor) {
                     @Override
                     protected boolean isDebuggable() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index cecc70c..d37e0fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.displayStateRepository
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.parameterizeSceneContainerFlag
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
@@ -41,6 +42,8 @@
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
+import com.android.systemui.statusbar.phone.scrimController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
@@ -54,6 +57,7 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.verify
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
@@ -167,6 +171,18 @@
             }
         }
 
+    @Test
+    @EnableSceneContainer
+    fun hydrateFullWidth() =
+        testScope.runTest {
+            underTest.start()
+
+            kosmos.displayStateRepository.setIsLargeScreen(true)
+            runCurrent()
+            verify(kosmos.notificationStackScrollLayoutController).setIsFullWidth(false)
+            assertThat(kosmos.scrimController.clipQsScrim).isFalse()
+        }
+
     private fun TestScope.changeScene(
         toScene: SceneKey,
         transitionState: MutableStateFlow<ObservableTransitionState>,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModelTest.kt
new file mode 100644
index 0000000..add33da
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModelTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationShadeWindowModelTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+    private val underTest: NotificationShadeWindowModel by lazy {
+        kosmos.notificationShadeWindowModel
+    }
+
+    @Test
+    fun transitionToOccluded() =
+        testScope.runTest {
+            val isKeyguardOccluded by collectLastValue(underTest.isKeyguardOccluded)
+            assertThat(isKeyguardOccluded).isFalse()
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.OCCLUDED,
+                testScope,
+            )
+            assertThat(isKeyguardOccluded).isTrue()
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.OCCLUDED,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+            assertThat(isKeyguardOccluded).isFalse()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 6f1bc7e..9fea7a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -19,12 +19,10 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
-import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX
 import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
@@ -38,8 +36,8 @@
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.Flags
-import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.flags.parameterizeSceneContainerFlag
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -89,10 +87,7 @@
         @JvmStatic
         @Parameters(name = "{0}")
         fun getParams(): List<FlagsParameterization> {
-            return FlagsParameterization.allCombinationsOf(
-                    FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX,
-                )
-                .andSceneContainer()
+            return parameterizeSceneContainerFlag()
         }
     }
 
@@ -177,25 +172,6 @@
         }
 
     @Test
-    @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
-    fun validatePaddingTopInSplitShade_refactorFlagOff_usesLargeHeaderResource() =
-        testScope.runTest {
-            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
-            overrideResource(R.bool.config_use_split_notification_shade, true)
-            overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, 10)
-            overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
-
-            val paddingTop by collectLastValue(underTest.paddingTopDimen)
-
-            configurationRepository.onAnyConfigurationChange()
-
-            // Should directly use the header height (flagged off value)
-            assertThat(paddingTop).isEqualTo(10)
-        }
-
-    @Test
-    @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
     fun validatePaddingTopInSplitShade_refactorFlagOn_usesLargeHeaderHelper() =
         testScope.runTest {
             whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
@@ -267,49 +243,8 @@
         }
 
     @Test
-    @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
     @DisableSceneContainer
-    fun validateMarginTopWithLargeScreenHeader_refactorFlagOff_usesResource() =
-        testScope.runTest {
-            val headerResourceHeight = 50
-            val headerHelperHeight = 100
-            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
-                .thenReturn(headerHelperHeight)
-            overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
-            overrideResource(R.dimen.notification_panel_margin_top, 0)
-
-            val dimens by collectLastValue(underTest.configurationBasedDimensions)
-
-            configurationRepository.onAnyConfigurationChange()
-
-            assertThat(dimens!!.marginTop).isEqualTo(headerResourceHeight)
-        }
-
-    @Test
-    @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
-    @EnableSceneContainer
-    fun validateMarginTopWithLargeScreenHeader_refactorFlagOff_sceneContainerFlagOn_stillZero() =
-        testScope.runTest {
-            val headerResourceHeight = 50
-            val headerHelperHeight = 100
-            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
-                .thenReturn(headerHelperHeight)
-            overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
-            overrideResource(R.dimen.notification_panel_margin_top, 0)
-
-            val dimens by collectLastValue(underTest.configurationBasedDimensions)
-
-            configurationRepository.onAnyConfigurationChange()
-
-            assertThat(dimens!!.marginTop).isEqualTo(0)
-        }
-
-    @Test
-    @DisableSceneContainer
-    @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
-    fun validateMarginTopWithLargeScreenHeader_refactorFlagOn_usesHelper() =
+    fun validateMarginTopWithLargeScreenHeader_usesHelper() =
         testScope.runTest {
             val headerResourceHeight = 50
             val headerHelperHeight = 100
@@ -328,7 +263,6 @@
 
     @Test
     @EnableSceneContainer
-    @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
     fun validateMarginTopWithLargeScreenHeader_sceneContainerFlagOn_stillZero() =
         testScope.runTest {
             val headerResourceHeight = 50
@@ -626,44 +560,8 @@
         }
 
     @Test
-    @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
     @DisableSceneContainer
-    fun boundsOnLockscreenInSplitShade_refactorFlagOff_usesLargeHeaderResource() =
-        testScope.runTest {
-            val bounds by collectLastValue(underTest.bounds)
-
-            // When in split shade
-            whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
-            overrideResource(R.bool.config_use_split_notification_shade, true)
-            overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, 10)
-            overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
-
-            configurationRepository.onAnyConfigurationChange()
-            runCurrent()
-
-            // Start on lockscreen
-            showLockscreen()
-
-            keyguardInteractor.setNotificationContainerBounds(
-                NotificationContainerBounds(top = 1f, bottom = 52f)
-            )
-            runCurrent()
-
-            // Top should be equal to bounds (1) - padding adjustment (10)
-            assertThat(bounds)
-                .isEqualTo(
-                    NotificationContainerBounds(
-                        top = -9f,
-                        bottom = 2f,
-                    )
-                )
-        }
-
-    @Test
-    @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
-    @DisableSceneContainer
-    fun boundsOnLockscreenInSplitShade_refactorFlagOn_usesLargeHeaderHelper() =
+    fun boundsOnLockscreenInSplitShade_usesLargeHeaderHelper() =
         testScope.runTest {
             val bounds by collectLastValue(underTest.bounds)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 88431f0..32f66c1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -18,15 +18,21 @@
 
 import android.app.NotificationManager.Policy
 import android.provider.Settings
+import android.provider.Settings.Secure.ZEN_DURATION
+import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
+import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.notification.data.repository.updateNotificationPolicy
+import com.android.settingslib.notification.modes.TestModeBuilder
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
 import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import java.time.Duration
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -39,7 +45,8 @@
 class ZenModeInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val repository = kosmos.fakeZenModeRepository
+    private val zenModeRepository = kosmos.fakeZenModeRepository
+    private val settingsRepository = kosmos.secureSettingsRepository
 
     private val underTest = kosmos.zenModeInteractor
 
@@ -48,7 +55,7 @@
         testScope.runTest {
             val enabled by collectLastValue(underTest.isZenModeEnabled)
 
-            repository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
             runCurrent()
 
             assertThat(enabled).isFalse()
@@ -59,7 +66,7 @@
         testScope.runTest {
             val enabled by collectLastValue(underTest.isZenModeEnabled)
 
-            repository.updateZenMode(Settings.Global.ZEN_MODE_ALARMS)
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_ALARMS)
             runCurrent()
 
             assertThat(enabled).isTrue()
@@ -70,7 +77,7 @@
         testScope.runTest {
             val enabled by collectLastValue(underTest.isZenModeEnabled)
 
-            repository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
             runCurrent()
 
             assertThat(enabled).isTrue()
@@ -81,7 +88,7 @@
         testScope.runTest {
             val enabled by collectLastValue(underTest.isZenModeEnabled)
 
-            repository.updateZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
             runCurrent()
 
             assertThat(enabled).isTrue()
@@ -92,7 +99,8 @@
         testScope.runTest {
             val enabled by collectLastValue(underTest.isZenModeEnabled)
 
-            repository.updateZenMode(4) // this should fail if we ever add another zen mode type
+            // this should fail if we ever add another zen mode type
+            zenModeRepository.updateZenMode(4)
             runCurrent()
 
             assertThat(enabled).isFalse()
@@ -103,8 +111,8 @@
         testScope.runTest {
             val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
 
-            repository.updateNotificationPolicy(null)
-            repository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            zenModeRepository.updateNotificationPolicy(null)
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
             runCurrent()
 
             assertThat(hidden).isFalse()
@@ -115,10 +123,10 @@
         testScope.runTest {
             val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
 
-            repository.updateNotificationPolicy(
+            zenModeRepository.updateNotificationPolicy(
                 suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
             )
-            repository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
             runCurrent()
 
             assertThat(hidden).isFalse()
@@ -129,10 +137,10 @@
         testScope.runTest {
             val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
 
-            repository.updateNotificationPolicy(
+            zenModeRepository.updateNotificationPolicy(
                 suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_STATUS_BAR
             )
-            repository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
             runCurrent()
 
             assertThat(hidden).isFalse()
@@ -143,12 +151,70 @@
         testScope.runTest {
             val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
 
-            repository.updateNotificationPolicy(
+            zenModeRepository.updateNotificationPolicy(
                 suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
             )
-            repository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
             runCurrent()
 
             assertThat(hidden).isTrue()
         }
+
+    @Test
+    fun shouldAskForZenDuration_falseForNonManualDnd() =
+        testScope.runTest {
+            settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_PROMPT)
+            runCurrent()
+
+            assertThat(underTest.shouldAskForZenDuration(TestModeBuilder.EXAMPLE)).isFalse()
+        }
+
+    @Test
+    fun shouldAskForZenDuration_changesWithSetting() =
+        testScope.runTest {
+            val manualDnd = TestModeBuilder.MANUAL_DND
+
+            settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER)
+            runCurrent()
+
+            assertThat(underTest.shouldAskForZenDuration(manualDnd)).isFalse()
+
+            settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_PROMPT)
+            runCurrent()
+
+            assertThat(underTest.shouldAskForZenDuration(manualDnd)).isTrue()
+        }
+
+    @Test
+    fun activateMode_nonManualDnd() =
+        testScope.runTest {
+            val mode = TestModeBuilder().setActive(false).build()
+            zenModeRepository.addModes(listOf(mode))
+            settingsRepository.setInt(ZEN_DURATION, 60)
+            runCurrent()
+
+            underTest.activateMode(mode)
+            assertThat(zenModeRepository.getMode(mode.id)?.isActive).isTrue()
+            assertThat(zenModeRepository.getModeActiveDuration(mode.id)).isNull()
+        }
+
+    @Test
+    fun activateMode_usesCorrectDuration() =
+        testScope.runTest {
+            val manualDnd = TestModeBuilder.MANUAL_DND
+            zenModeRepository.addModes(listOf(manualDnd))
+            settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER)
+            runCurrent()
+
+            underTest.activateMode(manualDnd)
+            assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)).isNull()
+
+            zenModeRepository.deactivateMode(manualDnd.id)
+            settingsRepository.setInt(ZEN_DURATION, 60)
+            runCurrent()
+
+            underTest.activateMode(manualDnd)
+            assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id))
+                .isEqualTo(Duration.ofMinutes(60))
+        }
 }
diff --git a/packages/SystemUI/res/drawable/brightness_bar.xml b/packages/SystemUI/res/drawable/brightness_bar.xml
new file mode 100644
index 0000000..2afe164
--- /dev/null
+++ b/packages/SystemUI/res/drawable/brightness_bar.xml
@@ -0,0 +1,36 @@
+<!--
+  ~ 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+        android:width="200dp"
+        android:height="32dp"
+        android:viewportWidth="304"
+        android:viewportHeight="48">
+<path
+    android:pathData="M2,22L302,22A2,2 0,0 1,304 24L304,24A2,2 0,0 1,302 26L2,26A2,2 0,0 1,0 24L0,24A2,2 0,0 1,2 22z"
+    android:fillColor="@color/brightness_slider_track"/>
+<path
+    android:pathData="M24,0L205.71,0A24,24 0,0 1,229.71 24L229.71,24A24,24 0,0 1,205.71 48L24,48A24,24 0,0 1,0 24L0,24A24,24 0,0 1,24 0z"
+    android:fillColor="?attr/shadeActive"/>
+<path
+    android:pathData="M0,24C0,10.75 10.75,0 24,0H63.85V48H24C10.75,48 0,37.25 0,24Z"
+    android:fillColor="?androidprv:attr/colorAccentPrimaryVariant"/>
+<path
+    android:pathData="M208.98,21.26V17.37H205.09L202.34,14.62L199.6,17.37H195.71V21.26L192.96,24L195.71,26.75V30.63H199.6L202.34,33.38L205.09,30.63H208.98V26.75L211.72,24L208.98,21.26ZM207.32,26.06V28.98H204.4L202.34,31.03L200.29,28.98H197.37V26.06L195.31,24L197.37,21.94V19.02H200.29L202.34,16.97L204.4,19.02H207.32V21.94L209.37,24L207.32,26.06ZM206.49,24C206.49,26.29 204.63,28.15 202.34,28.15V19.85C204.63,19.85 206.49,21.71 206.49,24Z"
+    android:fillColor="?attr/onShadeActive"
+    android:fillType="evenOdd"/>
+</vector>
+
diff --git a/packages/SystemUI/res/layout/accessibility_deprecate_extra_dim_dialog.xml b/packages/SystemUI/res/layout/accessibility_deprecate_extra_dim_dialog.xml
new file mode 100644
index 0000000..e839f4c
--- /dev/null
+++ b/packages/SystemUI/res/layout/accessibility_deprecate_extra_dim_dialog.xml
@@ -0,0 +1,41 @@
+<!--
+  ~ 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/illustration_frame"
+    android:orientation="vertical"
+    android:paddingHorizontal="16dp"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center">
+
+    <ImageView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:adjustViewBounds="true"
+        android:layout_marginVertical="24dp"
+        android:scaleType="fitCenter"
+        android:importantForAccessibility="no"
+        android:theme="@style/Theme.SystemUI.QuickSettings"
+        android:src="@drawable/brightness_bar"/>
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:textSize="16sp"
+        android:text="@string/accessibility_deprecate_extra_dim_dialog_description"
+        android:textAppearance="@style/TextAppearance.Dialog.Body"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 30f23bf..c29c236 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -104,7 +104,7 @@
 
     <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
     <string name="quick_settings_tiles_stock" translatable="false">
-        internet,bt,flashlight,dnd,modes,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices
+        internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices
     </string>
 
     <!-- The tiles to display in QuickSettings -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 28138a5..e590f15 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1109,6 +1109,14 @@
     <!-- Priority modes: label for a mode that cannot be manually turned on [CHAR LIMIT=35] -->
     <string name="zen_mode_no_manual_invocation">Manage in settings</string>
 
+    <string name="zen_mode_active_modes">
+        {count, plural,
+            =0 {No active modes}
+            =1 {{mode} is active}
+            other {# modes are active}
+        }
+    </string>
+
     <!-- Zen mode: Priority only introduction message on first use -->
     <string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string>
 
@@ -1217,6 +1225,9 @@
     <!-- Label for accessibility action that shows widgets on lock screen on click. [CHAR LIMIT=NONE] -->
     <string name="accessibility_action_open_communal_hub">Widgets on lock screen</string>
 
+    <!-- Label for an accessibility announcement when a widget has been added to the lock screen. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_announcement_communal_widget_added"><xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget added to lock screen</string>
+
     <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] -->
     <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string>
 
@@ -3670,7 +3681,10 @@
     <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_back_gesture_action_title">Go back</string>
     <!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
-    <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.</string>
+    <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.\n\nYou can also use the keyboard shortcut
+Action + ESC for this.</string>
+    <!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] -->
+    <string name="touchpad_back_gesture_finished">You completed the go back gesture.</string>
     <string name="touchpad_back_gesture_animation_content_description">Touchpad showing three fingers moving right and left</string>
     <string name="touchpad_back_gesture_screen_animation_content_description">Device screen showing animation for back gesture</string>
 
@@ -3711,4 +3725,16 @@
     <string name="all_apps_edu_notification_title">Use your keyboard to view all apps</string>
     <!-- Education notification text for All Apps [CHAR_LIMIT=100] -->
     <string name="all_apps_edu_notification_content">Press the action key at any time. Tap to learn more gestures.</string>
+
+    <!-- Title for Extra Dim dialog [CHAR LIMIT=NONE] -->
+    <string name="accessibility_deprecate_extra_dim_dialog_title">Extra dim is now part of the brightness bar</string>
+    <!-- Content description for Extra Dim dialog. This helps users understand that we could make screen much dimmer by lowering the brightness through the brightness bar in a dark environment. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_deprecate_extra_dim_dialog_description">
+        You can now make the screen extra dim by lowering the brightness level even further from the top of your screen.\n\nThis works best when you\'re in a dark environment.
+    </string>
+    <!-- Label for button removing Extra Dim shortcuts [CHAR LIMIT=NONE] -->
+    <string name="accessibility_deprecate_extra_dim_dialog_button">Remove extra dim shortcut</string>
+    <!-- Toast message for notifying users to use regular brightness bar to lower the brightness. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_deprecate_extra_dim_dialog_toast">
+        Extra dim shortcut removed. To lower your brightness, use the regular brightness bar.</string>
 </resources>
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index c702927..ad09b46 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -85,16 +85,6 @@
         <item>On</item>
     </string-array>
 
-    <!-- State names for modes (Priority modes) tile: unavailable, off, on.
-         This subtitle is shown when the tile is in that particular state but does not set its own
-         subtitle, so some of these may never appear on screen. They should still be translated as
-         if they could appear. [CHAR LIMIT=32] -->
-    <string-array name="tile_states_modes">
-        <item>Unavailable</item>
-        <item>Off</item>
-        <item>On</item>
-    </string-array>
-
     <!-- State names for flashlight tile: unavailable, off, on.
          This subtitle is shown when the tile is in that particular state but does not set its own
          subtitle, so some of these may never appear on screen. They should still be translated as
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 428cd0e..93ee179 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -724,7 +724,10 @@
     @Override
     public void onResume(int reason) {
         if (DEBUG) Log.d(TAG, "screen on, instance " + Integer.toHexString(hashCode()));
+        mView.clearFocus();
+        mView.clearAccessibilityFocus();
         mView.requestFocus();
+        mView.requestAccessibilityFocus();
         if (mCurrentSecurityMode != SecurityMode.None) {
             int state = SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN;
             if (mView.isSidedSecurityMode()) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 7f5839d4..0da252d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2086,6 +2086,7 @@
 
     private void handleUserUnlocked(int userId) {
         Assert.isMainThread();
+        mLogger.logUserUnlocked(userId);
         mUserIsUnlocked.put(userId, true);
         mNeedsSlowUnlockTransition = resolveNeedsSlowUnlockTransition();
         for (int i = 0; i < mCallbacks.size(); i++) {
@@ -2098,12 +2099,15 @@
 
     private void handleUserStopped(int userId) {
         Assert.isMainThread();
-        mUserIsUnlocked.put(userId, mUserManager.isUserUnlocked(userId));
+        boolean isUnlocked = mUserManager.isUserUnlocked(userId);
+        mLogger.logUserStopped(userId, isUnlocked);
+        mUserIsUnlocked.put(userId, isUnlocked);
     }
 
     @VisibleForTesting
     void handleUserRemoved(int userId) {
         Assert.isMainThread();
+        mLogger.logUserRemoved(userId);
         mUserIsUnlocked.delete(userId);
         mUserTrustIsUsuallyManaged.delete(userId);
     }
@@ -2444,7 +2448,9 @@
 
         mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener);
         int user = mSelectedUserInteractor.getSelectedUserId(true);
-        mUserIsUnlocked.put(user, mUserManager.isUserUnlocked(user));
+        boolean isUserUnlocked = mUserManager.isUserUnlocked(user);
+        mLogger.logUserUnlockedInitialState(user, isUserUnlocked);
+        mUserIsUnlocked.put(user, isUserUnlocked);
         mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled();
         updateSecondaryLockscreenRequirement(user);
         List<UserInfo> allUsers = mUserManager.getUsers();
@@ -4059,6 +4065,9 @@
         pw.println("    strongAuthFlags=" + Integer.toHexString(strongAuthFlags));
         pw.println("ActiveUnlockRunning="
                 + mTrustManager.isActiveUnlockRunning(mSelectedUserInteractor.getSelectedUserId()));
+        pw.println("userUnlockedCache[userid=" + userId + "]=" + isUserUnlocked(userId));
+        pw.println("actualUserUnlocked[userid=" + userId + "]="
+                + mUserManager.isUserUnlocked(userId));
         new DumpsysTableLogger(
                 "KeyguardActiveUnlockTriggers",
                 KeyguardActiveUnlockModel.TABLE_HEADERS,
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 1f4e732..0b58f06 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -391,6 +391,7 @@
             { "handleTimeFormatUpdate timeFormat=$str1" }
         )
     }
+
     fun logUdfpsPointerDown(sensorId: Int) {
         logBuffer.log(TAG, DEBUG, { int1 = sensorId }, { "onUdfpsPointerDown, sensorId: $int1" })
     }
@@ -639,12 +640,45 @@
             { "fingerprint acquire message: $int1" }
         )
     }
+
     fun logForceIsDismissibleKeyguard(keepUnlocked: Boolean) {
         logBuffer.log(
-                TAG,
-                DEBUG,
-                { bool1 = keepUnlocked },
-                { "keepUnlockedOnFold changed to: $bool1" }
+            TAG,
+            DEBUG,
+            { bool1 = keepUnlocked },
+            { "keepUnlockedOnFold changed to: $bool1" }
+        )
+    }
+
+    fun logUserUnlocked(userId: Int) {
+        logBuffer.log(TAG, DEBUG, { int1 = userId }, { "userUnlocked userId: $int1" })
+    }
+
+    fun logUserStopped(userId: Int, isUnlocked: Boolean) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = userId
+                bool1 = isUnlocked
+            },
+            { "userStopped userId: $int1 isUnlocked: $bool1" }
+        )
+    }
+
+    fun logUserRemoved(userId: Int) {
+        logBuffer.log(TAG, DEBUG, { int1 = userId }, { "userRemoved userId: $int1" })
+    }
+
+    fun logUserUnlockedInitialState(userId: Int, isUnlocked: Boolean) {
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                int1 = userId
+                bool1 = isUnlocked
+            },
+            { "userUnlockedInitialState userId: $int1 isUnlocked: $bool1" }
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
index 3c0ac9a..394f8dd 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
@@ -30,6 +30,8 @@
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.graphics.drawable.GradientDrawable;
+import android.hardware.display.DisplayManager;
 import android.os.Handler;
 import android.util.Log;
 import android.view.AttachedSurfaceControl;
@@ -49,6 +51,8 @@
 import androidx.annotation.UiThread;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.systemui.Flags;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.res.R;
 import com.android.systemui.util.leak.RotationUtils;
@@ -70,6 +74,7 @@
     private SurfaceControl.Transaction mTransaction;
     private View mFullscreenBorder = null;
     private int mBorderOffset;
+    private int mBorderStoke;
     private final int mDisplayId;
     private static final Region sEmptyRegion = new Region();
     private ValueAnimator mShowHideBorderAnimator;
@@ -86,16 +91,20 @@
         }
     };
     private final long mLongAnimationTimeMs;
+    private final DisplayManager mDisplayManager;
+    private final DisplayManager.DisplayListener mDisplayListener;
+    private String mCurrentDisplayUniqueId;
 
     public FullscreenMagnificationController(
             @UiContext Context context,
             @Main Handler handler,
             @Main Executor executor,
+            DisplayManager displayManager,
             AccessibilityManager accessibilityManager,
             WindowManager windowManager,
             IWindowManager iWindowManager,
             Supplier<SurfaceControlViewHost> scvhSupplier) {
-        this(context, handler, executor, accessibilityManager,
+        this(context, handler, executor, displayManager, accessibilityManager,
                 windowManager, iWindowManager, scvhSupplier,
                 new SurfaceControl.Transaction(), null);
     }
@@ -105,6 +114,7 @@
             @UiContext Context context,
             @Main Handler handler,
             @Main Executor executor,
+            DisplayManager displayManager,
             AccessibilityManager accessibilityManager,
             WindowManager windowManager,
             IWindowManager iWindowManager,
@@ -120,10 +130,7 @@
         mWindowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
         mTransaction = transaction;
         mScvhSupplier = scvhSupplier;
-        mBorderOffset = mContext.getResources().getDimensionPixelSize(
-                R.dimen.magnifier_border_width_fullscreen_with_offset)
-                - mContext.getResources().getDimensionPixelSize(
-                R.dimen.magnifier_border_width_fullscreen);
+        updateDimensions();
         mDisplayId = mContext.getDisplayId();
         mConfiguration = new Configuration(context.getResources().getConfiguration());
         mLongAnimationTimeMs = mContext.getResources().getInteger(
@@ -140,6 +147,31 @@
                 }
             }
         });
+        mCurrentDisplayUniqueId = mContext.getDisplayNoVerify().getUniqueId();
+        mDisplayManager = displayManager;
+        mDisplayListener = new DisplayManager.DisplayListener() {
+            @Override
+            public void onDisplayAdded(int displayId) {
+                // Do nothing
+            }
+
+            @Override
+            public void onDisplayRemoved(int displayId) {
+                // Do nothing
+            }
+
+            @Override
+            public void onDisplayChanged(int displayId) {
+                final String uniqueId = mContext.getDisplayNoVerify().getUniqueId();
+                if (uniqueId.equals(mCurrentDisplayUniqueId)) {
+                    // Same unique ID means the physical display doesn't change. Early return.
+                    return;
+                }
+
+                mCurrentDisplayUniqueId = uniqueId;
+                applyCornerRadiusToBorder();
+            }
+        };
     }
 
     private ValueAnimator createNullTargetObjectAnimator() {
@@ -180,10 +212,15 @@
         }
         mContext.unregisterComponentCallbacks(this);
 
+
         mShowHideBorderAnimator.reverse();
     }
 
     private void cleanUpBorder() {
+        if (Flags.updateCornerRadiusOnDisplayChanged()) {
+            mDisplayManager.unregisterDisplayListener(mDisplayListener);
+        }
+
         if (mSurfaceControlViewHost != null) {
             mSurfaceControlViewHost.release();
             mSurfaceControlViewHost = null;
@@ -226,6 +263,9 @@
             } catch (Exception e) {
                 Log.w(TAG, "Failed to register rotation watcher", e);
             }
+            if (Flags.updateCornerRadiusOnDisplayChanged()) {
+                mHandler.post(this::applyCornerRadiusToBorder);
+            }
         }
 
         mTransaction
@@ -247,6 +287,9 @@
 
         mAccessibilityManager.attachAccessibilityOverlayToDisplay(
                 mDisplayId, mBorderSurfaceControl);
+        if (Flags.updateCornerRadiusOnDisplayChanged()) {
+            mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
+        }
 
         applyTouchableRegion();
     }
@@ -304,6 +347,11 @@
             final int newWidth = mWindowBounds.width() + 2 * mBorderOffset;
             final int newHeight = mWindowBounds.height() + 2 * mBorderOffset;
             mSurfaceControlViewHost.relayout(newWidth, newHeight);
+            if (Flags.updateCornerRadiusOnDisplayChanged()) {
+                // Recenter the border
+                mTransaction.setPosition(
+                        mBorderSurfaceControl, -mBorderOffset, -mBorderOffset).apply();
+            }
         }
 
         // Rotating from Landscape to ReverseLandscape will not trigger the config changes in
@@ -352,6 +400,22 @@
                 R.dimen.magnifier_border_width_fullscreen_with_offset)
                 - mContext.getResources().getDimensionPixelSize(
                         R.dimen.magnifier_border_width_fullscreen);
+        mBorderStoke = mContext.getResources().getDimensionPixelSize(
+                R.dimen.magnifier_border_width_fullscreen_with_offset);
+    }
+
+    private void applyCornerRadiusToBorder() {
+        if (!isActivated()) {
+            return;
+        }
+
+        float cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+        GradientDrawable backgroundDrawable = (GradientDrawable) mFullscreenBorder.getBackground();
+        backgroundDrawable.setStroke(
+                mBorderStoke,
+                mContext.getResources().getColor(
+                        R.color.magnification_border_color, mContext.getTheme()));
+        backgroundDrawable.setCornerRadius(cornerRadius);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
index e9c9bc7..93c4630 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
@@ -149,6 +149,7 @@
         private final Context mContext;
         private final Handler mHandler;
         private final Executor mExecutor;
+        private final DisplayManager mDisplayManager;
         private final IWindowManager mIWindowManager;
 
         FullscreenMagnificationControllerSupplier(Context context,
@@ -159,6 +160,7 @@
             mContext = context;
             mHandler = handler;
             mExecutor = executor;
+            mDisplayManager = displayManager;
             mIWindowManager = iWindowManager;
         }
 
@@ -173,6 +175,7 @@
                     windowContext,
                     mHandler,
                     mExecutor,
+                    mDisplayManager,
                     windowContext.getSystemService(AccessibilityManager.class),
                     windowContext.getSystemService(WindowManager.class),
                     mIWindowManager,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegate.kt
new file mode 100644
index 0000000..fcb1206
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegate.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.accessibility.extradim
+
+import android.content.Context
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.accessibility.AccessibilityManager
+import android.widget.Toast
+import com.android.internal.accessibility.AccessibilityShortcutController
+import com.android.internal.accessibility.common.ShortcutConstants
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.res.R
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/** Dialog for removing Extra Dim shortcuts. */
+class ExtraDimDialogDelegate
+@Inject
+constructor(
+    private val context: Context,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val systemUIDialogFactory: SystemUIDialog.Factory,
+    private val accessibilityManager: AccessibilityManager,
+    private val userTracker: UserTracker,
+) : SystemUIDialog.Delegate {
+
+    private val onClickListener: DialogInterface.OnClickListener =
+        DialogInterface.OnClickListener { dialog, _ ->
+            applicationScope.launch {
+                dialog.dismiss()
+                onRemoveExtraDimShortcutButtonClicked()
+                Toast.makeText(
+                        context,
+                        context.getText(R.string.accessibility_deprecate_extra_dim_dialog_toast),
+                        Toast.LENGTH_LONG
+                    )
+                    .show()
+            }
+        }
+
+    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        dialog.setTitle(R.string.accessibility_deprecate_extra_dim_dialog_title)
+        dialog.setView(
+            LayoutInflater.from(dialog.context)
+                .inflate(R.layout.accessibility_deprecate_extra_dim_dialog, null)
+        )
+        dialog.setPositiveButton(
+            R.string.accessibility_deprecate_extra_dim_dialog_button,
+            onClickListener
+        )
+    }
+
+    override fun createDialog(): SystemUIDialog {
+        val dialog = systemUIDialogFactory.create(this)
+        dialog.setCanceledOnTouchOutside(false)
+        return dialog
+    }
+
+    private suspend fun onRemoveExtraDimShortcutButtonClicked() =
+        withContext(backgroundDispatcher) {
+            accessibilityManager.enableShortcutsForTargets(
+                /* enable= */ false,
+                ShortcutConstants.UserShortcutType.ALL,
+                setOf(
+                    AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME
+                        .flattenToString()
+                ),
+                userTracker.userId
+            )
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt
new file mode 100644
index 0000000..e1297d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.accessibility.extradim
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import javax.inject.Provider
+
+/** Managing the Extra Dim Dialog behaviors. */
+@SysUISingleton
+class ExtraDimDialogManager
+@Inject
+constructor(
+    private val extraDimDialogDelegateProvider: Provider<ExtraDimDialogDelegate>,
+    private val mActivityStarter: ActivityStarter
+) {
+    private var dialog: SystemUIDialog? = null
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    fun dismissKeyguardIfNeededAndShowDialog() {
+        mActivityStarter.executeRunnableDismissingKeyguard(
+            { showRemoveExtraDimShortcutsDialog() },
+            /* cancelAction= */ null,
+            /* dismissShade= */ false,
+            /* afterKeyguardGone= */ true,
+            /* deferred= */ false
+        )
+    }
+
+    /** Show the dialog for removing all Extra Dim shortcuts. */
+    private fun showRemoveExtraDimShortcutsDialog() {
+        dialog?.dismiss()
+        dialog = extraDimDialogDelegateProvider.get().createDialog()
+        dialog!!.show()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiver.kt
new file mode 100644
index 0000000..405993a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiver.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.accessibility.extradim
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.android.server.display.feature.flags.Flags
+import javax.inject.Inject
+
+/**
+ * BroadcastReceiver for handling [ExtraDimDialogDelegate] intent.
+ *
+ * This is not exported. Need to call from framework and use SYSTEM user to send the intent.
+ */
+class ExtraDimDialogReceiver
+@Inject
+constructor(
+    private val extraDimDialogManager: ExtraDimDialogManager,
+) : BroadcastReceiver() {
+
+    override fun onReceive(context: Context, intent: Intent) {
+        if (
+            !Flags.evenDimmer() ||
+                !context
+                    .getResources()
+                    .getBoolean(com.android.internal.R.bool.config_evenDimmerEnabled)
+        ) {
+            return
+        }
+
+        if (ACTION == intent.action) {
+            extraDimDialogManager.dismissKeyguardIfNeededAndShowDialog()
+        }
+    }
+
+    companion object {
+        const val ACTION = "com.android.systemui.action.LAUNCH_REMOVE_EXTRA_DIM_DIALOG"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index f041f4d..083f1db 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -52,6 +52,7 @@
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.settingslib.bluetooth.BluetoothCallback;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.HapClientProfile;
@@ -104,6 +105,7 @@
     private final AudioManager mAudioManager;
     private final LocalBluetoothProfileManager mProfileManager;
     private final HapClientProfile mHapClientProfile;
+    private final UiEventLogger mUiEventLogger;
     private HearingDevicesListAdapter mDeviceListAdapter;
     private HearingDevicesPresetsController mPresetsController;
     private Context mApplicationContext;
@@ -163,7 +165,8 @@
             DialogTransitionAnimator dialogTransitionAnimator,
             @Nullable LocalBluetoothManager localBluetoothManager,
             @Main Handler handler,
-            AudioManager audioManager) {
+            AudioManager audioManager,
+            UiEventLogger uiEventLogger) {
         mApplicationContext = applicationContext;
         mShowPairNewDevice = showPairNewDevice;
         mSystemUIDialogFactory = systemUIDialogFactory;
@@ -174,6 +177,7 @@
         mAudioManager = audioManager;
         mProfileManager = localBluetoothManager.getProfileManager();
         mHapClientProfile = mProfileManager.getHapClientProfile();
+        mUiEventLogger = uiEventLogger;
     }
 
     @Override
@@ -187,6 +191,7 @@
 
     @Override
     public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) {
+        mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK);
         dismissDialogIfExists();
         Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS);
         Bundle bundle = new Bundle();
@@ -198,13 +203,21 @@
     }
 
     @Override
-    public void onDeviceItemOnClicked(@NonNull DeviceItem deviceItem, @NonNull View view) {
+    public void onDeviceItemClicked(@NonNull DeviceItem deviceItem, @NonNull View view) {
         CachedBluetoothDevice cachedBluetoothDevice = deviceItem.getCachedBluetoothDevice();
         switch (deviceItem.getType()) {
-            case ACTIVE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE ->
-                    cachedBluetoothDevice.disconnect();
-            case AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> cachedBluetoothDevice.setActive();
-            case SAVED_BLUETOOTH_DEVICE -> cachedBluetoothDevice.connect();
+            case ACTIVE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE -> {
+                mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DISCONNECT);
+                cachedBluetoothDevice.disconnect();
+            }
+            case AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
+                mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_SET_ACTIVE);
+                cachedBluetoothDevice.setActive();
+            }
+            case SAVED_BLUETOOTH_DEVICE -> {
+                mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_CONNECT);
+                cachedBluetoothDevice.connect();
+            }
         }
     }
 
@@ -262,6 +275,7 @@
         if (mLocalBluetoothManager == null) {
             return;
         }
+        mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW);
         mPairButton = dialog.requireViewById(R.id.pair_new_device_button);
         mDeviceList = dialog.requireViewById(R.id.device_list);
         mPresetSpinner = dialog.requireViewById(R.id.preset_spinner);
@@ -341,12 +355,17 @@
             }
         });
 
+        // Refresh the spinner and setSelection(index, false) before setOnItemSelectedListener() to
+        // avoid extra onItemSelected() get called when first register the listener.
+        final List<BluetoothHapPresetInfo> presetInfos = mPresetsController.getAllPresetInfo();
+        final int activePresetIndex = mPresetsController.getActivePresetIndex();
+        refreshPresetInfoAdapter(presetInfos, activePresetIndex);
         mPresetSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
             @Override
             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+                mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PRESET_SELECT);
                 mPresetsController.selectPreset(
                         mPresetsController.getAllPresetInfo().get(position).getIndex());
-                mPresetSpinner.setSelection(position);
             }
 
             @Override
@@ -354,9 +373,6 @@
                 // Do nothing
             }
         });
-        final List<BluetoothHapPresetInfo> presetInfos = mPresetsController.getAllPresetInfo();
-        final int activePresetIndex = mPresetsController.getActivePresetIndex();
-        refreshPresetInfoAdapter(presetInfos, activePresetIndex);
         mPresetSpinner.setVisibility(
                 (activeHearingDevice != null && activeHearingDevice.isConnectedHapClientDevice()
                         && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE);
@@ -365,6 +381,7 @@
     private void setupPairNewDeviceButton(SystemUIDialog dialog, @Visibility int visibility) {
         if (visibility == VISIBLE) {
             mPairButton.setOnClickListener(v -> {
+                mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR);
                 dismissDialogIfExists();
                 final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
@@ -413,7 +430,7 @@
             final int size = mPresetInfoAdapter.getCount();
             for (int position = 0; position < size; position++) {
                 if (presetInfos.get(position).getIndex() == activePresetIndex) {
-                    mPresetSpinner.setSelection(position);
+                    mPresetSpinner.setSelection(position, /* animate= */ false);
                 }
             }
         }
@@ -464,12 +481,15 @@
         text.setText(item.getToolName());
         Intent intent = item.getToolIntent();
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        view.setOnClickListener(
-                v -> {
-                    dismissDialogIfExists();
-                    mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
-                            mDialogTransitionAnimator.createActivityTransitionController(view));
-                });
+        view.setOnClickListener(v -> {
+            final String name = intent.getComponent() != null
+                    ? intent.getComponent().flattenToString()
+                    : intent.getPackage() + "/" + intent.getAction();
+            mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_RELATED_TOOL_CLICK, 0, name);
+            dismissDialogIfExists();
+            mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
+                    mDialogTransitionAnimator.createActivityTransitionController(view));
+        });
         return view;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
index 737805b..b46b8fe 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
@@ -96,7 +96,7 @@
          * @param deviceItem bluetooth device item
          * @param view       the view that was clicked
          */
-        void onDeviceItemOnClicked(@NonNull DeviceItem deviceItem, @NonNull View view);
+        void onDeviceItemClicked(@NonNull DeviceItem deviceItem, @NonNull View view);
     }
 
     private static class DeviceItemViewHolder extends RecyclerView.ViewHolder {
@@ -119,7 +119,7 @@
 
         public void bindView(DeviceItem item, HearingDeviceItemCallback callback) {
             mContainer.setEnabled(item.isEnabled());
-            mContainer.setOnClickListener(view -> callback.onDeviceItemOnClicked(item, view));
+            mContainer.setOnClickListener(view -> callback.onDeviceItemClicked(item, view));
             Integer backgroundResId = item.getBackground();
             if (backgroundResId != null) {
                 mContainer.setBackground(mContext.getDrawable(item.getBackground()));
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.java
new file mode 100644
index 0000000..3fbe56e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.java
@@ -0,0 +1,51 @@
+/*
+ * 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.accessibility.hearingaid;
+
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+
+public enum HearingDevicesUiEvent implements UiEventLogger.UiEventEnum {
+
+    @UiEvent(doc = "Hearing devices dialog is shown")
+    HEARING_DEVICES_DIALOG_SHOW(1848),
+    @UiEvent(doc = "Pair new device")
+    HEARING_DEVICES_PAIR(1849),
+    @UiEvent(doc = "Connect to the device")
+    HEARING_DEVICES_CONNECT(1850),
+    @UiEvent(doc = "Disconnect from the device")
+    HEARING_DEVICES_DISCONNECT(1851),
+    @UiEvent(doc = "Set the device as active device")
+    HEARING_DEVICES_SET_ACTIVE(1852),
+    @UiEvent(doc = "Click on the device gear to enter device detail page")
+    HEARING_DEVICES_GEAR_CLICK(1853),
+    @UiEvent(doc = "Select a preset from preset spinner")
+    HEARING_DEVICES_PRESET_SELECT(1854),
+    @UiEvent(doc = "Click on related tool")
+    HEARING_DEVICES_RELATED_TOOL_CLICK(1856);
+
+    private final int mId;
+
+    HearingDevicesUiEvent(int id) {
+        mId = id;
+    }
+
+    @Override
+    public int getId() {
+        return mId;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index ecfbd66..9f6d565 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -382,8 +382,12 @@
                             backgroundView.importantForAccessibility =
                                 IMPORTANT_FOR_ACCESSIBILITY_NO
 
-                            // Allow icon to be used as confirmation button with a11y enabled
-                            if (accessibilityManager.isTouchExplorationEnabled) {
+                            // Allow icon to be used as confirmation button with udfps and a11y
+                            // enabled
+                            if (
+                                accessibilityManager.isTouchExplorationEnabled &&
+                                    modalities.hasUdfps
+                            ) {
                                 iconOverlayView.setOnClickListener {
                                     viewModel.confirmAuthenticated()
                                 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
index 6c83dac..c089143 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
@@ -469,7 +469,7 @@
         if (isPendingConfirmation) {
             when (sensorType) {
                 FingerprintSensorType.POWER_BUTTON -> -1
-                else -> R.string.fingerprint_dialog_authenticated_confirmation
+                else -> R.string.biometric_dialog_confirm
             }
         } else if (isAuthenticating || isAuthenticated) {
             when (sensorType) {
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java
index 9dac9b3..0dc6fda 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java
@@ -18,10 +18,8 @@
 
 import android.annotation.MainThread;
 import android.annotation.NonNull;
-import android.app.ICompatCameraControlCallback;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewRootImpl;
 import android.view.ViewTreeObserver;
@@ -104,12 +102,6 @@
         }
     }
 
-    @Override // ViewRootImpl.ActivityConfigCallback
-    public void requestCompatCameraControl(boolean showControl, boolean transformationApplied,
-            ICompatCameraControlCallback callback) {
-        Log.w(TAG, "unexpected requestCompatCameraControl call");
-    }
-
     void remove() {
         final View decorView = peekDecorView();
         if (decorView != null && decorView.isAttachedToWindow()) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt
index e1d9bef..86241a5 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt
@@ -19,6 +19,7 @@
 import android.app.smartspace.SmartspaceTarget
 import android.os.Parcelable
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.Flags.communalTimerFlickerFix
 import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
 import com.android.systemui.communal.smartspace.CommunalSmartspaceController
 import com.android.systemui.dagger.SysUISingleton
@@ -80,7 +81,8 @@
                     // The view layer should have the instance based smartspaceTargetId instead of
                     // stable id, so that when a new instance of the timer is created, for example,
                     // when it is paused, the view should re-render its remote views.
-                    smartspaceTargetId = target.smartspaceTargetId,
+                    smartspaceTargetId =
+                        if (communalTimerFlickerFix()) stableId else target.smartspaceTargetId,
                     createdTimestampMillis = targetCreationTimes[stableId]!!,
                     remoteViews = target.remoteViews!!,
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 4be93cc..d1a5a4b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.ui.viewmodel
 
+import android.appwidget.AppWidgetProviderInfo
 import android.content.ComponentName
 import android.os.UserHandle
 import android.view.View
@@ -197,6 +198,9 @@
     /** Called as the user request to show the customize widget button. */
     open fun onLongClick() {}
 
+    /** Called as the UI determines that a new widget has been added to the grid. */
+    open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {}
+
     /** Called when the grid scroll position has been updated. */
     open fun onScrollPositionUpdated(firstVisibleItemIndex: Int, firstVisibleItemScroll: Int) {
         currentScrollIndex = firstVisibleItemIndex
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 5b825d8..1a86c71 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -19,16 +19,18 @@
 import android.appwidget.AppWidgetManager
 import android.appwidget.AppWidgetProviderInfo
 import android.content.ComponentName
+import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.content.res.Resources
 import android.os.UserHandle
 import android.util.Log
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
 import androidx.activity.result.ActivityResultLauncher
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.communal.data.model.CommunalWidgetCategories
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalPrefsInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
@@ -37,6 +39,7 @@
 import com.android.systemui.communal.shared.model.EditModeState
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -74,8 +77,10 @@
     private val uiEventLogger: UiEventLogger,
     @CommunalLog logBuffer: LogBuffer,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
-    private val communalPrefsInteractor: CommunalPrefsInteractor,
     private val metricsLogger: CommunalMetricsLogger,
+    @Application private val context: Context,
+    private val accessibilityManager: AccessibilityManager,
+    private val packageManager: PackageManager,
 ) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) {
 
     private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
@@ -156,6 +161,25 @@
         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
     }
 
+    override fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {
+        if (!accessibilityManager.isEnabled) {
+            return
+        }
+
+        // Send an accessibility announcement for the newly added widget
+        val widgetLabel = provider.loadLabel(packageManager)
+        val announcementText =
+            context.getString(
+                R.string.accessibility_announcement_communal_widget_added,
+                widgetLabel
+            )
+        accessibilityManager.sendAccessibilityEvent(
+            AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
+                contentDescription = announcementText
+            }
+        )
+    }
+
     val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal
 
     /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
index 7ced932..5a0eb72 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
@@ -19,6 +19,7 @@
 import android.content.BroadcastReceiver;
 
 import com.android.systemui.GuestResetOrExitSessionReceiver;
+import com.android.systemui.accessibility.extradim.ExtraDimDialogReceiver;
 import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogReceiver;
 import com.android.systemui.media.dialog.MediaOutputDialogReceiver;
 import com.android.systemui.people.widget.PeopleSpaceWidgetPinnedReceiver;
@@ -88,4 +89,13 @@
     @ClassKey(HearingDevicesDialogReceiver.class)
     public abstract BroadcastReceiver bindHearingDevicesDialogReceiver(
             HearingDevicesDialogReceiver broadcastReceiver);
+
+    /**
+     *
+     */
+    @Binds
+    @IntoMap
+    @ClassKey(ExtraDimDialogReceiver.class)
+    public abstract BroadcastReceiver bindExtraDimDialogReceiver(
+            ExtraDimDialogReceiver broadcastReceiver);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 88601da..4286646 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.dreams.DreamMonitor
 import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
 import com.android.systemui.globalactions.GlobalActionsComponent
+import com.android.systemui.inputdevice.oobe.KeyboardTouchpadOobeTutorialCoreStartable
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
 import com.android.systemui.keyguard.KeyguardViewConfigurator
@@ -257,6 +258,13 @@
 
     @Binds
     @IntoMap
+    @ClassKey(KeyboardTouchpadOobeTutorialCoreStartable::class)
+    abstract fun bindOobeSchedulerCoreStartable(
+        listener: KeyboardTouchpadOobeTutorialCoreStartable
+    ): CoreStartable
+
+    @Binds
+    @IntoMap
     @ClassKey(PhysicalKeyboardCoreStartable::class)
     abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 1771f4d..a448072 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -80,6 +80,7 @@
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.motiontool.MotionToolModule;
 import com.android.systemui.navigationbar.NavigationBarComponent;
+import com.android.systemui.navigationbar.gestural.dagger.GestureModule;
 import com.android.systemui.notetask.NoteTaskModule;
 import com.android.systemui.people.PeopleModule;
 import com.android.systemui.plugins.BcSmartspaceConfigPlugin;
@@ -143,6 +144,7 @@
 import com.android.systemui.statusbar.window.StatusBarWindowModule;
 import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
 import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule;
+import com.android.systemui.touchpad.TouchpadModule;
 import com.android.systemui.tuner.dagger.TunerModule;
 import com.android.systemui.user.UserModule;
 import com.android.systemui.user.domain.UserDomainLayerModule;
@@ -215,6 +217,7 @@
         FlagsModule.class,
         FlagDependenciesModule.class,
         FooterActionsModule.class,
+        GestureModule.class,
         InputMethodModule.class,
         KeyEventRepositoryModule.class,
         KeyboardModule.class,
@@ -257,6 +260,7 @@
         CommonSystemUIUnfoldModule.class,
         TelephonyRepositoryModule.class,
         TemporaryDisplayModule.class,
+        TouchpadModule.class,
         TunerModule.class,
         UserDomainLayerModule.class,
         UserModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java
index e182d0b..f80e0be 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java
@@ -57,21 +57,29 @@
 
 
     /**
-     * Integer used to dim the screen while dozing.
+     * Integer in the scale [1, 255] used to dim the screen while dozing.
      *
      * @see R.integer.config_screenBrightnessDoze
      */
     public int defaultDozeBrightness;
 
     /**
-     * Integer used to dim the screen just before the screen turns off.
+     * Integer in the scale [1, 255] used to dim the screen just before the screen turns off.
      *
      * @see R.integer.config_screenBrightnessDim
      */
     public int dimBrightness;
 
     /**
-     * Integer array to map ambient brightness type to real screen brightness.
+     * Float in the scale [0, 1] used to dim the screen just before the screen turns off.
+     *
+     * @see R.integer.config_screenBrightnessDimFloat
+     */
+    public float dimBrightnessFloat;
+
+    /**
+     * Integer array to map ambient brightness type to real screen brightness in the integer scale
+     * [1, 255].
      *
      * @see Settings.Global#ALWAYS_ON_DISPLAY_CONSTANTS
      * @see #KEY_SCREEN_BRIGHTNESS_ARRAY
@@ -189,6 +197,8 @@
                         com.android.internal.R.integer.config_screenBrightnessDoze);
                 dimBrightness = resources.getInteger(
                         com.android.internal.R.integer.config_screenBrightnessDim);
+                dimBrightnessFloat = resources.getFloat(
+                        com.android.internal.R.dimen.config_screenBrightnessDimFloat);
                 screenBrightnessArray = mParser.getIntArray(KEY_SCREEN_BRIGHTNESS_ARRAY,
                         resources.getIntArray(
                                 R.array.config_doze_brightness_sensor_to_brightness));
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeBrightnessHostForwarder.java b/packages/SystemUI/src/com/android/systemui/doze/DozeBrightnessHostForwarder.java
index cf0dcad..0b33614 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeBrightnessHostForwarder.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeBrightnessHostForwarder.java
@@ -36,4 +36,10 @@
         super.setDozeScreenBrightness(brightness);
         mHost.setDozeScreenBrightness(brightness);
     }
+
+    @Override
+    public void setDozeScreenBrightnessFloat(float brightness) {
+        super.setDozeScreenBrightnessFloat(brightness);
+        mHost.setDozeScreenBrightnessFloat(brightness);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
index 17b455d..2e7a459 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
@@ -71,11 +71,17 @@
 
     /**
      * Sets the actual display brightness.
-     * @param value from 0 to 255.
+     * @param value from 1 to 255.
      */
     void setDozeScreenBrightness(int value);
 
     /**
+     * Sets the actual display brightness.
+     * @param value from {@link PowerManager#BRIGHTNESS_MIN} to {@link PowerManager#BRIGHTNESS_MAX}.
+     */
+    void setDozeScreenBrightnessFloat(float value);
+
+    /**
      * Fade out screen before switching off the display power mode.
      * @param onDisplayOffCallback Executed when the display is black.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 9a9e698..5bfcc97 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -401,13 +401,22 @@
 
     /**
      * Appends new AOD screen brightness to logs
-     * @param brightness display brightness setting
+     * @param brightness display brightness setting between 1 and 255
      */
     public void traceDozeScreenBrightness(int brightness) {
         mLogger.logDozeScreenBrightness(brightness);
     }
 
     /**
+     * Appends new AOD screen brightness to logs
+     * @param brightness display brightness setting between {@link PowerManager#BRIGHTNESS_MIN} and
+     *                   {@link PowerManager#BRIGHTNESS_MAX}
+     */
+    public void traceDozeScreenBrightnessFloat(float brightness) {
+        mLogger.logDozeScreenBrightnessFloat(brightness);
+    }
+
+    /**
     * Appends new AOD dimming scrim opacity to logs
     * @param scrimOpacity
      */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index 9d6693e..a31dbec 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -309,7 +309,15 @@
         buffer.log(TAG, INFO, {
             int1 = brightness
         }, {
-            "Doze screen brightness set, brightness=$int1"
+            "Doze screen brightness set (int), brightness=$int1"
+        })
+    }
+
+    fun logDozeScreenBrightnessFloat(brightness: Float) {
+        buffer.log(TAG, INFO, {
+            double1 = brightness.toDouble()
+        }, {
+            "Doze screen brightness set (float), brightness=$double1"
         })
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index 7f0b16b..8198ef4 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -507,9 +507,13 @@
         /** Request waking up. */
         void requestWakeUp(@DozeLog.Reason int reason);
 
-        /** Set screen brightness */
+        /** Set screen brightness between 1 and 255 */
         void setDozeScreenBrightness(int brightness);
 
+        /** Set screen brightness between {@link PowerManager#BRIGHTNESS_MIN} and
+         * {@link PowerManager#BRIGHTNESS_MAX} */
+        void setDozeScreenBrightnessFloat(float brightness);
+
         class Delegate implements Service {
             private final Service mDelegate;
             private final Executor mBgExecutor;
@@ -540,6 +544,13 @@
                     mDelegate.setDozeScreenBrightness(brightness);
                 });
             }
+
+            @Override
+            public void setDozeScreenBrightnessFloat(float brightness) {
+                mBgExecutor.execute(() -> {
+                    mDelegate.setDozeScreenBrightnessFloat(brightness);
+                });
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
index 323ed98..6ed84e5 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
@@ -20,6 +20,8 @@
 
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
 
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -27,6 +29,7 @@
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.SystemProperties;
@@ -34,6 +37,7 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.IndentingPrintWriter;
+import android.view.Display;
 
 import com.android.internal.R;
 import com.android.systemui.doze.dagger.BrightnessSensor;
@@ -46,6 +50,7 @@
 import com.android.systemui.util.settings.SystemSettings;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.Objects;
 import java.util.Optional;
 
@@ -74,6 +79,7 @@
     private final DozeHost mDozeHost;
     private final Handler mHandler;
     private final SensorManager mSensorManager;
+    private final DisplayManager mDisplayManager;
     private final Optional<Sensor>[] mLightSensorOptional; // light sensors to use per posture
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final DozeParameters mDozeParameters;
@@ -81,13 +87,17 @@
     private final DozeLog mDozeLog;
     private final SystemSettings mSystemSettings;
     private final int[] mSensorToBrightness;
+    @Nullable
+    private final float[] mSensorToBrightnessFloat;
     private final int[] mSensorToScrimOpacity;
     private final int mScreenBrightnessDim;
+    private final float mScreenBrightnessDimFloat;
 
     @DevicePostureController.DevicePostureInt
     private int mDevicePosture;
     private boolean mRegistered;
     private int mDefaultDozeBrightness;
+    private float mDefaultDozeBrightnessFloat;
     private boolean mPaused = false;
     private boolean mScreenOff = false;
     private int mLastSensorValue = -1;
@@ -102,6 +112,7 @@
     private int mDebugBrightnessBucket = -1;
 
     @Inject
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     public DozeScreenBrightness(
             Context context,
             @WrappedService DozeMachine.Service service,
@@ -113,10 +124,12 @@
             DozeParameters dozeParameters,
             DevicePostureController devicePostureController,
             DozeLog dozeLog,
-            SystemSettings systemSettings) {
+            SystemSettings systemSettings,
+            DisplayManager displayManager) {
         mContext = context;
         mDozeService = service;
         mSensorManager = sensorManager;
+        mDisplayManager = displayManager;
         mLightSensorOptional = lightSensorOptional;
         mDevicePostureController = devicePostureController;
         mDevicePosture = mDevicePostureController.getDevicePosture();
@@ -131,8 +144,13 @@
                 R.dimen.config_screenBrightnessMinimumDimAmountFloat);
 
         mDefaultDozeBrightness = alwaysOnDisplayPolicy.defaultDozeBrightness;
+        mDefaultDozeBrightnessFloat =
+                mDisplayManager.getDefaultDozeBrightness(mContext.getDisplayId());
         mScreenBrightnessDim = alwaysOnDisplayPolicy.dimBrightness;
+        mScreenBrightnessDimFloat = alwaysOnDisplayPolicy.dimBrightnessFloat;
         mSensorToBrightness = alwaysOnDisplayPolicy.screenBrightnessArray;
+        mSensorToBrightnessFloat =
+                mDisplayManager.getDozeBrightnessSensorValueToBrightness(mContext.getDisplayId());
         mSensorToScrimOpacity = alwaysOnDisplayPolicy.dimmingScrimArray;
 
         mDevicePostureController.addCallback(mDevicePostureCallback);
@@ -193,11 +211,22 @@
         if (force || mRegistered || mDebugBrightnessBucket != -1) {
             int sensorValue = mDebugBrightnessBucket == -1
                     ? mLastSensorValue : mDebugBrightnessBucket;
-            int brightness = computeBrightness(sensorValue);
-            boolean brightnessReady = brightness > 0;
-            if (brightnessReady) {
-                mDozeService.setDozeScreenBrightness(
-                        clampToDimBrightnessForScreenOff(clampToUserSetting(brightness)));
+            boolean brightnessReady;
+            if (shouldUseFloatBrightness()) {
+                float brightness = computeBrightnessFloat(sensorValue);
+                brightnessReady = brightness >= 0;
+                if (brightnessReady) {
+                    mDozeService.setDozeScreenBrightnessFloat(
+                            clampToDimBrightnessForScreenOffFloat(
+                                    clampToUserSettingFloat(brightness)));
+                }
+            } else {
+                int brightness = computeBrightness(sensorValue);
+                brightnessReady = brightness > 0;
+                if (brightnessReady) {
+                    mDozeService.setDozeScreenBrightness(
+                            clampToDimBrightnessForScreenOff(clampToUserSetting(brightness)));
+                }
             }
 
             int scrimOpacity = -1;
@@ -249,17 +278,30 @@
         return mSensorToBrightness[sensorValue];
     }
 
+    private float computeBrightnessFloat(int sensorValue) {
+        if (sensorValue < 0 || sensorValue >= mSensorToBrightnessFloat.length) {
+            return -1;
+        }
+        return mSensorToBrightnessFloat[sensorValue];
+    }
+
     @Override
     public void onAccuracyChanged(Sensor sensor, int accuracy) {
     }
 
     private void resetBrightnessToDefault() {
-        mDozeService.setDozeScreenBrightness(
-                clampToDimBrightnessForScreenOff(
-                        clampToUserSetting(mDefaultDozeBrightness)));
+        if (shouldUseFloatBrightness()) {
+            mDozeService.setDozeScreenBrightnessFloat(
+                    clampToDimBrightnessForScreenOffFloat(
+                            clampToUserSettingFloat(mDefaultDozeBrightnessFloat)));
+        } else {
+            mDozeService.setDozeScreenBrightness(
+                    clampToDimBrightnessForScreenOff(
+                            clampToUserSetting(mDefaultDozeBrightness)));
+        }
         mDozeHost.setAodDimmingScrim(0f);
     }
-    //TODO: brightnessfloat change usages to float.
+
     private int clampToUserSetting(int brightness) {
         int screenBrightnessModeSetting = mSystemSettings.getIntForUser(
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
@@ -274,6 +316,19 @@
         return Math.min(brightness, userSetting);
     }
 
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    private float clampToUserSettingFloat(float brightness) {
+        int screenBrightnessModeSetting = mSystemSettings.getIntForUser(
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
+        if (screenBrightnessModeSetting == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) {
+            return brightness;
+        }
+
+        float userSetting = mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY);
+        return Math.min(brightness, userSetting);
+    }
+
     /**
      * Clamp the brightness to the dim brightness value used by PowerManagerService just before the
      * device times out and goes to sleep, if we are sleeping from a timeout. This ensures that we
@@ -301,6 +356,31 @@
         }
     }
 
+    /**
+     * Clamp the brightness to the dim brightness value used by PowerManagerService just before the
+     * device times out and goes to sleep, if we are sleeping from a timeout. This ensures that we
+     * don't raise the brightness back to the user setting before or during the screen off
+     * animation.
+     */
+    private float clampToDimBrightnessForScreenOffFloat(float brightness) {
+        final boolean screenTurningOff =
+                (mDozeParameters.shouldClampToDimBrightness()
+                        || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_GOING_TO_SLEEP)
+                && mState == DozeMachine.State.INITIALIZED;
+        if (screenTurningOff
+                && mWakefulnessLifecycle.getLastSleepReason() == GO_TO_SLEEP_REASON_TIMEOUT) {
+            return Math.max(
+                    PowerManager.BRIGHTNESS_MIN,
+                    // Use the lower of either the dim brightness, or the current brightness reduced
+                    // by the minimum dim amount. This is the same logic used in
+                    // DisplayPowerController#updatePowerState to apply a minimum dim amount.
+                    Math.min(brightness - mScreenBrightnessMinimumDimAmountFloat,
+                            mScreenBrightnessDimFloat));
+        } else {
+            return brightness;
+        }
+    }
+
     private void setLightSensorEnabled(boolean enabled) {
         if (enabled && !mRegistered && isLightSensorPresent()) {
             // Wait until we get an event from the sensor until indicating ready.
@@ -342,6 +422,20 @@
         idpw.increaseIndent();
         idpw.println("registered=" + mRegistered);
         idpw.println("posture=" + DevicePostureController.devicePostureToString(mDevicePosture));
+        idpw.println("sensorToBrightness=" + Arrays.toString(mSensorToBrightness));
+        idpw.println("sensorToBrightnessFloat=" + Arrays.toString(mSensorToBrightnessFloat));
+        idpw.println("sensorToScrimOpacity=" + Arrays.toString(mSensorToScrimOpacity));
+        idpw.println("screenBrightnessDim=" + mScreenBrightnessDim);
+        idpw.println("screenBrightnessDimFloat=" + mScreenBrightnessDimFloat);
+        idpw.println("mDefaultDozeBrightness=" + mDefaultDozeBrightness);
+        idpw.println("mDefaultDozeBrightnessFloat=" + mDefaultDozeBrightnessFloat);
+        idpw.println("mLastSensorValue=" + mLastSensorValue);
+        idpw.println("shouldUseFloatBrightness()=" + shouldUseFloatBrightness());
+    }
+
+    private boolean shouldUseFloatBrightness() {
+        return com.android.server.display.feature.flags.Flags.dozeBrightnessFloat()
+                && mSensorToBrightnessFloat != null;
     }
 
     private final DevicePostureController.Callback mDevicePostureCallback =
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 9823985..931066d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -27,7 +27,9 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.graphics.drawable.ColorDrawable;
+import android.service.dreams.DreamActivity;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
@@ -63,6 +65,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.touch.TouchInsetManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -135,6 +138,8 @@
 
     private final DreamOverlayComponent mDreamOverlayComponent;
 
+    private ComponentName mCurrentBlockedGestureDreamActivityComponent;
+
     /**
      * This {@link LifecycleRegistry} controls when dream overlay functionality, like touch
      * handling, should be active. It will automatically be paused when the dream overlay is hidden
@@ -222,6 +227,8 @@
 
     private final DreamOverlayStateController mStateController;
 
+    private final GestureInteractor mGestureInteractor;
+
     @VisibleForTesting
     public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum {
         @UiEvent(doc = "The dream overlay has entered start.")
@@ -265,6 +272,7 @@
             ComponentName homeControlPanelDreamComponent,
             DreamOverlayCallbackController dreamOverlayCallbackController,
             KeyguardInteractor keyguardInteractor,
+            GestureInteractor gestureInteractor,
             @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) {
         super(executor);
         mContext = context;
@@ -281,6 +289,7 @@
         mWindowTitle = windowTitle;
         mCommunalInteractor = communalInteractor;
         mSystemDialogsCloser = systemDialogsCloser;
+        mGestureInteractor = gestureInteractor;
 
         final ViewModelStore viewModelStore = new ViewModelStore();
         final Complication.Host host =
@@ -391,6 +400,7 @@
         mStarted = true;
 
         updateRedirectWakeup();
+        updateBlockedGestureDreamActivityComponent();
     }
 
     private void updateRedirectWakeup() {
@@ -401,6 +411,18 @@
         redirectWake(mCommunalAvailable && !glanceableHubAllowKeyguardWhenDreaming());
     }
 
+    private void updateBlockedGestureDreamActivityComponent() {
+        // TODO(b/343815446): We should not be crafting this ActivityInfo ourselves. It should be
+        // in a common place, Such as DreamActivity itself.
+        final ActivityInfo info = new ActivityInfo();
+        info.name = DreamActivity.class.getName();
+        info.packageName = getDreamComponent().getPackageName();
+        mCurrentBlockedGestureDreamActivityComponent = info.getComponentName();
+
+        mGestureInteractor.addGestureBlockedActivity(mCurrentBlockedGestureDreamActivityComponent,
+                GestureInteractor.Scope.Global);
+    }
+
     @Override
     public void onEndDream() {
         resetCurrentDreamOverlayLocked();
@@ -472,6 +494,7 @@
      *                     into the dream window.
      */
     private boolean addOverlayWindowLocked(WindowManager.LayoutParams layoutParams) {
+
         mWindow = new PhoneWindow(mContext);
         // Default to SystemUI name for TalkBack.
         mWindow.setTitle(mWindowTitle);
@@ -554,6 +577,14 @@
         }
 
         mWindow = null;
+
+        // Always unregister the any set DreamActivity from being blocked from gestures.
+        if (mCurrentBlockedGestureDreamActivityComponent != null) {
+            mGestureInteractor.removeGestureBlockedActivity(
+                    mCurrentBlockedGestureDreamActivityComponent, GestureInteractor.Scope.Global);
+            mCurrentBlockedGestureDreamActivityComponent = null;
+        }
+
         mStarted = false;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 0e06117..6e4038d 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -40,6 +40,7 @@
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import javax.inject.Inject
 
@@ -51,6 +52,7 @@
         // Internal notification backend dependencies
         crossAppPoliteNotifications dependsOn politeNotifications
         vibrateWhileUnlockedToken dependsOn politeNotifications
+        modesUi dependsOn modesApi
 
         // Internal notification frontend dependencies
         NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token
@@ -58,6 +60,7 @@
         NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
         PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
         NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token
+        NotificationsHeadsUpRefactor.token dependsOn NotificationThrottleHun.token
 
         // SceneContainer dependencies
         SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
@@ -85,6 +88,12 @@
     private inline val vibrateWhileUnlockedToken: FlagToken
         get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
 
+    private inline val modesUi
+        get() = FlagToken(android.app.Flags.FLAG_MODES_UI, android.app.Flags.modesUi())
+
+    private inline val modesApi
+        get() = FlagToken(android.app.Flags.FLAG_MODES_API, android.app.Flags.modesApi())
+
     private inline val communalHub
         get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
index 27c20aa..4652b2a 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -26,6 +26,9 @@
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.QSLog
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -47,6 +50,7 @@
 constructor(
     private val vibratorHelper: VibratorHelper?,
     private val keyguardStateController: KeyguardStateController,
+    @QSLog private val logBuffer: LogBuffer,
 ) {
 
     var effectDuration = 0
@@ -101,6 +105,7 @@
     }
 
     fun handleActionDown() {
+        logEvent(qsTile?.tileSpec, state, "action down received")
         when (state) {
             State.IDLE -> {
                 setState(State.TIMEOUT_WAIT)
@@ -112,6 +117,7 @@
     }
 
     fun handleActionUp() {
+        logEvent(qsTile?.tileSpec, state, "action up received")
         if (state == State.RUNNING_FORWARD) {
             setState(State.RUNNING_BACKWARDS_FROM_UP)
             callback?.onReverseAnimator()
@@ -130,6 +136,7 @@
     }
 
     fun handleAnimationStart() {
+        logEvent(qsTile?.tileSpec, state, "animation started")
         if (state == State.TIMEOUT_WAIT) {
             vibrate(longPressHint)
             setState(State.RUNNING_FORWARD)
@@ -138,6 +145,7 @@
 
     /** This function is called both when an animator completes or gets cancelled */
     fun handleAnimationComplete() {
+        logEvent(qsTile?.tileSpec, state, "animation completed")
         when (state) {
             State.RUNNING_FORWARD -> {
                 vibrate(snapEffect)
@@ -147,11 +155,13 @@
                     callback?.onResetProperties()
                     setState(State.IDLE)
                 }
+                logEvent(qsTile?.tileSpec, state, "long click action triggered")
                 qsTile?.longClick(expandable)
             }
             State.RUNNING_BACKWARDS_FROM_UP -> {
                 callback?.onEffectFinishedReversing()
                 setState(getStateForClick())
+                logEvent(qsTile?.tileSpec, state, "click action triggered")
                 qsTile?.click(expandable)
             }
             State.RUNNING_BACKWARDS_FROM_CANCEL -> {
@@ -179,6 +189,7 @@
         if (keyguardStateController.isPrimaryBouncerShowing || !isStateClickable) return false
 
         setState(getStateForClick())
+        logEvent(qsTile?.tileSpec, state, "click action triggered")
         qsTile?.click(expandable)
         return true
     }
@@ -273,6 +284,20 @@
         return delegated
     }
 
+    private fun logEvent(tileSpec: String?, state: State, event: String) {
+        if (!DEBUG) return
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = tileSpec
+                str2 = event
+                str3 = state.name
+            },
+            { "[long-press effect on $str1 tile] $str2 on state: $str3" }
+        )
+    }
+
     enum class State {
         IDLE, /* The effect is idle waiting for touch input */
         TIMEOUT_WAIT, /* The effect is waiting for a tap timeout period */
@@ -304,4 +329,9 @@
         /** Cancel the effect animator */
         fun onCancelAnimator()
     }
+
+    companion object {
+        private const val TAG = "QSLongPressEffect"
+        private const val DEBUG = true
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
index 3b161b6..5a008bd 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
@@ -45,7 +45,7 @@
 
     data class DeviceAdded(val deviceId: Int) : DeviceChange
 
-    data object DeviceRemoved : DeviceChange
+    data class DeviceRemoved(val deviceId: Int) : DeviceChange
 
     data object FreshStart : DeviceChange
 
@@ -72,7 +72,7 @@
 
                         override fun onInputDeviceRemoved(deviceId: Int) {
                             connectedDevices = connectedDevices - deviceId
-                            sendWithLogging(connectedDevices to DeviceRemoved)
+                            sendWithLogging(connectedDevices to DeviceRemoved(deviceId))
                         }
                     }
                 sendWithLogging(connectedDevices to FreshStart)
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt
new file mode 100644
index 0000000..dbfea76
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.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.inputdevice.oobe
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputdevice.oobe.domain.interactor.OobeTutorialSchedulerInteractor
+import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial
+import dagger.Lazy
+import javax.inject.Inject
+
+/** A [CoreStartable] to launch a scheduler for keyboard and touchpad OOBE education */
+@SysUISingleton
+class KeyboardTouchpadOobeTutorialCoreStartable
+@Inject
+constructor(private val oobeTutorialSchedulerInteractor: Lazy<OobeTutorialSchedulerInteractor>) :
+    CoreStartable {
+    override fun start() {
+        if (newTouchpadGesturesTutorial()) {
+            oobeTutorialSchedulerInteractor.get().start()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt
new file mode 100644
index 0000000..0d69081
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.inputdevice.oobe.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.data.repository.KeyboardRepository
+import com.android.systemui.touchpad.data.repository.TouchpadRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** When keyboards or touchpads are connected, schedule a tutorial after given time has elapsed */
+@SysUISingleton
+class OobeTutorialSchedulerInteractor
+@Inject
+constructor(
+    @Application private val context: Context,
+    @Application private val applicationScope: CoroutineScope,
+    keyboardRepository: KeyboardRepository,
+    touchpadRepository: TouchpadRepository
+) {
+    private val isAnyKeyboardConnected = keyboardRepository.isAnyKeyboardConnected
+    private val isAnyTouchpadConnected = touchpadRepository.isAnyTouchpadConnected
+
+    fun start() {
+        applicationScope.launch { isAnyKeyboardConnected.collect { startOobe() } }
+        applicationScope.launch { isAnyTouchpadConnected.collect { startOobe() } }
+    }
+
+    private fun startOobe() {
+        val intent = Intent(TUTORIAL_ACTION)
+        intent.addCategory(Intent.CATEGORY_DEFAULT)
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        context.startActivity(intent)
+    }
+
+    companion object {
+        const val TAG = "OobeSchedulerInteractor"
+        const val TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
index 817849c..b654307 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -41,6 +41,7 @@
 import kotlinx.coroutines.flow.asFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapConcat
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
@@ -78,9 +79,15 @@
 ) : KeyboardRepository {
 
     private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> =
-        inputDeviceRepository.deviceChange.map { (ids, change) ->
-            ids.filter { id -> isPhysicalFullKeyboard(id) } to change
-        }
+        inputDeviceRepository.deviceChange
+            .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change }
+            .filter { (_, change) ->
+                when (change) {
+                    FreshStart -> true
+                    is DeviceAdded -> isPhysicalFullKeyboard(change.deviceId)
+                    is DeviceRemoved -> isPhysicalFullKeyboard(change.deviceId)
+                }
+            }
 
     @FlowPreview
     override val newlyConnectedKeyboard: Flow<Keyboard> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index af755d3..58719fe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -24,6 +24,9 @@
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
@@ -77,11 +80,16 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
@@ -100,6 +108,7 @@
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
+import androidx.compose.ui.zIndex
 import com.android.compose.ui.graphics.painter.rememberDrawablePainter
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
@@ -405,7 +414,7 @@
         Row(Modifier.fillMaxWidth()) {
             StartSidePanel(
                 onSearchQueryChanged = onSearchQueryChanged,
-                modifier = Modifier.fillMaxWidth(fraction = 0.32f),
+                modifier = Modifier.width(200.dp),
                 categories = categories,
                 onKeyboardSettingsClicked = onKeyboardSettingsClicked,
                 selectedCategory = selectedCategoryType,
@@ -462,7 +471,18 @@
 
 @Composable
 private fun ShortcutView(modifier: Modifier, searchQuery: String, shortcut: Shortcut) {
-    Row(modifier) {
+    val interactionSource = remember { MutableInteractionSource() }
+    val isFocused by interactionSource.collectIsFocusedAsState()
+    Row(
+        modifier
+            .focusable(interactionSource = interactionSource)
+            .outlineFocusModifier(
+                isFocused = isFocused,
+                focusColor = MaterialTheme.colorScheme.secondary,
+                padding = 8.dp,
+                cornerRadius = 16.dp
+            )
+    ) {
         Row(
             modifier = Modifier.width(128.dp).align(Alignment.CenterVertically),
             horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -670,10 +690,23 @@
     colors: NavigationDrawerItemColors =
         NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent),
 ) {
+    val interactionSource = remember { MutableInteractionSource() }
+    val isFocused by interactionSource.collectIsFocusedAsState()
+
     Surface(
         selected = selected,
         onClick = onClick,
-        modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(),
+        modifier =
+            Modifier.semantics { role = Role.Tab }
+                .heightIn(min = 64.dp)
+                .fillMaxWidth()
+                .focusable(interactionSource = interactionSource)
+                .outlineFocusModifier(
+                    isFocused = isFocused,
+                    focusColor = MaterialTheme.colorScheme.secondary,
+                    padding = 2.dp,
+                    cornerRadius = 33.dp
+                ),
         shape = RoundedCornerShape(28.dp),
         color = colors.containerColor(selected).value,
     ) {
@@ -697,6 +730,39 @@
     }
 }
 
+private fun Modifier.outlineFocusModifier(
+    isFocused: Boolean,
+    focusColor: Color,
+    padding: Dp,
+    cornerRadius: Dp
+): Modifier {
+    if (isFocused) {
+        return this.drawWithContent {
+                val focusOutline =
+                    Rect(Offset.Zero, size).let {
+                        if (padding > 0.dp) {
+                            it.inflate(padding.toPx())
+                        } else {
+                            it.deflate(padding.unaryMinus().toPx())
+                        }
+                    }
+                drawContent()
+                drawRoundRect(
+                    color = focusColor,
+                    style = Stroke(width = 3.dp.toPx()),
+                    topLeft = focusOutline.topLeft,
+                    size = focusOutline.size,
+                    cornerRadius = CornerRadius(cornerRadius.toPx())
+                )
+            }
+            // Increasing Z-Index so focus outline is drawn on top of "selected" category
+            // background.
+            .zIndex(1f)
+    } else {
+        return this
+    }
+}
+
 @Composable
 @OptIn(ExperimentalMaterial3Api::class)
 private fun TitleBar() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/DismissCallbackRegistry.java b/packages/SystemUI/src/com/android/systemui/keyguard/DismissCallbackRegistry.java
index d1bbc33..0d01ee1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/DismissCallbackRegistry.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/DismissCallbackRegistry.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard;
 
+import android.util.Log;
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -33,6 +34,7 @@
 
     private final ArrayList<DismissCallbackWrapper> mDismissCallbacks = new ArrayList<>();
     private final Executor mUiBgExecutor;
+    private final static String TAG = "DismissCallbackRegistry";
 
     @Inject
     public DismissCallbackRegistry(@UiBackground Executor uiBgExecutor) {
@@ -40,10 +42,12 @@
     }
 
     public void addCallback(IKeyguardDismissCallback callback) {
+        Log.d(TAG, "Adding callback: " + callback);
         mDismissCallbacks.add(new DismissCallbackWrapper(callback));
     }
 
     public void notifyDismissCancelled() {
+        Log.d(TAG, "notifyDismissCancelled(" + mDismissCallbacks.size() + ")");
         for (int i = mDismissCallbacks.size() - 1; i >= 0; i--) {
             DismissCallbackWrapper callback = mDismissCallbacks.get(i);
             mUiBgExecutor.execute(callback::notifyDismissCancelled);
@@ -52,6 +56,7 @@
     }
 
     public void notifyDismissSucceeded() {
+        Log.d(TAG, "notifyDismissSucceeded(" + mDismissCallbacks.size() + ")");
         for (int i = mDismissCallbacks.size() - 1; i >= 0; i--) {
             DismissCallbackWrapper callback = mDismissCallbacks.get(i);
             mUiBgExecutor.execute(callback::notifyDismissSucceeded);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index a915241..ae830ee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -198,7 +198,12 @@
             interpolator = Interpolators.LINEAR
             duration =
                 when (toState) {
+                    KeyguardState.AOD -> TO_AOD_DURATION
+                    KeyguardState.DOZING -> TO_DOZING_DURATION
                     KeyguardState.GONE -> TO_GONE_DURATION
+                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+                    KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
+                    KeyguardState.PRIMARY_BOUNCER -> TO_PRIMARY_BOUNCER_DURATION
                     else -> TRANSITION_DURATION_MS
                 }.inWholeMilliseconds
         }
@@ -211,10 +216,11 @@
     companion object {
         const val TAG = "FromAlternateBouncerTransitionInteractor"
         val TRANSITION_DURATION_MS = 300.milliseconds
-        val TO_GONE_DURATION = 500.milliseconds
         val TO_AOD_DURATION = TRANSITION_DURATION_MS
-        val TO_PRIMARY_BOUNCER_DURATION = TRANSITION_DURATION_MS
         val TO_DOZING_DURATION = TRANSITION_DURATION_MS
+        val TO_GONE_DURATION = 500.milliseconds
+        val TO_LOCKSCREEN_DURATION = 300.milliseconds
         val TO_OCCLUDED_DURATION = TRANSITION_DURATION_MS
+        val TO_PRIMARY_BOUNCER_DURATION = TRANSITION_DURATION_MS
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index e5ccc4a..2a8bb47 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -220,7 +220,10 @@
             interpolator = Interpolators.LINEAR
             duration =
                 when (toState) {
+                    KeyguardState.GONE -> TO_GONE_DURATION
                     KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+                    KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
+                    KeyguardState.PRIMARY_BOUNCER -> TO_PRIMARY_BOUNCER_DURATION
                     else -> DEFAULT_DURATION
                 }.inWholeMilliseconds
         }
@@ -229,9 +232,9 @@
     companion object {
         private const val TAG = "FromAodTransitionInteractor"
         private val DEFAULT_DURATION = 500.milliseconds
-        val TO_LOCKSCREEN_DURATION = 500.milliseconds
         val TO_GONE_DURATION = DEFAULT_DURATION
-        val TO_OCCLUDED_DURATION = DEFAULT_DURATION
+        val TO_LOCKSCREEN_DURATION = 500.milliseconds
+        val TO_OCCLUDED_DURATION = 550.milliseconds
         val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 8ef138e..61446c1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -302,16 +302,25 @@
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
         return ValueAnimator().apply {
             interpolator = Interpolators.LINEAR
-            duration = DEFAULT_DURATION.inWholeMilliseconds
+            duration =
+                when (toState) {
+                    KeyguardState.GONE -> TO_GONE_DURATION
+                    KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION
+                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+                    KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
+                    KeyguardState.PRIMARY_BOUNCER -> TO_PRIMARY_BOUNCER_DURATION
+                    else -> DEFAULT_DURATION
+                }.inWholeMilliseconds
         }
     }
 
     companion object {
         const val TAG = "FromDozingTransitionInteractor"
         private val DEFAULT_DURATION = 500.milliseconds
-        val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
-        val TO_GONE_DURATION = DEFAULT_DURATION
-        val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
         val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
+        val TO_GONE_DURATION = DEFAULT_DURATION
+        val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
+        val TO_OCCLUDED_DURATION = 550.milliseconds
+        val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 206bbc5..51d92f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -393,7 +393,7 @@
         val TO_DOZING_DURATION = 500.milliseconds
         val TO_DREAMING_DURATION = 933.milliseconds
         val TO_DREAMING_HOSTED_DURATION = 933.milliseconds
-        val TO_OCCLUDED_DURATION = 450.milliseconds
+        val TO_OCCLUDED_DURATION = 550.milliseconds
         val TO_AOD_DURATION = 500.milliseconds
         val TO_AOD_FOLD_DURATION = 1100.milliseconds
         val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index e2d7851..2823b93 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -112,14 +112,18 @@
                         keyguardInteractor.isActiveDreamLockscreenHosted,
                         communalSceneInteractor.isIdleOnCommunal
                     )
-                    .filterRelevantKeyguardState()
-                    .collect {
-                        (isBouncerShowing, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal)
-                        ->
+                    .filterRelevantKeyguardStateAnd { (isBouncerShowing, _, _, _) ->
+                        // TODO(b/307976454) - See if we need to listen for SHOW_WHEN_LOCKED
+                        // activities showing up over the bouncer. Camera launch can't show up over
+                        // bouncer since the first power press hides bouncer. Do occluding
+                        // activities auto hide bouncer? Not sure.
+                        !isBouncerShowing
+                    }
+                    .collect { (_, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal) ->
                         if (
                             !maybeStartTransitionToOccludedOrInsecureCamera { state, reason ->
                                 startTransitionTo(state, ownerReason = reason)
-                            } && !isBouncerShowing && isAwake && !isActiveDreamLockscreenHosted
+                            } && isAwake && !isActiveDreamLockscreenHosted
                         ) {
                             val toState =
                                 if (isIdleOnCommunal) {
@@ -241,6 +245,7 @@
                     KeyguardState.GONE -> TO_GONE_DURATION
                     KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
                     KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION
+                    KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
                     else -> DEFAULT_DURATION
                 }.inWholeMilliseconds
         }
@@ -253,7 +258,8 @@
         val TO_GONE_DURATION = 500.milliseconds
         val TO_GONE_SHORT_DURATION = 200.milliseconds
         val TO_LOCKSCREEN_DURATION = 450.milliseconds
+        val TO_OCCLUDED_DURATION = 550.milliseconds
         val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
-        val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.5f
+        val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.1f
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
index 805dbb0..2ebd9e8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
@@ -30,6 +30,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
@@ -52,11 +53,12 @@
      * then we'll seed the repository with a transition from OFF -> GONE.
      */
     @OptIn(ExperimentalCoroutinesApi::class)
-    private val showLockscreenOnBoot =
+    private val showLockscreenOnBoot: Flow<Boolean> by lazy {
         deviceProvisioningInteractor.isDeviceProvisioned.map { provisioned ->
             (provisioned || deviceEntryInteractor.isAuthenticationRequired()) &&
                 deviceEntryInteractor.isLockscreenEnabled()
         }
+    }
 
     override fun start() {
         scope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index db33acb..a250b22 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
 import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
 import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
@@ -66,6 +67,7 @@
     private val alternateBouncerDependencies: Lazy<AlternateBouncerDependencies>,
     private val windowManager: Lazy<WindowManager>,
     private val layoutInflater: Lazy<LayoutInflater>,
+    private val dismissCallbackRegistry: DismissCallbackRegistry,
 ) : CoreStartable {
     private val layoutParams: WindowManager.LayoutParams
         get() =
@@ -162,6 +164,7 @@
 
             fun onBackRequested() {
                 alternateBouncerDependencies.get().viewModel.hideAlternateBouncer()
+                dismissCallbackRegistry.notifyDismissCancelled()
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 1b9788f..4d6577c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -74,15 +74,26 @@
         val bgView = view.bgView
         longPressHandlingView.listener =
             object : LongPressHandlingView.Listener {
-                override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) {
-                    if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+                override fun onLongPressDetected(
+                    view: View,
+                    x: Int,
+                    y: Int,
+                    isA11yAction: Boolean
+                ) {
+                    if (
+                        !isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
+                    ) {
                         return
                     }
                     vibratorHelper.performHapticFeedback(
                         view,
                         HapticFeedbackConstants.CONFIRM,
                     )
-                    applicationScope.launch { viewModel.onUserInteraction() }
+                    applicationScope.launch {
+                        view.clearFocus()
+                        view.clearAccessibilityFocus()
+                        viewModel.onUserInteraction()
+                    }
                 }
             }
 
@@ -95,6 +106,7 @@
                     launch("$TAG#viewModel.isVisible") {
                         viewModel.isVisible.collect { isVisible ->
                             longPressHandlingView.isInvisible = !isVisible
+                            view.isClickable = isVisible
                         }
                     }
                     launch("$TAG#viewModel.isLongPressEnabled") {
@@ -131,7 +143,11 @@
                                         view,
                                         HapticFeedbackConstants.CONFIRM,
                                     )
-                                    applicationScope.launch { viewModel.onUserInteraction() }
+                                    applicationScope.launch {
+                                        view.clearFocus()
+                                        view.clearAccessibilityFocus()
+                                        viewModel.onUserInteraction()
+                                    }
                                 }
                             } else {
                                 view.setOnClickListener(null)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
index dc7a649..0032c2f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -18,6 +18,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToDozingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
@@ -80,6 +81,12 @@
 
     @Binds
     @IntoSet
+    abstract fun alternateBouncerToLockscreen(
+        impl: AlternateBouncerToLockscreenTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
     abstract fun alternateBouncerToOccluded(
         impl: AlternateBouncerToOccludedTransitionViewModel
     ): DeviceEntryIconTransition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..b04521c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.ui.viewmodel
+
+import android.util.MathUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down ALTERNATE_BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views
+ * to consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class AlternateBouncerToLockscreenTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromAlternateBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION,
+            edge = Edge.create(from = ALTERNATE_BOUNCER, to = LOCKSCREEN),
+        )
+
+    fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+        var startAlpha = 1f
+        return transitionAnimation.sharedFlow(
+            duration = 250.milliseconds,
+            onStart = { startAlpha = viewState.alpha() },
+            onStep = { MathUtils.lerp(startAlpha, 1f, it) },
+        )
+    }
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(1f)
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(1f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 350ceb4..11889c5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -85,6 +85,8 @@
     private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
     private val alternateBouncerToGoneTransitionViewModel:
         AlternateBouncerToGoneTransitionViewModel,
+    private val alternateBouncerToLockscreenTransitionViewModel:
+        AlternateBouncerToLockscreenTransitionViewModel,
     private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
     private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
@@ -238,6 +240,7 @@
                         alphaOnShadeExpansion,
                         keyguardInteractor.dismissAlpha,
                         alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
+                        alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                         aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                         aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 4bfefda..3fffeff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.content.res.Resources
-import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.ContentKey
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.SysUISingleton
@@ -90,12 +90,12 @@
 
     /**
      * Returns a flow that indicates whether lockscreen notifications should be rendered in the
-     * given [sceneKey].
+     * given [contentKey].
      */
-    fun areNotificationsVisible(sceneKey: SceneKey): Flow<Boolean> {
+    fun areNotificationsVisible(contentKey: ContentKey): Flow<Boolean> {
         // `Scenes.NotificationsShade` renders its own separate notifications stack, so when it's
         // open we avoid rendering the lockscreen notifications stack.
-        if (sceneKey == Scenes.NotificationsShade) {
+        if (contentKey == Scenes.NotificationsShade) {
             return flowOf(false)
         }
 
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 947336d..9eca34f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -76,6 +76,7 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.NavigationEdgeBackPlugin;
 import com.android.systemui.plugins.PluginListener;
@@ -95,15 +96,14 @@
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.util.concurrency.BackPanelUiThread;
 import com.android.systemui.util.concurrency.UiThreadContext;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.pip.Pip;
 
 import java.io.PrintWriter;
 import java.util.ArrayDeque;
-import java.util.ArrayList;
 import java.util.Date;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
@@ -157,12 +157,7 @@
     private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
         @Override
         public void onTaskStackChanged() {
-            if (edgebackGestureHandlerGetRunningTasksBackground()) {
-                mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set(
-                        isGestureBlockingActivityRunning()));
-            } else {
-                mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning());
-            }
+            updateRunningActivityGesturesBlocked();
         }
         @Override
         public void onTaskCreated(int taskId, ComponentName componentName) {
@@ -209,8 +204,6 @@
     private final Optional<DesktopMode> mDesktopModeOptional;
     private final FalsingManager mFalsingManager;
     private final Configuration mLastReportedConfig = new Configuration();
-    // Activities which should not trigger Back gesture.
-    private final List<ComponentName> mGestureBlockingActivities = new ArrayList<>();
 
     private final Point mDisplaySize = new Point();
     private final int mDisplayId;
@@ -227,6 +220,10 @@
             mBackGestureTfClassifierProviderProvider;
     private final Provider<LightBarController> mLightBarControllerProvider;
 
+    private final GestureInteractor mGestureInteractor;
+
+    private final JavaAdapter mJavaAdapter;
+
     // The left side edge width where touch down is allowed
     private int mEdgeWidthLeft;
     // The right side edge width where touch down is allowed
@@ -426,7 +423,9 @@
             FalsingManager falsingManager,
             Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider,
             Provider<LightBarController> lightBarControllerProvider,
-            NotificationShadeWindowController notificationShadeWindowController) {
+            NotificationShadeWindowController notificationShadeWindowController,
+            GestureInteractor gestureInteractor,
+            JavaAdapter javaAdapter) {
         mContext = context;
         mDisplayId = context.getDisplayId();
         mUiThreadContext = uiThreadContext;
@@ -446,7 +445,13 @@
         mFalsingManager = falsingManager;
         mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
         mLightBarControllerProvider = lightBarControllerProvider;
+        mGestureInteractor = gestureInteractor;
+        mJavaAdapter = javaAdapter;
         mLastReportedConfig.setTo(mContext.getResources().getConfiguration());
+
+        mJavaAdapter.alwaysCollectFlow(mGestureInteractor.getGestureBlockedActivities(),
+                componentNames -> updateRunningActivityGesturesBlocked());
+
         ComponentName recentsComponentName = ComponentName.unflattenFromString(
                 context.getString(com.android.internal.R.string.config_recentsComponentName));
         if (recentsComponentName != null) {
@@ -466,8 +471,9 @@
                 } else {
                     String[] gestureBlockingActivities = resources.getStringArray(resId);
                     for (String gestureBlockingActivity : gestureBlockingActivities) {
-                        mGestureBlockingActivities.add(
-                                ComponentName.unflattenFromString(gestureBlockingActivity));
+                        mGestureInteractor.addGestureBlockedActivity(
+                                ComponentName.unflattenFromString(gestureBlockingActivity),
+                                GestureInteractor.Scope.Local);
                     }
                 }
             } catch (NameNotFoundException e) {
@@ -561,6 +567,15 @@
         }
     }
 
+    private void updateRunningActivityGesturesBlocked() {
+        if (edgebackGestureHandlerGetRunningTasksBackground()) {
+            mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set(
+                    isGestureBlockingActivityRunning()));
+        } else {
+            mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning());
+        }
+    }
+
     /**
      * Called when the nav/task bar is attached.
      */
@@ -1293,7 +1308,8 @@
         } else {
             mPackageName = "_UNKNOWN";
         }
-        return topActivity != null && mGestureBlockingActivities.contains(topActivity);
+
+        return topActivity != null && mGestureInteractor.areGesturesBlocked(topActivity);
     }
 
     public void setBackAnimation(BackAnimation backAnimation) {
@@ -1342,6 +1358,10 @@
         private final Provider<LightBarController> mLightBarControllerProvider;
         private final NotificationShadeWindowController mNotificationShadeWindowController;
 
+        private final GestureInteractor mGestureInteractor;
+
+        private final JavaAdapter mJavaAdapter;
+
         @Inject
         public Factory(OverviewProxyService overviewProxyService,
                         SysUiState sysUiState,
@@ -1361,8 +1381,10 @@
                         FalsingManager falsingManager,
                         Provider<BackGestureTfClassifierProvider>
                                 backGestureTfClassifierProviderProvider,
-                Provider<LightBarController> lightBarControllerProvider,
-                NotificationShadeWindowController notificationShadeWindowController) {
+                        Provider<LightBarController> lightBarControllerProvider,
+                        NotificationShadeWindowController notificationShadeWindowController,
+                        GestureInteractor gestureInteractor,
+                        JavaAdapter javaAdapter) {
             mOverviewProxyService = overviewProxyService;
             mSysUiState = sysUiState;
             mPluginManager = pluginManager;
@@ -1382,6 +1404,8 @@
             mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
             mLightBarControllerProvider = lightBarControllerProvider;
             mNotificationShadeWindowController = notificationShadeWindowController;
+            mGestureInteractor = gestureInteractor;
+            mJavaAdapter = javaAdapter;
         }
 
         /** Construct a {@link EdgeBackGestureHandler}. */
@@ -1407,7 +1431,9 @@
                             mFalsingManager,
                             mBackGestureTfClassifierProviderProvider,
                             mLightBarControllerProvider,
-                            mNotificationShadeWindowController));
+                            mNotificationShadeWindowController,
+                            mGestureInteractor,
+                            mJavaAdapter));
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/dagger/GestureModule.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/dagger/GestureModule.kt
new file mode 100644
index 0000000..72a84f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/dagger/GestureModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.navigationbar.gestural.dagger
+
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+/** {@link Module} for gesture related dependencies */
+@Module
+interface GestureModule {
+    /**  */
+    @Binds fun gestureRespoitory(impl: GestureRepositoryImpl): GestureRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt
new file mode 100644
index 0000000..8f35343
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.navigationbar.gestural.data.respository
+
+import android.content.ComponentName
+import android.util.ArraySet
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.withContext
+
+/** A repository for storing gesture related information */
+interface GestureRepository {
+    /** A {@link StateFlow} tracking activities currently blocked from gestures. */
+    val gestureBlockedActivities: StateFlow<Set<ComponentName>>
+
+    /** Adds an activity to be blocked from gestures. */
+    suspend fun addGestureBlockedActivity(activity: ComponentName)
+
+    /** Removes an activity from being blocked from gestures. */
+    suspend fun removeGestureBlockedActivity(activity: ComponentName)
+}
+
+@SysUISingleton
+class GestureRepositoryImpl
+@Inject
+constructor(@Main private val mainDispatcher: CoroutineDispatcher) : GestureRepository {
+    private val _gestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(ArraySet())
+
+    override val gestureBlockedActivities: StateFlow<Set<ComponentName>>
+        get() = _gestureBlockedActivities
+
+    override suspend fun addGestureBlockedActivity(activity: ComponentName) =
+        withContext(mainDispatcher) {
+            _gestureBlockedActivities.emit(
+                _gestureBlockedActivities.value.toMutableSet().apply { add(activity) }
+            )
+        }
+
+    override suspend fun removeGestureBlockedActivity(activity: ComponentName) =
+        withContext(mainDispatcher) {
+            _gestureBlockedActivities.emit(
+                _gestureBlockedActivities.value.toMutableSet().apply { remove(activity) }
+            )
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
new file mode 100644
index 0000000..6dc5939
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.navigationbar.gestural.domain
+
+import android.content.ComponentName
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/**
+ * {@link GestureInteractor} helps interact with gesture-related logic, including accessing the
+ * underlying {@link GestureRepository}.
+ */
+class GestureInteractor
+@Inject
+constructor(
+    private val gestureRepository: GestureRepository,
+    @Application private val scope: CoroutineScope
+) {
+    enum class Scope {
+        Local,
+        Global
+    }
+
+    private val _localGestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(setOf())
+    /** A {@link StateFlow} for listening to changes in Activities where gestures are blocked */
+    val gestureBlockedActivities: StateFlow<Set<ComponentName>>
+        get() =
+            combine(
+                    gestureRepository.gestureBlockedActivities,
+                    _localGestureBlockedActivities.asStateFlow()
+                ) { global, local ->
+                    global + local
+                }
+                .stateIn(scope, SharingStarted.WhileSubscribed(), setOf())
+
+    /**
+     * Adds an {@link Activity} to be blocked based on component when the topmost, focused {@link
+     * Activity}.
+     */
+    fun addGestureBlockedActivity(activity: ComponentName, gestureScope: Scope) {
+        scope.launch {
+            when (gestureScope) {
+                Scope.Local -> {
+                    _localGestureBlockedActivities.emit(
+                        _localGestureBlockedActivities.value.toMutableSet().apply { add(activity) }
+                    )
+                }
+                Scope.Global -> {
+                    gestureRepository.addGestureBlockedActivity(activity)
+                }
+            }
+        }
+    }
+
+    /** Removes an {@link Activity} from being blocked from gestures. */
+    fun removeGestureBlockedActivity(activity: ComponentName, gestureScope: Scope) {
+        scope.launch {
+            when (gestureScope) {
+                Scope.Local -> {
+                    _localGestureBlockedActivities.emit(
+                        _localGestureBlockedActivities.value.toMutableSet().apply {
+                            remove(activity)
+                        }
+                    )
+                }
+                Scope.Global -> {
+                    gestureRepository.removeGestureBlockedActivity(activity)
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks whether the specified {@link Activity} {@link ComponentName} is being blocked from
+     * gestures.
+     */
+    fun areGesturesBlocked(activity: ComponentName): Boolean {
+        return gestureBlockedActivities.value.contains(activity)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 97b5e87..02379e6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -46,7 +46,7 @@
 import com.android.systemui.retail.data.repository.RetailModeRepository
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.pairwiseBy
 import dagger.Lazy
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -63,7 +63,6 @@
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -169,17 +168,19 @@
     private val userAndTiles =
         currentUser
             .flatMapLatest { userId ->
-                tileSpecRepository.tilesSpecs(userId).map { UserAndTiles(userId, it) }
+                val currentTiles = tileSpecRepository.tilesSpecs(userId)
+                val installedComponents =
+                    installedTilesComponentRepository.getInstalledTilesComponents(userId)
+                currentTiles.combine(installedComponents) { tiles, components ->
+                    UserTilesAndComponents(userId, tiles, components)
+                }
             }
             .distinctUntilChanged()
-            .pairwise(UserAndTiles(-1, emptyList()))
+            .pairwiseBy(UserTilesAndComponents(-1, emptyList(), emptySet())) { prev, new ->
+                DataWithUserChange(data = new, userChange = prev.userId != new.userId)
+            }
             .flowOn(backgroundDispatcher)
 
-    private val installedPackagesWithTiles =
-        currentUser.flatMapLatest {
-            installedTilesComponentRepository.getInstalledTilesComponents(it)
-        }
-
     private val minTiles: Int
         get() =
             if (retailModeRepository.inRetailMode) {
@@ -194,7 +195,6 @@
         }
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class)
     private fun startTileCollection() {
         scope.launch {
             launch {
@@ -205,95 +205,82 @@
             }
 
             launch(backgroundDispatcher) {
-                userAndTiles
-                    .combine(installedPackagesWithTiles) { usersAndTiles, packages ->
-                        Data(
-                            usersAndTiles.previousValue,
-                            usersAndTiles.newValue,
-                            packages,
-                        )
-                    }
-                    .collectLatest {
-                        val newTileList = it.newData.tiles
-                        val userChanged = it.oldData.userId != it.newData.userId
-                        val newUser = it.newData.userId
-                        val components = it.installedComponents
+                userAndTiles.collectLatest {
+                    val newUser = it.userId
+                    val newTileList = it.tiles
+                    val components = it.installedComponents
+                    val userChanged = it.userChange
 
-                        // Destroy all tiles that are not in the new set
-                        specsToTiles
-                            .filter {
-                                it.key !in newTileList && it.value is TileOrNotInstalled.Tile
-                            }
-                            .forEach { entry ->
-                                logger.logTileDestroyed(
-                                    entry.key,
-                                    if (userChanged) {
-                                        QSPipelineLogger.TileDestroyedReason
-                                            .TILE_NOT_PRESENT_IN_NEW_USER
-                                    } else {
-                                        QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
-                                    }
-                                )
-                                (entry.value as TileOrNotInstalled.Tile).tile.destroy()
-                            }
-                        // MutableMap will keep the insertion order
-                        val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>()
-
-                        newTileList.forEach { tileSpec ->
-                            if (tileSpec !in newTileMap) {
-                                if (
-                                    tileSpec is TileSpec.CustomTileSpec &&
-                                        tileSpec.componentName !in components
-                                ) {
-                                    newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled
+                    // Destroy all tiles that are not in the new set
+                    specsToTiles
+                        .filter { it.key !in newTileList && it.value is TileOrNotInstalled.Tile }
+                        .forEach { entry ->
+                            logger.logTileDestroyed(
+                                entry.key,
+                                if (userChanged) {
+                                    QSPipelineLogger.TileDestroyedReason
+                                        .TILE_NOT_PRESENT_IN_NEW_USER
                                 } else {
-                                    // Create tile here will never try to create a CustomTile that
-                                    // is not installed
-                                    val newTile =
-                                        if (tileSpec in specsToTiles) {
-                                            processExistingTile(
-                                                tileSpec,
-                                                specsToTiles.getValue(tileSpec),
-                                                userChanged,
-                                                newUser
-                                            )
-                                                ?: createTile(tileSpec)
-                                        } else {
-                                            createTile(tileSpec)
-                                        }
-                                    if (newTile != null) {
-                                        newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile)
+                                    QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
+                                }
+                            )
+                            (entry.value as TileOrNotInstalled.Tile).tile.destroy()
+                        }
+                    // MutableMap will keep the insertion order
+                    val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>()
+
+                    newTileList.forEach { tileSpec ->
+                        if (tileSpec !in newTileMap) {
+                            if (
+                                tileSpec is TileSpec.CustomTileSpec &&
+                                    tileSpec.componentName !in components
+                            ) {
+                                newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled
+                            } else {
+                                // Create tile here will never try to create a CustomTile that
+                                // is not installed
+                                val newTile =
+                                    if (tileSpec in specsToTiles) {
+                                        processExistingTile(
+                                            tileSpec,
+                                            specsToTiles.getValue(tileSpec),
+                                            userChanged,
+                                            newUser
+                                        ) ?: createTile(tileSpec)
+                                    } else {
+                                        createTile(tileSpec)
                                     }
+                                if (newTile != null) {
+                                    newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile)
                                 }
                             }
                         }
-
-                        val resolvedSpecs = newTileMap.keys.toList()
-                        specsToTiles.clear()
-                        specsToTiles.putAll(newTileMap)
-                        val newResolvedTiles =
-                            newTileMap
-                                .filter { it.value is TileOrNotInstalled.Tile }
-                                .map {
-                                    TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile)
-                                }
-
-                        _currentSpecsAndTiles.value = newResolvedTiles
-                        logger.logTilesNotInstalled(
-                            newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys,
-                            newUser
-                        )
-                        if (newResolvedTiles.size < minTiles) {
-                            // We ended up with not enough tiles (some may be not installed).
-                            // Prepend the default set of tiles
-                            launch { tileSpecRepository.prependDefault(currentUser.value) }
-                        } else if (resolvedSpecs != newTileList) {
-                            // There were some tiles that couldn't be created. Change the value in
-                            // the
-                            // repository
-                            launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
-                        }
                     }
+
+                    val resolvedSpecs = newTileMap.keys.toList()
+                    specsToTiles.clear()
+                    specsToTiles.putAll(newTileMap)
+                    val newResolvedTiles =
+                        newTileMap
+                            .filter { it.value is TileOrNotInstalled.Tile }
+                            .map { TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile) }
+
+                    _currentSpecsAndTiles.value = newResolvedTiles
+                    logger.logTilesNotInstalled(
+                        newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys,
+                        newUser
+                    )
+                    if (newResolvedTiles.size < minTiles) {
+                        // We ended up with not enough tiles (some may be not installed).
+                        // Prepend the default set of tiles
+                        launch { tileSpecRepository.prependDefault(currentUser.value) }
+                    } else if (resolvedSpecs != newTileList) {
+                        // There were some tiles that couldn't be created. Change the value in
+                        // the
+                        // repository
+                        launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
+                    }
+                }
             }
         }
     }
@@ -362,8 +349,7 @@
                     newQSTileFactory.get().createTile(spec.spec)
                 } else {
                     null
-                }
-                    ?: tileFactory.createTile(spec.spec)
+                } ?: tileFactory.createTile(spec.spec)
             }
         if (tile == null) {
             logger.logTileNotFoundInFactory(spec)
@@ -436,15 +422,25 @@
 
         @JvmInline value class Tile(val tile: QSTile) : TileOrNotInstalled
     }
-
-    private data class UserAndTiles(
-        val userId: Int,
-        val tiles: List<TileSpec>,
-    )
-
-    private data class Data(
-        val oldData: UserAndTiles,
-        val newData: UserAndTiles,
-        val installedComponents: Set<ComponentName>,
-    )
 }
+
+private data class UserTilesAndComponents(
+    val userId: Int,
+    val tiles: List<TileSpec>,
+    val installedComponents: Set<ComponentName>
+)
+
+private data class DataWithUserChange(
+    val userId: Int,
+    val tiles: List<TileSpec>,
+    val installedComponents: Set<ComponentName>,
+    val userChange: Boolean,
+)
+
+private fun DataWithUserChange(data: UserTilesAndComponents, userChange: Boolean) =
+    DataWithUserChange(
+        data.userId,
+        data.tiles,
+        data.installedComponents,
+        userChange,
+    )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
index 53594bb..f702da4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
@@ -27,7 +27,6 @@
         subtitleIdsMap["cell"] = R.array.tile_states_cell
         subtitleIdsMap["battery"] = R.array.tile_states_battery
         subtitleIdsMap["dnd"] = R.array.tile_states_dnd
-        subtitleIdsMap["modes"] = R.array.tile_states_modes
         subtitleIdsMap["flashlight"] = R.array.tile_states_flashlight
         subtitleIdsMap["rotation"] = R.array.tile_states_rotation
         subtitleIdsMap["bt"] = R.array.tile_states_bt
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
index bdf935e..b927134 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
@@ -49,6 +49,7 @@
 import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.RefactorFlagUtils;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QSTile.BooleanState;
@@ -105,6 +106,11 @@
     ) {
         super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
+
+        // If the flag is on, this shouldn't run at all since the modes tile replaces the DND tile.
+        RefactorFlagUtils.INSTANCE.assertInLegacyMode(android.app.Flags.modesUi(),
+                android.app.Flags.FLAG_MODES_UI);
+
         mController = zenModeController;
         mSharedPreferences = sharedPreferences;
         mController.observe(getLifecycle(), mZenCallback);
@@ -253,18 +259,20 @@
             case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
                 state.contentDescription =
                         mContext.getString(R.string.accessibility_quick_settings_dnd) + ", "
-                        + state.secondaryLabel;
+                                + state.secondaryLabel;
                 break;
             case Global.ZEN_MODE_NO_INTERRUPTIONS:
                 state.contentDescription =
                         mContext.getString(R.string.accessibility_quick_settings_dnd) + ", " +
-                        mContext.getString(R.string.accessibility_quick_settings_dnd_none_on)
+                                mContext.getString(
+                                        R.string.accessibility_quick_settings_dnd_none_on)
                                 + ", " + state.secondaryLabel;
                 break;
             case ZEN_MODE_ALARMS:
                 state.contentDescription =
                         mContext.getString(R.string.accessibility_quick_settings_dnd) + ", " +
-                        mContext.getString(R.string.accessibility_quick_settings_dnd_alarms_on)
+                                mContext.getString(
+                                        R.string.accessibility_quick_settings_dnd_alarms_on)
                                 + ", " + state.secondaryLabel;
                 break;
             default:
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
index a300031..2a33a16 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
@@ -23,13 +23,15 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.coroutineScope
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.R.attr.contentDescription
 import com.android.internal.logging.MetricsLogger
 import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.RefactorFlagUtils.isUnexpectedlyInLegacyMode
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.plugins.qs.QSTile.BooleanState
+import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.QsEventLogger
@@ -63,7 +65,7 @@
     private val tileMapper: ModesTileMapper,
     private val userActionInteractor: ModesTileUserActionInteractor,
 ) :
-    QSTileImpl<BooleanState>(
+    QSTileImpl<QSTile.State>(
         host,
         uiEventLogger,
         backgroundLooper,
@@ -79,6 +81,8 @@
     private val config = qsTileConfigProvider.getConfig(TILE_SPEC)
 
     init {
+        /* Check if */ isUnexpectedlyInLegacyMode(Flags.modesUi(), Flags.FLAG_MODES_UI)
+
         lifecycle.coroutineScope.launch {
             lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
                 dataInteractor.tileData().collect { refreshState(it) }
@@ -90,7 +94,7 @@
 
     override fun getTileLabel(): CharSequence = tileState.label
 
-    override fun newTileState() = BooleanState()
+    override fun newTileState() = QSTile.State()
 
     override fun handleClick(expandable: Expandable?) = runBlocking {
         userActionInteractor.handleClick(expandable)
@@ -98,22 +102,22 @@
 
     override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent
 
-    override fun handleUpdateState(booleanState: BooleanState?, arg: Any?) {
+    override fun handleUpdateState(state: QSTile.State?, arg: Any?) {
         if (arg is ModesTileModel) {
             tileState = tileMapper.map(config, arg)
 
-            booleanState?.apply {
-                state = tileState.activationState.legacyState
+            state?.apply {
+                this.state = tileState.activationState.legacyState
                 icon = ResourceIcon.get(tileState.iconRes ?: R.drawable.qs_dnd_icon_off)
                 label = tileLabel
                 secondaryLabel = tileState.secondaryLabel
                 contentDescription = tileState.contentDescription
-                forceExpandIcon = true
+                expandedAccessibilityClassName = tileState.expandedAccessibilityClassName
             }
         }
     }
 
     companion object {
-        const val TILE_SPEC = "modes"
+        const val TILE_SPEC = "dnd"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index d9546ec..1750347 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -106,7 +106,8 @@
     @Override
     @MainThread
     public void onManagedProfileRemoved() {
-        mHost.removeTile(getTileSpec());
+        // No OP as this may race with the user change in CurrentTilesInteractor.
+        // If the tile needs to be removed, AutoAdd (or AutoTileManager) will take care of that.
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
index 31e91aa..92efa40 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
@@ -19,20 +19,27 @@
 import android.app.Flags
 import android.os.UserHandle
 import com.android.settingslib.notification.data.repository.ZenModeRepository
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
 import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 
-class ModesTileDataInteractor @Inject constructor(val zenModeRepository: ZenModeRepository) :
-    QSTileDataInteractor<ModesTileModel> {
-    private val zenModeActive =
+class ModesTileDataInteractor
+@Inject
+constructor(
+    val zenModeRepository: ZenModeRepository,
+    @Background val bgDispatcher: CoroutineDispatcher,
+) : QSTileDataInteractor<ModesTileModel> {
+    private val activeModes =
         zenModeRepository.modes
-            .map { modes -> modes.any { mode -> mode.isActive } }
+            .map { modes -> modes.filter { mode -> mode.isActive }.map { it.name } }
             .distinctUntilChanged()
 
     override fun tileData(
@@ -45,7 +52,10 @@
      *
      * TODO(b/299909989): Remove after the transition.
      */
-    fun tileData() = zenModeActive.map { ModesTileModel(isActivated = it) }
+    fun tileData() =
+        activeModes
+            .map { ModesTileModel(isActivated = it.isNotEmpty(), activeModes = it) }
+            .flowOn(bgDispatcher)
 
     override fun availability(user: UserHandle): Flow<Boolean> = flowOf(Flags.modesUi())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
index e44413a..cc509ea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
@@ -15,4 +15,4 @@
  */
 
 package com.android.systemui.qs.tiles.impl.modes.domain.model
-data class ModesTileModel(val isActivated: Boolean)
+data class ModesTileModel(val isActivated: Boolean, val activeModes: List<String>)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
index 7048ada..7afdb75 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.qs.tiles.impl.modes.ui
 
 import android.content.res.Resources
+import android.icu.text.MessageFormat
+import android.widget.Button
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
@@ -24,6 +26,7 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.res.R
+import java.util.Locale
 import javax.inject.Inject
 
 class ModesTileMapper
@@ -46,19 +49,32 @@
                     contentDescription = null,
                 )
             this.icon = { icon }
-            if (data.isActivated) {
-                activationState = QSTileState.ActivationState.ACTIVE
-                secondaryLabel = "Some modes enabled idk" // TODO(b/346519570)
-            } else {
-                activationState = QSTileState.ActivationState.INACTIVE
-                secondaryLabel = "Off" // TODO(b/346519570)
-            }
-            contentDescription = label
+            activationState =
+                if (data.isActivated) {
+                    QSTileState.ActivationState.ACTIVE
+                } else {
+                    QSTileState.ActivationState.INACTIVE
+                }
+            secondaryLabel = getModesStatus(data, resources)
+            contentDescription = "$label. $secondaryLabel"
             supportedActions =
                 setOf(
                     QSTileState.UserAction.CLICK,
                     QSTileState.UserAction.LONG_CLICK,
                 )
             sideViewIcon = QSTileState.SideViewIcon.Chevron
+            expandedAccessibilityClass = Button::class
         }
+
+    private fun getModesStatus(data: ModesTileModel, resources: Resources): String {
+        val msgFormat =
+            MessageFormat(resources.getString(R.string.zen_mode_active_modes), Locale.getDefault())
+        val count = data.activeModes.count()
+        val args: MutableMap<String, Any> = HashMap()
+        args["count"] = count
+        if (count >= 1) {
+            args["mode"] = data.activeModes[0]
+        }
+        return msgFormat.format(args)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
index e9e9d8b..cdcefdb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
@@ -22,12 +22,15 @@
 import com.android.internal.logging.InstanceId
 import com.android.systemui.qs.pipeline.shared.TileSpec
 
-data class QSTileConfig(
+data class QSTileConfig
+@JvmOverloads
+constructor(
     val tileSpec: TileSpec,
     val uiConfig: QSTileUIConfig,
     val instanceId: InstanceId,
     val metricsSpec: String = tileSpec.spec,
     val policy: QSTilePolicy = QSTilePolicy.NoRestrictions,
+    val autoRemoveOnUnavailable: Boolean = true,
 )
 
 /**
@@ -38,6 +41,7 @@
 
     val iconRes: Int
         @DrawableRes get
+
     val labelRes: Int
         @StringRes get
 
@@ -48,6 +52,7 @@
     data object Empty : QSTileUIConfig {
         override val iconRes: Int
             get() = Resources.ID_NULL
+
         override val labelRes: Int
             get() = Resources.ID_NULL
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 2cdcc24..c6f9ae8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -70,7 +70,7 @@
             applicationScope.launch {
                 launch {
                     qsTileViewModel.isAvailable.collectIndexed { index, isAvailable ->
-                        if (!isAvailable) {
+                        if (!isAvailable && qsTileViewModel.config.autoRemoveOnUnavailable) {
                             qsHost.removeTile(tileSpec)
                         }
                         // qsTileViewModel.isAvailable flow often starts with isAvailable == true.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index bb36fd5..ae2f32a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -24,6 +24,7 @@
 import androidx.asynclayoutinflater.view.AsyncLayoutInflater
 import com.android.settingslib.applications.InterestingConfigChanges
 import com.android.systemui.Dumpable
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -196,6 +197,7 @@
     private val qsSceneComponentFactory: QSSceneComponent.Factory,
     private val qsImplProvider: Provider<QSImpl>,
     shadeInteractor: ShadeInteractor,
+    displayStateInteractor: DisplayStateInteractor,
     dumpManager: DumpManager,
     @Main private val mainDispatcher: CoroutineDispatcher,
     @Application applicationScope: CoroutineScope,
@@ -208,6 +210,7 @@
         qsSceneComponentFactory: QSSceneComponent.Factory,
         qsImplProvider: Provider<QSImpl>,
         shadeInteractor: ShadeInteractor,
+        displayStateInteractor: DisplayStateInteractor,
         dumpManager: DumpManager,
         @Main dispatcher: CoroutineDispatcher,
         @Application scope: CoroutineScope,
@@ -216,6 +219,7 @@
         qsSceneComponentFactory,
         qsImplProvider,
         shadeInteractor,
+        displayStateInteractor,
         dumpManager,
         dispatcher,
         scope,
@@ -319,6 +323,10 @@
                     qsImpl.value?.setInSplitShade(it == ShadeMode.Split)
                 }
             }
+            launch {
+                combine(displayStateInteractor.isLargeScreen, qsImpl.filterNotNull(), ::Pair)
+                    .collect { it.second.setIsNotificationPanelFullWidth(!it.first) }
+            }
         }
     }
 
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 3fca84e..5b50133 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
@@ -172,12 +172,28 @@
 
     private fun resetShadeSessions() {
         applicationScope.launch {
-            sceneBackInteractor.backStack
-                // We are in a session if either Shade or QuickSettings is on the back stack
-                .map { backStack ->
-                    backStack.asIterable().any { it == Scenes.Shade || it == Scenes.QuickSettings }
+            combine(
+                    sceneBackInteractor.backStack
+                        // We are in a session if either Shade or QuickSettings is on the back stack
+                        .map { backStack ->
+                            backStack.asIterable().any {
+                                it == Scenes.Shade || it == Scenes.QuickSettings
+                            }
+                        }
+                        .distinctUntilChanged(),
+                    sceneInteractor.transitionState
+                        .mapNotNull { state ->
+                            // We are also in a session if either Shade or QuickSettings is the
+                            // current scene
+                            when (state) {
+                                is ObservableTransitionState.Idle -> state.currentScene
+                                is ObservableTransitionState.Transition -> state.fromScene
+                            }.let { it == Scenes.Shade || it == Scenes.QuickSettings }
+                        }
+                        .distinctUntilChanged()
+                ) { inBackStack, isCurrentScene ->
+                    inBackStack || isCurrentScene
                 }
-                .distinctUntilChanged()
                 // Once a session has ended, clear the session storage.
                 .filter { inSession -> !inSession }
                 .collect { shadeSessionStorage.clear() }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
index a2583e6..bc8642c 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
@@ -35,7 +35,6 @@
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ICompatCameraControlCallback;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -492,13 +491,6 @@
                                 }
                             }
                         }
-
-                        @Override
-                        public void requestCompatCameraControl(boolean showControl,
-                                boolean transformationApplied,
-                                ICompatCameraControlCallback callback) {
-                            Log.w(TAG, "Unexpected requestCompatCameraControl callback");
-                        }
                     });
         });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 653e49f..ec529cd 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -35,7 +35,6 @@
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ICompatCameraControlCallback;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -492,13 +491,6 @@
                                 }
                             }
                         }
-
-                        @Override
-                        public void requestCompatCameraControl(boolean showControl,
-                                boolean transformationApplied,
-                                ICompatCameraControlCallback callback) {
-                            Log.w(TAG, "Unexpected requestCompatCameraControl callback");
-                        }
                     });
         });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/SystemSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/settings/SystemSettingsRepositoryModule.kt
new file mode 100644
index 0000000..02ce74a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/SystemSettingsRepositoryModule.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.settings
+
+import android.content.ContentResolver
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
+import com.android.systemui.shared.settings.data.repository.SystemSettingsRepositoryImpl
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+
+@Module
+object SystemSettingsRepositoryModule {
+    @JvmStatic
+    @Provides
+    @SysUISingleton
+    fun provideSystemSettingsRepository(
+        contentResolver: ContentResolver,
+        @Background backgroundDispatcher: CoroutineDispatcher,
+    ): SystemSettingsRepository =
+        SystemSettingsRepositoryImpl(contentResolver, backgroundDispatcher)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index b60c193..7e0454c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -47,6 +47,7 @@
 import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.Dumpable;
+import com.android.systemui.Flags;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.communal.domain.interactor.CommunalInteractor;
@@ -63,6 +64,7 @@
 import com.android.systemui.scene.ui.view.WindowRootViewComponent;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -130,6 +132,7 @@
             mCallbacks = new ArrayList<>();
 
     private final SysuiColorExtractor mColorExtractor;
+    private final NotificationShadeWindowModel mNotificationShadeWindowModel;
     /**
      * Layout params would be aggregated and dispatched all at once if this is > 0.
      *
@@ -162,6 +165,7 @@
             ShadeWindowLogger logger,
             Lazy<SelectedUserInteractor> userInteractor,
             UserTracker userTracker,
+            NotificationShadeWindowModel notificationShadeWindowModel,
             Lazy<CommunalInteractor> communalInteractor) {
         mContext = context;
         mWindowRootViewComponentFactory = windowRootViewComponentFactory;
@@ -176,6 +180,7 @@
         mKeyguardBypassController = keyguardBypassController;
         mBackgroundExecutor = backgroundExecutor;
         mColorExtractor = colorExtractor;
+        mNotificationShadeWindowModel = notificationShadeWindowModel;
         // prefix with {slow} to make sure this dumps at the END of the critical section.
         dumpManager.registerCriticalDumpable("{slow}NotificationShadeWindowControllerImpl", this);
         mAuthController = authController;
@@ -329,6 +334,14 @@
                 mCommunalInteractor.get().isCommunalVisible(),
                 this::onCommunalVisibleChanged
         );
+
+        if (!SceneContainerFlag.isEnabled() && Flags.useTransitionsForKeyguardOccluded()) {
+            collectFlow(
+                    mWindowRootView,
+                    mNotificationShadeWindowModel.isKeyguardOccluded(),
+                    this::setKeyguardOccluded
+            );
+        }
     }
 
     @Override
@@ -341,6 +354,11 @@
         mScreenBrightnessDoze = value / 255f;
     }
 
+    @Override
+    public void setDozeScreenBrightnessFloat(float value) {
+        mScreenBrightnessDoze = value;
+    }
+
     private void setKeyguardDark(boolean dark) {
         int vis = mWindowRootView.getSystemUiVisibility();
         if (dark) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
index 5eb3a1c..330f53f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import com.android.systemui.CoreStartable
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -32,6 +33,7 @@
 import com.android.systemui.shade.transition.ScrimShadeTransitionController
 import com.android.systemui.statusbar.PulseExpansionHandler
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import javax.inject.Inject
 import javax.inject.Provider
@@ -56,11 +58,14 @@
     private val panelExpansionInteractorProvider: Provider<PanelExpansionInteractor>,
     private val shadeExpansionStateManager: ShadeExpansionStateManager,
     private val pulseExpansionHandler: PulseExpansionHandler,
+    private val displayStateInteractor: DisplayStateInteractor,
     private val nsslc: NotificationStackScrollLayoutController,
+    private val scrimController: ScrimController,
 ) : CoreStartable {
 
     override fun start() {
         hydrateShadeLayoutWidth()
+        hydrateFullWidth()
         hydrateShadeExpansionStateManager()
         logTouchesTo(touchLog)
         scrimShadeTransitionController.init()
@@ -98,4 +103,16 @@
                 }
         }
     }
+
+    private fun hydrateFullWidth() {
+        if (SceneContainerFlag.isEnabled) {
+            applicationScope.launch {
+                displayStateInteractor.isLargeScreen.collect {
+                    val isFullWidth = !it
+                    nsslc.setIsFullWidth(isFullWidth)
+                    scrimController.setClipsQsScrim(isFullWidth)
+                }
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt
new file mode 100644
index 0000000..e1289af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Models UI state for the shade window. */
+@SysUISingleton
+class NotificationShadeWindowModel
+@Inject
+constructor(
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) {
+    val isKeyguardOccluded: Flow<Boolean> =
+        keyguardTransitionInteractor.transitionValue(OCCLUDED).map { it == 1f }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 707d59aa..85fad42 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -131,10 +131,20 @@
     /** Sets the state of whether the remote input is active or not. */
     default void onRemoteInputActive(boolean remoteInputActive) {}
 
-    /** Sets the screen brightness level for when the device is dozing. */
+    /**
+     * Sets the screen brightness level for when the device is dozing.
+     * @param value The brightness value between 1 and 255
+     */
     default void setDozeScreenBrightness(int value) {}
 
     /**
+     * Sets the screen brightness level for when the device is dozing.
+     * @param value The brightness value between {@link PowerManager#BRIGHTNESS_MIN} and
+     * {@link PowerManager#BRIGHTNESS_MAX}
+     */
+    default void setDozeScreenBrightnessFloat(float value) {}
+
+    /**
      * Sets whether the screen brightness is forced to the value we use for doze mode by the status
      * bar window. No-op if the device does not support dozing.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index aff57bd..e50d64b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -50,10 +50,10 @@
 import javax.inject.Inject
 
 /**
- * Coordinates heads up notification (HUN) interactions with the notification pipeline based on
- * the HUN state reported by the [HeadsUpManager]. In this class we only consider one
- * notification, in particular the [HeadsUpManager.getTopEntry], to be HeadsUpping at a
- * time even though other notifications may be queued to heads up next.
+ * Coordinates heads up notification (HUN) interactions with the notification pipeline based on the
+ * HUN state reported by the [HeadsUpManager]. In this class we only consider one notification, in
+ * particular the [HeadsUpManager.getTopEntry], to be HeadsUpping at a time even though other
+ * notifications may be queued to heads up next.
  *
  * The current HUN, but not HUNs that are queued to heads up, will be:
  * - Lifetime extended until it's no longer heads upping.
@@ -64,7 +64,9 @@
  * Note: The inflation callback in [PreparationCoordinator] handles showing HUNs.
  */
 @CoordinatorScope
-class HeadsUpCoordinator @Inject constructor(
+class HeadsUpCoordinator
+@Inject
+constructor(
     private val mLogger: HeadsUpCoordinatorLogger,
     private val mSystemClock: SystemClock,
     private val mHeadsUpManager: HeadsUpManager,
@@ -104,8 +106,8 @@
     }
 
     /**
-     * Once the pipeline starts running, we can look through posted entries and quickly process
-     * any that don't have groups, and thus will never gave a group heads up edge case.
+     * Once the pipeline starts running, we can look through posted entries and quickly process any
+     * that don't have groups, and thus will never gave a group heads up edge case.
      */
     fun onBeforeTransformGroups(list: List<ListEntry>) {
         mNow = mSystemClock.currentTimeMillis()
@@ -128,120 +130,137 @@
      * we know that stability and [NotifPromoter]s have been applied, so we can use the location of
      * notifications in this list to determine what kind of group heads up behavior should happen.
      */
-    fun onBeforeFinalizeFilter(list: List<ListEntry>) = mHeadsUpManager.modifyHuns { hunMutator ->
-        // Nothing to do if there are no other adds/updates
-        if (mPostedEntries.isEmpty()) {
-            return@modifyHuns
-        }
-        // Calculate a bunch of information about the logical group and the locations of group
-        // entries in the nearly-finalized shade list.  These may be used in the per-group loop.
-        val postedEntriesByGroup = mPostedEntries.values.groupBy { it.entry.sbn.groupKey }
-        val logicalMembersByGroup = mNotifPipeline.allNotifs.asSequence()
-            .filter { postedEntriesByGroup.contains(it.sbn.groupKey) }
-            .groupBy { it.sbn.groupKey }
-        val groupLocationsByKey: Map<String, GroupLocation> by lazy { getGroupLocationsByKey(list) }
-        mLogger.logEvaluatingGroups(postedEntriesByGroup.size)
-        // For each group, determine which notification(s) for a group should heads up.
-        postedEntriesByGroup.forEach { (groupKey, postedEntries) ->
-            // get and classify the logical members
-            val logicalMembers = logicalMembersByGroup[groupKey] ?: emptyList()
-            val logicalSummary = logicalMembers.find { it.sbn.notification.isGroupSummary }
+    fun onBeforeFinalizeFilter(list: List<ListEntry>) =
+        mHeadsUpManager.modifyHuns { hunMutator ->
+            // Nothing to do if there are no other adds/updates
+            if (mPostedEntries.isEmpty()) {
+                return@modifyHuns
+            }
+            // Calculate a bunch of information about the logical group and the locations of group
+            // entries in the nearly-finalized shade list.  These may be used in the per-group loop.
+            val postedEntriesByGroup = mPostedEntries.values.groupBy { it.entry.sbn.groupKey }
+            val logicalMembersByGroup =
+                mNotifPipeline.allNotifs
+                    .asSequence()
+                    .filter { postedEntriesByGroup.contains(it.sbn.groupKey) }
+                    .groupBy { it.sbn.groupKey }
+            val groupLocationsByKey: Map<String, GroupLocation> by lazy {
+                getGroupLocationsByKey(list)
+            }
+            mLogger.logEvaluatingGroups(postedEntriesByGroup.size)
+            // For each group, determine which notification(s) for a group should heads up.
+            postedEntriesByGroup.forEach { (groupKey, postedEntries) ->
+                // get and classify the logical members
+                val logicalMembers = logicalMembersByGroup[groupKey] ?: emptyList()
+                val logicalSummary = logicalMembers.find { it.sbn.notification.isGroupSummary }
 
-            // Report the start of this group's evaluation
-            mLogger.logEvaluatingGroup(groupKey, postedEntries.size, logicalMembers.size)
+                // Report the start of this group's evaluation
+                mLogger.logEvaluatingGroup(groupKey, postedEntries.size, logicalMembers.size)
 
-            // If there is no logical summary, then there is no heads up to transfer
-            if (logicalSummary == null) {
-                postedEntries.forEach {
-                    handlePostedEntry(it, hunMutator, scenario = "logical-summary-missing")
+                // If there is no logical summary, then there is no heads up to transfer
+                if (logicalSummary == null) {
+                    postedEntries.forEach {
+                        handlePostedEntry(it, hunMutator, scenario = "logical-summary-missing")
+                    }
+                    return@forEach
                 }
-                return@forEach
-            }
 
-            // If summary isn't wanted to be heads up, then there is no heads up to transfer
-            if (!isGoingToShowHunStrict(logicalSummary)) {
-                postedEntries.forEach {
-                    handlePostedEntry(it, hunMutator, scenario = "logical-summary-not-heads-up")
+                // If summary isn't wanted to be heads up, then there is no heads up to transfer
+                if (!isGoingToShowHunStrict(logicalSummary)) {
+                    postedEntries.forEach {
+                        handlePostedEntry(it, hunMutator, scenario = "logical-summary-not-heads-up")
+                    }
+                    return@forEach
                 }
-                return@forEach
-            }
 
-            // The group is heads up! Overall goals:
-            //  - Maybe transfer its heads up to a child
-            //  - Also let any/all newly heads up children still heads up
-            var childToReceiveParentHeadsUp: NotificationEntry?
-            var targetType = "undefined"
+                // The group is heads up! Overall goals:
+                //  - Maybe transfer its heads up to a child
+                //  - Also let any/all newly heads up children still heads up
+                var childToReceiveParentHeadsUp: NotificationEntry?
+                var targetType = "undefined"
 
-            // If the parent is heads up, always look at the posted notification with the newest
-            // 'when', and if it is isolated with GROUP_ALERT_SUMMARY, then it should receive the
-            // parent's heads up.
-            childToReceiveParentHeadsUp =
-                findHeadsUpOverride(postedEntries, groupLocationsByKey::getLocation)
-            if (childToReceiveParentHeadsUp != null) {
-                targetType = "headsUpOverride"
-            }
-
-            // If the summary is Detached and we have not picked a receiver of the heads up, then we
-            // need to look for the best child to heads up in place of the summary.
-            val isSummaryAttached = groupLocationsByKey.contains(logicalSummary.key)
-            if (!isSummaryAttached && childToReceiveParentHeadsUp == null) {
+                // If the parent is heads up, always look at the posted notification with the newest
+                // 'when', and if it is isolated with GROUP_ALERT_SUMMARY, then it should receive
+                // the
+                // parent's heads up.
                 childToReceiveParentHeadsUp =
-                    findBestTransferChild(logicalMembers, groupLocationsByKey::getLocation)
+                    findHeadsUpOverride(postedEntries, groupLocationsByKey::getLocation)
                 if (childToReceiveParentHeadsUp != null) {
-                    targetType = "bestChild"
+                    targetType = "headsUpOverride"
                 }
-            }
 
-            // If there is no child to receive the parent heads up, then just handle the posted
-            // entries and return.
-            if (childToReceiveParentHeadsUp == null) {
-                postedEntries.forEach {
-                    handlePostedEntry(it, hunMutator, scenario = "no-transfer-target")
+                // If the summary is Detached and we have not picked a receiver of the heads up,
+                // then we
+                // need to look for the best child to heads up in place of the summary.
+                val isSummaryAttached = groupLocationsByKey.contains(logicalSummary.key)
+                if (!isSummaryAttached && childToReceiveParentHeadsUp == null) {
+                    childToReceiveParentHeadsUp =
+                        findBestTransferChild(logicalMembers, groupLocationsByKey::getLocation)
+                    if (childToReceiveParentHeadsUp != null) {
+                        targetType = "bestChild"
+                    }
                 }
-                return@forEach
-            }
 
-            // At this point we just need to initiate the transfer
-            val summaryUpdate = mPostedEntries[logicalSummary.key]
+                // If there is no child to receive the parent heads up, then just handle the posted
+                // entries and return.
+                if (childToReceiveParentHeadsUp == null) {
+                    postedEntries.forEach {
+                        handlePostedEntry(it, hunMutator, scenario = "no-transfer-target")
+                    }
+                    return@forEach
+                }
 
-            // Because we now know for certain that some child is going to heads up for this summary
-            // (as we have found a child to transfer the heads up to), mark the group as having
-            // interrupted. This will allow us to know in the future that the "should heads up"
-            // state of this group has already been handled, just not via the summary entry itself.
-            logicalSummary.setInterruption()
-            mLogger.logSummaryMarkedInterrupted(logicalSummary.key, childToReceiveParentHeadsUp.key)
+                // At this point we just need to initiate the transfer
+                val summaryUpdate = mPostedEntries[logicalSummary.key]
 
-            // If the summary was not attached, then remove the heads up from the detached summary.
-            // Otherwise we can simply ignore its posted update.
-            if (!isSummaryAttached) {
-                val summaryUpdateForRemoval = summaryUpdate?.also {
-                    it.shouldHeadsUpEver = false
-                } ?: PostedEntry(
-                        logicalSummary,
-                        wasAdded = false,
-                        wasUpdated = false,
-                        shouldHeadsUpEver = false,
-                        shouldHeadsUpAgain = false,
-                        isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(logicalSummary.key),
-                        isBinding = isEntryBinding(logicalSummary),
+                // Because we now know for certain that some child is going to heads up for this
+                // summary
+                // (as we have found a child to transfer the heads up to), mark the group as having
+                // interrupted. This will allow us to know in the future that the "should heads up"
+                // state of this group has already been handled, just not via the summary entry
+                // itself.
+                logicalSummary.setInterruption()
+                mLogger.logSummaryMarkedInterrupted(
+                    logicalSummary.key,
+                    childToReceiveParentHeadsUp.key
                 )
-                // If we transfer the heads up notification and the summary isn't even attached,
-                // that means we should ensure the summary is no longer a heads up notification,
-                // so we remove it here.
-                handlePostedEntry(
+
+                // If the summary was not attached, then remove the heads up from the detached
+                // summary.
+                // Otherwise we can simply ignore its posted update.
+                if (!isSummaryAttached) {
+                    val summaryUpdateForRemoval =
+                        summaryUpdate?.also { it.shouldHeadsUpEver = false }
+                            ?: PostedEntry(
+                                logicalSummary,
+                                wasAdded = false,
+                                wasUpdated = false,
+                                shouldHeadsUpEver = false,
+                                shouldHeadsUpAgain = false,
+                                isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(logicalSummary.key),
+                                isBinding = isEntryBinding(logicalSummary),
+                            )
+                    // If we transfer the heads up notification and the summary isn't even attached,
+                    // that means we should ensure the summary is no longer a heads up notification,
+                    // so we remove it here.
+                    handlePostedEntry(
                         summaryUpdateForRemoval,
                         hunMutator,
-                        scenario = "detached-summary-remove-heads-up")
-            } else if (summaryUpdate != null) {
-                mLogger.logPostedEntryWillNotEvaluate(
+                        scenario = "detached-summary-remove-heads-up"
+                    )
+                } else if (summaryUpdate != null) {
+                    mLogger.logPostedEntryWillNotEvaluate(
                         summaryUpdate,
-                        reason = "attached-summary-transferred")
-            }
+                        reason = "attached-summary-transferred"
+                    )
+                }
 
-            // Handle all posted entries -- if the child receiving the parent's heads up is in the
-            // list, then set its flags to ensure it heads up.
-            var didHeadsUpChildToReceiveParentHeadsUp = false
-            postedEntries.asSequence()
+                // Handle all posted entries -- if the child receiving the parent's heads up is in
+                // the
+                // list, then set its flags to ensure it heads up.
+                var didHeadsUpChildToReceiveParentHeadsUp = false
+                postedEntries
+                    .asSequence()
                     .filter { it.key != logicalSummary.key }
                     .forEach { postedEntry ->
                         if (childToReceiveParentHeadsUp.key == postedEntry.key) {
@@ -249,44 +268,49 @@
                             postedEntry.shouldHeadsUpEver = true
                             postedEntry.shouldHeadsUpAgain = true
                             handlePostedEntry(
-                                    postedEntry,
-                                    hunMutator,
-                                    scenario = "child-heads-up-transfer-target-$targetType")
+                                postedEntry,
+                                hunMutator,
+                                scenario = "child-heads-up-transfer-target-$targetType"
+                            )
                             didHeadsUpChildToReceiveParentHeadsUp = true
                         } else {
                             handlePostedEntry(
-                                    postedEntry,
-                                    hunMutator,
-                                    scenario = "child-heads-up-non-target")
+                                postedEntry,
+                                hunMutator,
+                                scenario = "child-heads-up-non-target"
+                            )
                         }
                     }
 
-            // If the child receiving the heads up notification was not updated on this tick
-            // (which can happen in a standard heads up transfer scenario), then construct an update
-            // so that we can apply it.
-            if (!didHeadsUpChildToReceiveParentHeadsUp) {
-                val posted = PostedEntry(
-                        childToReceiveParentHeadsUp,
-                        wasAdded = false,
-                        wasUpdated = false,
-                        shouldHeadsUpEver = true,
-                        shouldHeadsUpAgain = true,
-                        isHeadsUpEntry =
+                // If the child receiving the heads up notification was not updated on this tick
+                // (which can happen in a standard heads up transfer scenario), then construct an
+                // update
+                // so that we can apply it.
+                if (!didHeadsUpChildToReceiveParentHeadsUp) {
+                    val posted =
+                        PostedEntry(
+                            childToReceiveParentHeadsUp,
+                            wasAdded = false,
+                            wasUpdated = false,
+                            shouldHeadsUpEver = true,
+                            shouldHeadsUpAgain = true,
+                            isHeadsUpEntry =
                                 mHeadsUpManager.isHeadsUpEntry(childToReceiveParentHeadsUp.key),
-                        isBinding = isEntryBinding(childToReceiveParentHeadsUp),
-                )
-                handlePostedEntry(
+                            isBinding = isEntryBinding(childToReceiveParentHeadsUp),
+                        )
+                    handlePostedEntry(
                         posted,
                         hunMutator,
-                        scenario = "non-posted-child-heads-up-transfer-target-$targetType")
+                        scenario = "non-posted-child-heads-up-transfer-target-$targetType"
+                    )
+                }
             }
-        }
-        // After this method runs, all posted entries should have been handled (or skipped).
-        mPostedEntries.clear()
+            // After this method runs, all posted entries should have been handled (or skipped).
+            mPostedEntries.clear()
 
-        // Also take this opportunity to clean up any stale entry update times
-        cleanUpEntryTimes()
-    }
+            // Also take this opportunity to clean up any stale entry update times
+            cleanUpEntryTimes()
+        }
 
     /**
      * Find the posted child with the newest when, and return it if it is isolated and has
@@ -295,34 +319,38 @@
     private fun findHeadsUpOverride(
         postedEntries: List<PostedEntry>,
         locationLookupByKey: (String) -> GroupLocation,
-    ): NotificationEntry? = postedEntries.asSequence()
-        .filter { posted -> !posted.entry.sbn.notification.isGroupSummary }
-        .sortedBy { posted ->
-            -posted.entry.sbn.notification.getWhen()
-        }
-        .firstOrNull()
-        ?.let { posted ->
-            posted.entry.takeIf { entry ->
-                locationLookupByKey(entry.key) == GroupLocation.Isolated &&
+    ): NotificationEntry? =
+        postedEntries
+            .asSequence()
+            .filter { posted -> !posted.entry.sbn.notification.isGroupSummary }
+            .sortedBy { posted -> -posted.entry.sbn.notification.getWhen() }
+            .firstOrNull()
+            ?.let { posted ->
+                posted.entry.takeIf { entry ->
+                    locationLookupByKey(entry.key) == GroupLocation.Isolated &&
                         entry.sbn.notification.groupAlertBehavior == GROUP_ALERT_SUMMARY
+                }
             }
-        }
 
     /**
-     * Of children which are attached, look for the child to receive the notification:
-     * First prefer children which were updated, then looking for the ones with the newest 'when'
+     * Of children which are attached, look for the child to receive the notification: First prefer
+     * children which were updated, then looking for the ones with the newest 'when'
      */
     private fun findBestTransferChild(
         logicalMembers: List<NotificationEntry>,
         locationLookupByKey: (String) -> GroupLocation,
-    ): NotificationEntry? = logicalMembers.asSequence()
-        .filter { !it.sbn.notification.isGroupSummary }
-        .filter { locationLookupByKey(it.key) != GroupLocation.Detached }
-        .sortedWith(compareBy(
-            { !mPostedEntries.contains(it.key) },
-            { -it.sbn.notification.getWhen() },
-        ))
-        .firstOrNull()
+    ): NotificationEntry? =
+        logicalMembers
+            .asSequence()
+            .filter { !it.sbn.notification.isGroupSummary }
+            .filter { locationLookupByKey(it.key) != GroupLocation.Detached }
+            .sortedWith(
+                compareBy(
+                    { !mPostedEntries.contains(it.key) },
+                    { -it.sbn.notification.getWhen() },
+                )
+            )
+            .firstOrNull()
 
     private fun getGroupLocationsByKey(list: List<ListEntry>): Map<String, GroupLocation> =
         mutableMapOf<String, GroupLocation>().also { map ->
@@ -387,197 +415,217 @@
         mHeadsUpViewBinder.bindHeadsUpView(posted.entry, this::onHeadsUpViewBound)
     }
 
-    private val mNotifCollectionListener = object : NotifCollectionListener {
-        /**
-         * Notification was just added and if it should heads up, bind the view and then show it.
-         */
-        override fun onEntryAdded(entry: NotificationEntry) {
-            // First check whether this notification should launch a full screen intent, and
-            // launch it if needed.
-            val fsiDecision =
-                mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)
-            mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(fsiDecision)
-            if (fsiDecision.shouldInterrupt) {
-                mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
-            } else if (fsiDecision.wouldInterruptWithoutDnd) {
-                // If DND was the only reason this entry was suppressed, note it for potential
-                // reconsideration on later ranking updates.
-                addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
-            }
-
-            // makeAndLogHeadsUpDecision includes check for whether this notification should be
-            // filtered
-            val shouldHeadsUpEver =
-                mVisualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry).shouldInterrupt
-            mPostedEntries[entry.key] = PostedEntry(
-                entry,
-                wasAdded = true,
-                wasUpdated = false,
-                shouldHeadsUpEver = shouldHeadsUpEver,
-                shouldHeadsUpAgain = true,
-                isHeadsUpEntry = false,
-                isBinding = false,
-            )
-
-            // Record the last updated time for this key
-            setUpdateTime(entry, mSystemClock.currentTimeMillis())
-        }
-
-        /**
-         * Notification could've updated to be heads up or not heads up. Even if it did update to
-         * heads up, if the notification specified that it only wants to heads up once, don't heads
-         * up again.
-         */
-        override fun onEntryUpdated(entry: NotificationEntry) {
-            val shouldHeadsUpEver =
-                mVisualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry).shouldInterrupt
-            val shouldHeadsUpAgain = shouldHunAgain(entry)
-            val isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key)
-            val isBinding = isEntryBinding(entry)
-            val posted = mPostedEntries.compute(entry.key) { _, value ->
-                value?.also { update ->
-                    update.wasUpdated = true
-                    update.shouldHeadsUpEver = shouldHeadsUpEver
-                    update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain
-                    update.isHeadsUpEntry = isHeadsUpEntry
-                    update.isBinding = isBinding
-                } ?: PostedEntry(
-                    entry,
-                    wasAdded = false,
-                    wasUpdated = true,
-                    shouldHeadsUpEver = shouldHeadsUpEver,
-                    shouldHeadsUpAgain = shouldHeadsUpAgain,
-                    isHeadsUpEntry = isHeadsUpEntry,
-                    isBinding = isBinding,
-                )
-            }
-            // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so that
-            // work can be done before the ShadeListBuilder is run. This prevents re-entrant
-            // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
-            if (posted?.shouldHeadsUpEver == false) {
-                if (posted.isHeadsUpEntry) {
-                    // We don't want this to be interrupting anymore, let's remove it
-                    mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/)
-                } else if (posted.isBinding) {
-                    // Don't let the bind finish
-                    cancelHeadsUpBind(posted.entry)
+    private val mNotifCollectionListener =
+        object : NotifCollectionListener {
+            /**
+             * Notification was just added and if it should heads up, bind the view and then show
+             * it.
+             */
+            override fun onEntryAdded(entry: NotificationEntry) {
+                // First check whether this notification should launch a full screen intent, and
+                // launch it if needed.
+                val fsiDecision =
+                    mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)
+                mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(fsiDecision)
+                if (fsiDecision.shouldInterrupt) {
+                    mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+                } else if (fsiDecision.wouldInterruptWithoutDnd) {
+                    // If DND was the only reason this entry was suppressed, note it for potential
+                    // reconsideration on later ranking updates.
+                    addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
                 }
+
+                // makeAndLogHeadsUpDecision includes check for whether this notification should be
+                // filtered
+                val shouldHeadsUpEver =
+                    mVisualInterruptionDecisionProvider
+                        .makeAndLogHeadsUpDecision(entry)
+                        .shouldInterrupt
+                mPostedEntries[entry.key] =
+                    PostedEntry(
+                        entry,
+                        wasAdded = true,
+                        wasUpdated = false,
+                        shouldHeadsUpEver = shouldHeadsUpEver,
+                        shouldHeadsUpAgain = true,
+                        isHeadsUpEntry = false,
+                        isBinding = false,
+                    )
+
+                // Record the last updated time for this key
+                setUpdateTime(entry, mSystemClock.currentTimeMillis())
             }
 
-            // Update last updated time for this entry
-            setUpdateTime(entry, mSystemClock.currentTimeMillis())
-        }
-
-        /**
-         * Stop showing as heads up once removed from the notification collection
-         */
-        override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
-            mPostedEntries.remove(entry.key)
-            mEntriesUpdateTimes.remove(entry.key)
-            cancelHeadsUpBind(entry)
-            val entryKey = entry.key
-            if (mHeadsUpManager.isHeadsUpEntry(entryKey)) {
-                // TODO: This should probably know the RemoteInputCoordinator's conditions,
-                //  or otherwise reference that coordinator's state, rather than replicate its logic
-                val removeImmediatelyForRemoteInput = (mRemoteInputManager.isSpinning(entryKey) &&
-                        !NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY)
-                mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput)
-            }
-        }
-
-        override fun onEntryCleanUp(entry: NotificationEntry) {
-            mHeadsUpViewBinder.abortBindCallback(entry)
-        }
-
-        /**
-         * Identify notifications whose heads-up state changes when the notification rankings are
-         * updated, and have those changed notifications heads up if necessary.
-         *
-         * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
-         * handling of ranking changes needs to take into account that we may have just made a
-         * PostedEntry for some of these notifications.
-         */
-        override fun onRankingApplied() {
-            // Because a ranking update may cause some notifications that are no longer (or were
-            // never) in mPostedEntries to need to heads up, we need to check every notification
-            // known to the pipeline.
-            for (entry in mNotifPipeline.allNotifs) {
-                // Only consider entries that are recent enough, since we want to apply a fairly
-                // strict threshold for when an entry should be updated via only ranking and not an
-                // app-provided notification update.
-                if (!isNewEnoughForRankingUpdate(entry)) continue
-
-                // The only entries we consider heads up for here are entries that have never
-                // interrupted and that now say they should heads up or FSI; if they've heads uped in
-                // the past, we don't want to incorrectly heads up a second time if there wasn't an
-                // explicit notification update.
-                if (entry.hasInterrupted()) continue
-
-                // Before potentially allowing heads-up, check for any candidates for a FSI launch.
-                // Any entry that is a candidate meets two criteria:
-                //   - was suppressed from FSI launch only by a DND suppression
-                //   - is within the recency window for reconsideration
-                // If any of these entries are no longer suppressed, launch the FSI now.
-                if (isCandidateForFSIReconsideration(entry)) {
-                    val decision =
-                        mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(
-                            entry
-                        )
-                    if (decision.shouldInterrupt) {
-                        // Log both the launch of the full screen and also that this was via a
-                        // ranking update, and finally revoke candidacy for FSI reconsideration
-                        mLogger.logEntryUpdatedToFullScreen(entry.key, decision.logReason)
-                        mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(decision)
-                        mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
-                        mFSIUpdateCandidates.remove(entry.key)
-
-                        // if we launch the FSI then this is no longer a candidate for HUN
-                        continue
-                    } else if (decision.wouldInterruptWithoutDnd) {
-                        // decision has not changed; no need to log
-                    } else {
-                        // some other condition is now blocking FSI; log that and revoke candidacy
-                        // for FSI reconsideration
-                        mLogger.logEntryDisqualifiedFromFullScreen(entry.key, decision.logReason)
-                        mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(decision)
-                        mFSIUpdateCandidates.remove(entry.key)
+            /**
+             * Notification could've updated to be heads up or not heads up. Even if it did update
+             * to heads up, if the notification specified that it only wants to heads up once, don't
+             * heads up again.
+             */
+            override fun onEntryUpdated(entry: NotificationEntry) {
+                val shouldHeadsUpEver =
+                    mVisualInterruptionDecisionProvider
+                        .makeAndLogHeadsUpDecision(entry)
+                        .shouldInterrupt
+                val shouldHeadsUpAgain = shouldHunAgain(entry)
+                val isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key)
+                val isBinding = isEntryBinding(entry)
+                val posted =
+                    mPostedEntries.compute(entry.key) { _, value ->
+                        value?.also { update ->
+                            update.wasUpdated = true
+                            update.shouldHeadsUpEver = shouldHeadsUpEver
+                            update.shouldHeadsUpAgain =
+                                update.shouldHeadsUpAgain || shouldHeadsUpAgain
+                            update.isHeadsUpEntry = isHeadsUpEntry
+                            update.isBinding = isBinding
+                        }
+                            ?: PostedEntry(
+                                entry,
+                                wasAdded = false,
+                                wasUpdated = true,
+                                shouldHeadsUpEver = shouldHeadsUpEver,
+                                shouldHeadsUpAgain = shouldHeadsUpAgain,
+                                isHeadsUpEntry = isHeadsUpEntry,
+                                isBinding = isBinding,
+                            )
+                    }
+                // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so
+                // that
+                // work can be done before the ShadeListBuilder is run. This prevents re-entrant
+                // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
+                if (posted?.shouldHeadsUpEver == false) {
+                    if (posted.isHeadsUpEntry) {
+                        // We don't want this to be interrupting anymore, let's remove it
+                        mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/)
+                    } else if (posted.isBinding) {
+                        // Don't let the bind finish
+                        cancelHeadsUpBind(posted.entry)
                     }
                 }
 
-                // The cases where we should consider this notification to be updated:
-                // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
-                //   state
-                // - if it is present in PostedEntries and the previous state of shouldHeadsUp
-                //   differs from the updated one
-                val decision =
-                    mVisualInterruptionDecisionProvider.makeUnloggedHeadsUpDecision(entry)
-                val shouldHeadsUpEver = decision.shouldInterrupt
-                val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
-                val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
+                // Update last updated time for this entry
+                setUpdateTime(entry, mSystemClock.currentTimeMillis())
+            }
 
-                if (shouldUpdateEntry) {
-                    mLogger.logEntryUpdatedByRanking(
-                        entry.key,
-                        shouldHeadsUpEver,
-                        decision.logReason
-                    )
-                    onEntryUpdated(entry)
+            /** Stop showing as heads up once removed from the notification collection */
+            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+                mPostedEntries.remove(entry.key)
+                mEntriesUpdateTimes.remove(entry.key)
+                cancelHeadsUpBind(entry)
+                val entryKey = entry.key
+                if (mHeadsUpManager.isHeadsUpEntry(entryKey)) {
+                    // TODO: This should probably know the RemoteInputCoordinator's conditions,
+                    //  or otherwise reference that coordinator's state, rather than replicate its
+                    // logic
+                    val removeImmediatelyForRemoteInput =
+                        (mRemoteInputManager.isSpinning(entryKey) &&
+                            !NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY)
+                    mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput)
+                }
+            }
+
+            override fun onEntryCleanUp(entry: NotificationEntry) {
+                mHeadsUpViewBinder.abortBindCallback(entry)
+            }
+
+            /**
+             * Identify notifications whose heads-up state changes when the notification rankings
+             * are updated, and have those changed notifications heads up if necessary.
+             *
+             * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any
+             * handling of ranking changes needs to take into account that we may have just made a
+             * PostedEntry for some of these notifications.
+             */
+            override fun onRankingApplied() {
+                // Because a ranking update may cause some notifications that are no longer (or were
+                // never) in mPostedEntries to need to heads up, we need to check every notification
+                // known to the pipeline.
+                for (entry in mNotifPipeline.allNotifs) {
+                    // Only consider entries that are recent enough, since we want to apply a fairly
+                    // strict threshold for when an entry should be updated via only ranking and not
+                    // an
+                    // app-provided notification update.
+                    if (!isNewEnoughForRankingUpdate(entry)) continue
+
+                    // The only entries we consider heads up for here are entries that have never
+                    // interrupted and that now say they should heads up or FSI; if they've heads
+                    // uped in
+                    // the past, we don't want to incorrectly heads up a second time if there wasn't
+                    // an
+                    // explicit notification update.
+                    if (entry.hasInterrupted()) continue
+
+                    // Before potentially allowing heads-up, check for any candidates for a FSI
+                    // launch.
+                    // Any entry that is a candidate meets two criteria:
+                    //   - was suppressed from FSI launch only by a DND suppression
+                    //   - is within the recency window for reconsideration
+                    // If any of these entries are no longer suppressed, launch the FSI now.
+                    if (isCandidateForFSIReconsideration(entry)) {
+                        val decision =
+                            mVisualInterruptionDecisionProvider
+                                .makeUnloggedFullScreenIntentDecision(entry)
+                        if (decision.shouldInterrupt) {
+                            // Log both the launch of the full screen and also that this was via a
+                            // ranking update, and finally revoke candidacy for FSI reconsideration
+                            mLogger.logEntryUpdatedToFullScreen(entry.key, decision.logReason)
+                            mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(
+                                decision
+                            )
+                            mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+                            mFSIUpdateCandidates.remove(entry.key)
+
+                            // if we launch the FSI then this is no longer a candidate for HUN
+                            continue
+                        } else if (decision.wouldInterruptWithoutDnd) {
+                            // decision has not changed; no need to log
+                        } else {
+                            // some other condition is now blocking FSI; log that and revoke
+                            // candidacy
+                            // for FSI reconsideration
+                            mLogger.logEntryDisqualifiedFromFullScreen(
+                                entry.key,
+                                decision.logReason
+                            )
+                            mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(
+                                decision
+                            )
+                            mFSIUpdateCandidates.remove(entry.key)
+                        }
+                    }
+
+                    // The cases where we should consider this notification to be updated:
+                    // - if this entry is not present in PostedEntries, and is now in a
+                    // shouldHeadsUp
+                    //   state
+                    // - if it is present in PostedEntries and the previous state of shouldHeadsUp
+                    //   differs from the updated one
+                    val decision =
+                        mVisualInterruptionDecisionProvider.makeUnloggedHeadsUpDecision(entry)
+                    val shouldHeadsUpEver = decision.shouldInterrupt
+                    val postedShouldHeadsUpEver =
+                        mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
+                    val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
+
+                    if (shouldUpdateEntry) {
+                        mLogger.logEntryUpdatedByRanking(
+                            entry.key,
+                            shouldHeadsUpEver,
+                            decision.logReason
+                        )
+                        onEntryUpdated(entry)
+                    }
                 }
             }
         }
-    }
 
-    /**
-     * Checks whether an update for a notification warrants an heads up for the user.
-     */
+    /** Checks whether an update for a notification warrants an heads up for the user. */
     private fun shouldHunAgain(entry: NotificationEntry): Boolean {
         return (!entry.hasInterrupted() ||
-                (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
+            (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
     }
 
-    /**
-     * Sets the updated time for the given entry to the specified time.
-     */
+    /** Sets the updated time for the given entry to the specified time. */
     @VisibleForTesting
     fun setUpdateTime(entry: NotificationEntry, time: Long) {
         mEntriesUpdateTimes[entry.key] = time
@@ -593,10 +641,10 @@
     }
 
     /**
-     * Checks whether the entry is new enough to be updated via ranking update.
-     * We want to avoid updating an entry too long after it was originally posted/updated when we're
-     * only reacting to a ranking change, as relevant ranking updates are expected to come in
-     * fairly soon after the posting of a notification.
+     * Checks whether the entry is new enough to be updated via ranking update. We want to avoid
+     * updating an entry too long after it was originally posted/updated when we're only reacting to
+     * a ranking change, as relevant ranking updates are expected to come in fairly soon after the
+     * posting of a notification.
      */
     private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean {
         // If we don't have an update time for this key, default to "too old"
@@ -648,72 +696,92 @@
      * @see HeadsUpManager.setUserActionMayIndirectlyRemove
      * @see HeadsUpManager.canRemoveImmediately
      */
-    private val mActionPressListener = Consumer<NotificationEntry> { entry ->
-        mHeadsUpManager.setUserActionMayIndirectlyRemove(entry)
-        mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) }
-    }
-
-    private val mLifetimeExtender = object : NotifLifetimeExtender {
-        override fun getName() = TAG
-
-        override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
-            mEndLifetimeExtension = callback
+    private val mActionPressListener =
+        Consumer<NotificationEntry> { entry ->
+            mHeadsUpManager.setUserActionMayIndirectlyRemove(entry)
+            mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) }
         }
 
-        override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
-            if (mHeadsUpManager.canRemoveImmediately(entry.key)) {
-                return false
+    private val mLifetimeExtender =
+        object : NotifLifetimeExtender {
+            override fun getName() = TAG
+
+            override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
+                mEndLifetimeExtension = callback
             }
-            if (isSticky(entry)) {
-                val removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key)
-                mNotifsExtendingLifetime[entry] = mExecutor.executeDelayed({
-                    mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ true)
-                }, removeAfterMillis)
-            } else {
-                mExecutor.execute {
-                    mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ false)
+
+            override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
+                if (mHeadsUpManager.canRemoveImmediately(entry.key)) {
+                    return false
                 }
-                mNotifsExtendingLifetime[entry] = null
+                if (isSticky(entry)) {
+                    val removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key)
+                    mNotifsExtendingLifetime[entry] =
+                        mExecutor.executeDelayed(
+                            {
+                                mHeadsUpManager.removeNotification(
+                                    entry.key, /* releaseImmediately */
+                                    true
+                                )
+                            },
+                            removeAfterMillis
+                        )
+                } else {
+                    mExecutor.execute {
+                        mHeadsUpManager.removeNotification(
+                            entry.key, /* releaseImmediately */
+                            false
+                        )
+                    }
+                    mNotifsExtendingLifetime[entry] = null
+                }
+                return true
             }
-            return true
-        }
 
-        override fun cancelLifetimeExtension(entry: NotificationEntry) {
-            mNotifsExtendingLifetime.remove(entry)?.run()
-        }
-    }
-
-    private val mNotifPromoter = object : NotifPromoter(TAG) {
-        override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean =
-            isGoingToShowHunNoRetract(entry)
-    }
-
-    val sectioner = object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) {
-        override fun isInSection(entry: ListEntry): Boolean =
-            // TODO: This check won't notice if a child of the group is going to HUN...
-            isGoingToShowHunNoRetract(entry)
-
-        override fun getComparator(): NotifComparator {
-            return object : NotifComparator("HeadsUp") {
-                override fun compare(o1: ListEntry, o2: ListEntry): Int =
-                    mHeadsUpManager.compare(o1.representativeEntry, o2.representativeEntry)
+            override fun cancelLifetimeExtension(entry: NotificationEntry) {
+                mNotifsExtendingLifetime.remove(entry)?.run()
             }
         }
 
-        override fun getHeaderNodeController(): NodeController? =
-            // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController
-            if (RankingCoordinator.SHOW_ALL_SECTIONS) mIncomingHeaderController else null
-    }
+    private val mNotifPromoter =
+        object : NotifPromoter(TAG) {
+            override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean =
+                isGoingToShowHunNoRetract(entry)
+        }
 
-    private val mOnHeadsUpChangedListener = object : OnHeadsUpChangedListener {
-        override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
-            if (!isHeadsUp) {
-                mNotifPromoter.invalidateList("headsUpEnded: ${entry.logKey}")
-                mHeadsUpViewBinder.unbindHeadsUpView(entry)
-                endNotifLifetimeExtensionIfExtended(entry)
+    val sectioner =
+        object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) {
+            override fun isInSection(entry: ListEntry): Boolean =
+                // TODO: This check won't notice if a child of the group is going to HUN...
+                isGoingToShowHunNoRetract(entry)
+
+            override fun getComparator(): NotifComparator {
+                return object : NotifComparator("HeadsUp") {
+                    override fun compare(o1: ListEntry, o2: ListEntry): Int =
+                        mHeadsUpManager.compare(o1.representativeEntry, o2.representativeEntry)
+                }
+            }
+
+            override fun getHeaderNodeController(): NodeController? =
+                // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and
+                // mIncomingHeaderController
+                if (RankingCoordinator.SHOW_ALL_SECTIONS) mIncomingHeaderController else null
+        }
+
+    private val mOnHeadsUpChangedListener =
+        object : OnHeadsUpChangedListener {
+            override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
+                if (!isHeadsUp) {
+                    mNotifPromoter.invalidateList("headsUpEnded: ${entry.logKey}")
+                    mHeadsUpViewBinder.unbindHeadsUpView(entry)
+                    endNotifLifetimeExtensionIfExtended(entry)
+                }
+            }
+
+            override fun onHeadsUpAnimatingAwayEnded(entry: NotificationEntry) {
+                mNotifPromoter.invalidateList("headsUpAnimatingAwayEnded: ${entry.logKey}")
             }
         }
-    }
 
     private fun isSticky(entry: NotificationEntry) = mHeadsUpManager.isSticky(entry.key)
 
@@ -726,8 +794,9 @@
      * Whether the notification is already heads up or binding so that it can imminently heads up
      */
     private fun isAttemptingToShowHun(entry: ListEntry) =
-        mHeadsUpManager.isHeadsUpEntry(entry.key) || isEntryBinding(entry)
-                || isHeadsUpAnimatingAway(entry)
+        mHeadsUpManager.isHeadsUpEntry(entry.key) ||
+            isEntryBinding(entry) ||
+            isHeadsUpAnimatingAway(entry)
 
     private fun isHeadsUpAnimatingAway(entry: ListEntry): Boolean {
         if (!GroupHunAnimationFix.isEnabled) return false
@@ -735,19 +804,19 @@
     }
 
     /**
-     * Whether the notification is already heads up/binding per [isAttemptingToShowHun] OR if it
-     * has been updated so that it should heads up this update.  This method is permissive because
-     * it returns `true` even if the update would (in isolation of its group) cause the heads up to
-     * be retracted.  This is important for not retracting transferred group heads ups.
+     * Whether the notification is already heads up/binding per [isAttemptingToShowHun] OR if it has
+     * been updated so that it should heads up this update. This method is permissive because it
+     * returns `true` even if the update would (in isolation of its group) cause the heads up to be
+     * retracted. This is important for not retracting transferred group heads ups.
      */
     private fun isGoingToShowHunNoRetract(entry: ListEntry) =
         mPostedEntries[entry.key]?.calculateShouldBeHeadsUpNoRetract ?: isAttemptingToShowHun(entry)
 
     /**
      * If the notification has been updated, then whether it should HUN in isolation, otherwise
-     * defers to the already heads up/binding state of [isAttemptingToShowHun].  This method is
-     * strict because any update which would revoke the heads up supersedes the current
-     * heads up/binding state.
+     * defers to the already heads up/binding state of [isAttemptingToShowHun]. This method is
+     * strict because any update which would revoke the heads up supersedes the current heads
+     * up/binding state.
      */
     private fun isGoingToShowHunStrict(entry: ListEntry) =
         mPostedEntries[entry.key]?.calculateShouldBeHeadsUpStrict ?: isAttemptingToShowHun(entry)
@@ -779,14 +848,21 @@
         val key = entry.key
         val isHeadsUpAlready: Boolean
             get() = isHeadsUpEntry || isBinding
+
         val calculateShouldBeHeadsUpStrict: Boolean
             get() = shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain || isHeadsUpAlready)
+
         val calculateShouldBeHeadsUpNoRetract: Boolean
             get() = isHeadsUpAlready || (shouldHeadsUpEver && (wasAdded || shouldHeadsUpAgain))
     }
 }
 
-private enum class GroupLocation { Detached, Isolated, Summary, Child }
+private enum class GroupLocation {
+    Detached,
+    Isolated,
+    Summary,
+    Child
+}
 
 private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation =
     getOrDefault(key, GroupLocation.Detached)
@@ -804,6 +880,7 @@
 /** Mutates the HeadsUp state of notifications. */
 private interface HunMutator {
     fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
+
     fun removeNotification(key: String, releaseImmediately: Boolean)
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
index ac2a0d8..1e0e597a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
@@ -20,10 +20,14 @@
 import android.os.UserHandle
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.server.notification.Flags.screenshareNotificationHiding
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
 import com.android.systemui.statusbar.notification.DynamicPrivacyController
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
@@ -32,27 +36,33 @@
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
 
 @Module(includes = [PrivateSensitiveContentCoordinatorModule::class])
 interface SensitiveContentCoordinatorModule
 
 @Module
 interface PrivateSensitiveContentCoordinatorModule {
-    @Binds
-    fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
+    @Binds fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
 }
 
 /** Coordinates re-inflation and post-processing of sensitive notification content. */
 interface SensitiveContentCoordinator : Coordinator
 
 @CoordinatorScope
-class SensitiveContentCoordinatorImpl @Inject constructor(
+class SensitiveContentCoordinatorImpl
+@Inject
+constructor(
     private val dynamicPrivacyController: DynamicPrivacyController,
     private val lockscreenUserManager: NotificationLockscreenUserManager,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -61,45 +71,85 @@
     private val selectedUserInteractor: SelectedUserInteractor,
     private val sensitiveNotificationProtectionController:
         SensitiveNotificationProtectionController,
-) : Invalidator("SensitiveContentInvalidator"),
-        SensitiveContentCoordinator,
-        DynamicPrivacyController.Listener,
-        OnBeforeRenderListListener {
-    private val onSensitiveStateChanged = Runnable() {
-        invalidateList("onSensitiveStateChanged")
-    }
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val sceneInteractor: SceneInteractor,
+    @Application private val scope: CoroutineScope,
+) :
+    Invalidator("SensitiveContentInvalidator"),
+    SensitiveContentCoordinator,
+    DynamicPrivacyController.Listener,
+    OnBeforeRenderListListener {
+    private var inTransitionFromLockedToGone = false
 
-    private val screenshareSecretFilter = object : NotifFilter("ScreenshareSecretFilter") {
-        val NotificationEntry.isSecret
-            get() = channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET ||
-                sbn.notification?.visibility == Notification.VISIBILITY_SECRET
-        override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
-            return screenshareNotificationHiding() &&
-                sensitiveNotificationProtectionController.isSensitiveStateActive &&
-                entry.isSecret
+    private val onSensitiveStateChanged = Runnable() { invalidateList("onSensitiveStateChanged") }
+
+    private val screenshareSecretFilter =
+        object : NotifFilter("ScreenshareSecretFilter") {
+            val NotificationEntry.isSecret
+                get() =
+                    channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET ||
+                        sbn.notification?.visibility == Notification.VISIBILITY_SECRET
+
+            override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
+                return screenshareNotificationHiding() &&
+                    sensitiveNotificationProtectionController.isSensitiveStateActive &&
+                    entry.isSecret
+            }
         }
-    }
 
     override fun attach(pipeline: NotifPipeline) {
         dynamicPrivacyController.addListener(this)
         if (screenshareNotificationHiding()) {
-            sensitiveNotificationProtectionController
-                .registerSensitiveStateListener(onSensitiveStateChanged)
+            sensitiveNotificationProtectionController.registerSensitiveStateListener(
+                onSensitiveStateChanged
+            )
         }
         pipeline.addOnBeforeRenderListListener(this)
         pipeline.addPreRenderInvalidator(this)
         if (screenshareNotificationHiding()) {
             pipeline.addFinalizeFilter(screenshareSecretFilter)
         }
+
+        if (SceneContainerFlag.isEnabled) {
+            scope.launch {
+                sceneInteractor.transitionState
+                    .mapNotNull {
+                        val transitioningToGone = it.isTransitioning(to = Scenes.Gone)
+                        val deviceEntered = deviceEntryInteractor.isDeviceEntered.value
+                        when {
+                            transitioningToGone && !deviceEntered -> true
+                            !transitioningToGone -> false
+                            else -> null
+                        }
+                    }
+                    .distinctUntilChanged()
+                    .collect {
+                        inTransitionFromLockedToGone = it
+                        invalidateList("inTransitionFromLockedToGoneChanged")
+                    }
+            }
+        }
     }
 
     override fun onDynamicPrivacyChanged(): Unit = invalidateList("onDynamicPrivacyChanged")
 
+    private val isKeyguardGoingAway: Boolean
+        get() {
+            if (SceneContainerFlag.isEnabled) {
+                return inTransitionFromLockedToGone
+            } else {
+                return keyguardStateController.isKeyguardGoingAway
+            }
+        }
+
     override fun onBeforeRenderList(entries: List<ListEntry>) {
-        if (keyguardStateController.isKeyguardGoingAway ||
+        if (
+            isKeyguardGoingAway ||
                 statusBarStateController.state == StatusBarState.KEYGUARD &&
-                keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
-                        selectedUserInteractor.getSelectedUserId())) {
+                    keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
+                        selectedUserInteractor.getSelectedUserId()
+                    )
+        ) {
             // don't update yet if:
             // - the keyguard is currently going away
             // - LS is about to be dismissed by a biometric that bypasses LS (avoid notif flash)
@@ -109,35 +159,40 @@
             return
         }
 
-        val isSensitiveContentProtectionActive = screenshareNotificationHiding() &&
-            sensitiveNotificationProtectionController.isSensitiveStateActive
+        val isSensitiveContentProtectionActive =
+            screenshareNotificationHiding() &&
+                sensitiveNotificationProtectionController.isSensitiveStateActive
         val currentUserId = lockscreenUserManager.currentUserId
         val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId)
-        val deviceSensitive = (devicePublic &&
+        val deviceSensitive =
+            (devicePublic &&
                 !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) ||
                 isSensitiveContentProtectionActive
         val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked
         for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) {
             val notifUserId = entry.sbn.user.identifier
-            val userLockscreen = devicePublic ||
-                    lockscreenUserManager.isLockscreenPublicMode(notifUserId)
-            val userPublic = when {
-                // if we're not on the lockscreen, we're definitely private
-                !userLockscreen -> false
-                // we are on the lockscreen, so unless we're dynamically unlocked, we're
-                // definitely public
-                !dynamicallyUnlocked -> true
-                // we're dynamically unlocked, but check if the notification needs
-                // a separate challenge if it's from a work profile
-                else -> when (notifUserId) {
-                    currentUserId -> false
-                    UserHandle.USER_ALL -> false
-                    else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
+            val userLockscreen =
+                devicePublic || lockscreenUserManager.isLockscreenPublicMode(notifUserId)
+            val userPublic =
+                when {
+                    // if we're not on the lockscreen, we're definitely private
+                    !userLockscreen -> false
+                    // we are on the lockscreen, so unless we're dynamically unlocked, we're
+                    // definitely public
+                    !dynamicallyUnlocked -> true
+                    // we're dynamically unlocked, but check if the notification needs
+                    // a separate challenge if it's from a work profile
+                    else ->
+                        when (notifUserId) {
+                            currentUserId -> false
+                            UserHandle.USER_ALL -> false
+                            else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
+                        }
                 }
-            }
 
-            val shouldProtectNotification = screenshareNotificationHiding() &&
-                sensitiveNotificationProtectionController.shouldProtectNotification(entry)
+            val shouldProtectNotification =
+                screenshareNotificationHiding() &&
+                    sensitiveNotificationProtectionController.shouldProtectNotification(entry)
 
             val needsRedaction = lockscreenUserManager.needsRedaction(entry)
             val isSensitive = userPublic && needsRedaction
@@ -149,9 +204,7 @@
     }
 }
 
-private fun extractAllRepresentativeEntries(
-    entries: List<ListEntry>
-): Sequence<NotificationEntry> =
+private fun extractAllRepresentativeEntries(entries: List<ListEntry>): Sequence<NotificationEntry> =
     entries.asSequence().flatMap(::extractAllRepresentativeEntries)
 
 private fun extractAllRepresentativeEntries(listEntry: ListEntry): Sequence<NotificationEntry> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
index a7970c7..af21e75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
@@ -19,14 +19,16 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.settings.SecureSettingsRepositoryModule
+import com.android.systemui.settings.SystemSettingsRepositoryModule
 import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
 import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
 import dagger.Module
 import dagger.Provides
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 
-@Module(includes = [SecureSettingsRepositoryModule::class])
+@Module(includes = [SecureSettingsRepositoryModule::class, SystemSettingsRepositoryModule::class])
 object NotificationSettingsRepositoryModule {
     @Provides
     @SysUISingleton
@@ -34,10 +36,12 @@
         @Background backgroundScope: CoroutineScope,
         @Background backgroundDispatcher: CoroutineDispatcher,
         secureSettingsRepository: SecureSettingsRepository,
+        systemSettingsRepository: SystemSettingsRepository,
     ): NotificationSettingsRepository =
         NotificationSettingsRepository(
             backgroundScope,
             backgroundDispatcher,
-            secureSettingsRepository
+            secureSettingsRepository,
+            systemSettingsRepository
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 90a05ef..2956432 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -109,7 +109,7 @@
             .map {
                 secureSettings.getIntForUser(
                     name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
-                    def = 0,
+                    default = 0,
                     userHandle = UserHandle.USER_CURRENT,
                 ) == 1
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 9d09595..a6ca3ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -46,6 +46,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
 import com.android.systemui.statusbar.StatusBarState.SHADE
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS
@@ -57,7 +58,6 @@
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.util.NotificationChannels
 import com.android.systemui.util.settings.GlobalSettings
-import com.android.systemui.util.settings.SystemSettings
 import com.android.systemui.util.time.SystemClock
 import com.android.wm.shell.bubbles.Bubbles
 import java.util.Optional
@@ -148,12 +148,12 @@
         }
 }
 
-class PeekDndSuppressor() :
+class PeekDndSuppressor :
     VisualInterruptionFilter(types = setOf(PEEK), reason = "suppressed by DND") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressPeek()
 }
 
-class PeekNotImportantSuppressor() :
+class PeekNotImportantSuppressor :
     VisualInterruptionFilter(types = setOf(PEEK), reason = "importance < HIGH") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_HIGH
 }
@@ -194,12 +194,12 @@
         }
 }
 
-class PulseEffectSuppressor() :
+class PulseEffectSuppressor :
     VisualInterruptionFilter(types = setOf(PULSE), reason = "suppressed by DND") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressAmbient()
 }
 
-class PulseLockscreenVisibilityPrivateSuppressor() :
+class PulseLockscreenVisibilityPrivateSuppressor :
     VisualInterruptionFilter(
         types = setOf(PULSE),
         reason = "hidden by lockscreen visibility override"
@@ -208,12 +208,12 @@
         entry.ranking.lockscreenVisibilityOverride == VISIBILITY_PRIVATE
 }
 
-class PulseLowImportanceSuppressor() :
+class PulseLowImportanceSuppressor :
     VisualInterruptionFilter(types = setOf(PULSE), reason = "importance < DEFAULT") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_DEFAULT
 }
 
-class HunGroupAlertBehaviorSuppressor() :
+class HunGroupAlertBehaviorSuppressor :
     VisualInterruptionFilter(
         types = setOf(PEEK, PULSE),
         reason = "suppressive group alert behavior"
@@ -222,26 +222,23 @@
         entry.sbn.let { it.isGroup && it.notification.suppressAlertingDueToGrouping() }
 }
 
-class HunSilentNotificationSuppressor() :
-    VisualInterruptionFilter(
-        types = setOf(PEEK, PULSE),
-        reason = "notification isSilent"
-    ) {
+class HunSilentNotificationSuppressor :
+    VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "notification isSilent") {
     override fun shouldSuppress(entry: NotificationEntry) =
         entry.sbn.let { Flags.notificationSilentFlag() && it.notification.isSilent }
 }
 
-class HunJustLaunchedFsiSuppressor() :
+class HunJustLaunchedFsiSuppressor :
     VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "just launched FSI") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.hasJustLaunchedFullScreenIntent()
 }
 
-class BubbleNotAllowedSuppressor() :
-    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble") {
+class BubbleNotAllowedSuppressor :
+    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble", isSpammy = true) {
     override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble()
 }
 
-class BubbleNoMetadataSuppressor() :
+class BubbleNoMetadataSuppressor :
     VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") {
 
     private fun isValidMetadata(metadata: BubbleMetadata?) =
@@ -264,6 +261,7 @@
 
 /**
  * Set with:
+ *
  * adb shell setprop persist.force_show_avalanche_edu_once 1 && adb shell stop; adb shell start
  */
 private const val FORCE_SHOW_AVALANCHE_EDU_ONCE = "persist.force_show_avalanche_edu_once"
@@ -273,7 +271,7 @@
 class AvalancheSuppressor(
     private val avalancheProvider: AvalancheProvider,
     private val systemClock: SystemClock,
-    private val systemSettings: SystemSettings,
+    private val settingsInteractor: NotificationSettingsInteractor,
     private val packageManager: PackageManager,
     private val uiEventLogger: UiEventLogger,
     private val context: Context,
@@ -298,7 +296,7 @@
     // education HUNs.
     private var hasShownOnceForDebug = false
 
-    private fun shouldShowEdu() : Boolean {
+    private fun shouldShowEdu(): Boolean {
         val forceShowOnce = SystemProperties.get(FORCE_SHOW_AVALANCHE_EDU_ONCE, "").equals("1")
         return !hasSeenEdu || (forceShowOnce && !hasShownOnceForDebug)
     }
@@ -361,28 +359,26 @@
         return true
     }
 
-    /**
-     * Show avalanche education HUN from SystemUI.
-     */
+    /** Show avalanche education HUN from SystemUI. */
     private fun showEdu() {
         val res = context.resources
-        val titleStr = res.getString(
-            com.android.systemui.res.R.string.adaptive_notification_edu_hun_title)
-        val textStr = res.getString(
-            com.android.systemui.res.R.string.adaptive_notification_edu_hun_text)
-        val actionStr = res.getString(
-            com.android.systemui.res.R.string.go_to_adaptive_notification_settings)
+        val titleStr =
+            res.getString(com.android.systemui.res.R.string.adaptive_notification_edu_hun_title)
+        val textStr =
+            res.getString(com.android.systemui.res.R.string.adaptive_notification_edu_hun_text)
+        val actionStr =
+            res.getString(com.android.systemui.res.R.string.go_to_adaptive_notification_settings)
 
         val intent = Intent(Settings.ACTION_MANAGE_ADAPTIVE_NOTIFICATIONS)
-        val pendingIntent = PendingIntent.getActivity(
-            context, 0, intent,
-            PendingIntent.FLAG_IMMUTABLE
-        )
+        val pendingIntent =
+            PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
 
         // Replace "System UI" app name with "Android System"
         val bundle = Bundle()
-        bundle.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
-                context.getString(com.android.internal.R.string.android_system_label))
+        bundle.putString(
+            Notification.EXTRA_SUBSTITUTE_APP_NAME,
+            context.getString(com.android.internal.R.string.android_system_label)
+        )
 
         val builder =
             Notification.Builder(context, NotificationChannels.ALERTS)
@@ -400,7 +396,7 @@
 
         notificationManager.notify(SystemMessage.NOTE_ADAPTIVE_NOTIFICATIONS, builder.build())
         hasSeenEdu = true
-        hasShownOnceForDebug = true;
+        hasShownOnceForDebug = true
     }
 
     private fun calculateState(entry: NotificationEntry): State {
@@ -452,7 +448,6 @@
     }
 
     private fun isCooldownEnabled(): Boolean {
-        return systemSettings.getInt(Settings.System.NOTIFICATION_COOLDOWN_ENABLED, /* def */ 1) ==
-            1
+        return settingsInteractor.isCooldownEnabled.value
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
index 1470b03..c204ea9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.interruption
 
+import android.util.Log
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.core.LogLevel.INFO
@@ -24,11 +25,15 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision
 import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 class VisualInterruptionDecisionLogger
 @Inject
 constructor(@NotificationInterruptLog val buffer: LogBuffer) {
+
+    val spew: Boolean = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
+
     fun logHeadsUpFeatureChanged(isEnabled: Boolean) {
         buffer.log(
             TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index 1c476ce..8e8d9b6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.Decision
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision
@@ -72,7 +73,8 @@
     private val packageManager: PackageManager,
     private val bubbles: Optional<Bubbles>,
     private val context: Context,
-    private val notificationManager: NotificationManager
+    private val notificationManager: NotificationManager,
+    private val settingsInteractor: NotificationSettingsInteractor
 ) : VisualInterruptionDecisionProvider {
 
     init {
@@ -93,7 +95,8 @@
     private constructor(
         val decision: DecisionImpl,
         override val uiEventId: UiEventEnum? = null,
-        override val eventLogData: EventLogData? = null
+        override val eventLogData: EventLogData? = null,
+        val isSpammy: Boolean = false,
     ) : Loggable {
         companion object {
             val unsuppressed =
@@ -111,7 +114,8 @@
                 LoggableDecision(
                     DecisionImpl(shouldInterrupt = false, logReason = suppressor.reason),
                     uiEventId = suppressor.uiEventId,
-                    eventLogData = suppressor.eventLogData
+                    eventLogData = suppressor.eventLogData,
+                    isSpammy = suppressor.isSpammy,
                 )
         }
     }
@@ -183,8 +187,15 @@
 
         if (NotificationAvalancheSuppression.isEnabled) {
             addFilter(
-                AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                        uiEventLogger, context, notificationManager)
+                AvalancheSuppressor(
+                    avalancheProvider,
+                    systemClock,
+                    settingsInteractor,
+                    packageManager,
+                    uiEventLogger,
+                    context,
+                    notificationManager
+                )
             )
             avalancheProvider.register()
         }
@@ -278,7 +289,9 @@
         entry: NotificationEntry,
         loggableDecision: LoggableDecision
     ) {
-        logger.logDecision(type.name, entry, loggableDecision.decision)
+        if (!loggableDecision.isSpammy || logger.spew) {
+            logger.logDecision(type.name, entry, loggableDecision.decision)
+        }
         logEvents(entry, loggableDecision)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
index ee79727..5fe75c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
@@ -59,6 +59,10 @@
     /** Optional data to be logged in the EventLog when this suppresses an interruption. */
     val eventLogData: EventLogData?
 
+    /** Whether the interruption is spammy and should be dropped under normal circumstances. */
+    val isSpammy: Boolean
+        get() = false
+
     /**
      * Called after the suppressor is added to the [VisualInterruptionDecisionProvider] but before
      * any other methods are called on the suppressor.
@@ -76,7 +80,7 @@
     constructor(
         types: Set<VisualInterruptionType>,
         reason: String
-    ) : this(types, reason, /* uiEventId = */ null)
+    ) : this(types, reason, /* uiEventId= */ null)
 
     /** @return true if these interruptions should be suppressed right now. */
     abstract fun shouldSuppress(): Boolean
@@ -87,12 +91,13 @@
     override val types: Set<VisualInterruptionType>,
     override val reason: String,
     override val uiEventId: UiEventEnum? = null,
-    override val eventLogData: EventLogData? = null
+    override val eventLogData: EventLogData? = null,
+    override val isSpammy: Boolean = false,
 ) : VisualInterruptionSuppressor {
     constructor(
         types: Set<VisualInterruptionType>,
         reason: String
-    ) : this(types, reason, /* uiEventId = */ null)
+    ) : this(types, reason, /* uiEventId= */ null)
 
     /**
      * @param entry the notification to consider suppressing
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt
index 5867612..3b30c86 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.kt
@@ -29,7 +29,7 @@
     val token: FlagToken
         get() = FlagToken(FLAG_NAME, isEnabled)
 
-    /** Are sections sorted by time? */
+    /** Return whether the fix is enabled */
     @JvmStatic
     inline val isEnabled
         get() = Flags.notificationGroupHunRemovalAnimationFix()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 5d2b61b..2f3b3a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1544,12 +1544,7 @@
     public void setExpandedHeight(float height) {
         final boolean skipHeightUpdate = shouldSkipHeightUpdate();
 
-        // when scene framework is enabled and in single shade, updateStackPosition is already
-        // called by updateTopPadding every time the stack moves, so skip it here to avoid
-        // flickering.
-        if (!SceneContainerFlag.isEnabled() || mShouldUseSplitNotificationShade) {
-            updateStackPosition();
-        }
+        updateStackPosition();
 
         if (!skipHeightUpdate) {
             mExpandedHeight = height;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index a072ea6..fb1c525 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -124,6 +124,7 @@
 import com.android.systemui.statusbar.notification.row.NotificationGuts;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
+import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix;
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -1987,6 +1988,10 @@
                 NotificationEntry entry = row.getEntry();
                 mHeadsUpAppearanceController.updateHeader(entry);
                 mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(entry);
+                if (GroupHunAnimationFix.isEnabled() && !animatingAway) {
+                    // invalidate list to make sure the row is sorted to the correct section
+                    mHeadsUpManager.onEntryAnimatingAwayEnded(entry);
+                }
             });
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 461a38d..b6de78e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2995,7 +2995,7 @@
                 @Override
                 public void onFalse() {
                     // Hides quick settings, bouncer, and quick-quick settings.
-                    mStatusBarKeyguardViewManager.reset(true);
+                    mStatusBarKeyguardViewManager.reset(true, /* isFalsingReset= */true);
                 }
             };
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index a32d5fe..ca1fb78b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -437,6 +437,13 @@
         mNotificationShadeWindowController.setDozeScreenBrightness(brightness);
     }
 
+
+    @Override
+    public void setDozeScreenBrightnessFloat(float brightness) {
+        mDozeLog.traceDozeScreenBrightnessFloat(brightness);
+        mNotificationShadeWindowController.setDozeScreenBrightnessFloat(brightness);
+    }
+
     @Override
     public void setAodDimmingScrim(float scrimOpacity) {
         mDozeLog.traceSetAodDimmingScrim(scrimOpacity);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index b2035e1..25d9cc7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -117,7 +117,7 @@
 
         @Override
         public HeadsUpEntryPhone acquire() {
-            NotificationsHeadsUpRefactor.assertInLegacyMode();
+            NotificationThrottleHun.assertInLegacyMode();
             if (!mPoolObjects.isEmpty()) {
                 return mPoolObjects.pop();
             }
@@ -126,7 +126,7 @@
 
         @Override
         public boolean release(@NonNull HeadsUpEntryPhone instance) {
-            NotificationsHeadsUpRefactor.assertInLegacyMode();
+            NotificationThrottleHun.assertInLegacyMode();
             mPoolObjects.push(instance);
             return true;
         }
@@ -428,7 +428,7 @@
     @NonNull
     @Override
     protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
-        if (NotificationsHeadsUpRefactor.isEnabled()) {
+        if (NotificationThrottleHun.isEnabled()) {
             return new HeadsUpEntryPhone(entry);
         } else {
             HeadsUpEntryPhone headsUpEntry = mEntryPool.acquire();
@@ -454,7 +454,7 @@
     @Override
     protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
         super.onEntryRemoved(headsUpEntry);
-        if (!NotificationsHeadsUpRefactor.isEnabled()) {
+        if (!NotificationThrottleHun.isEnabled()) {
             mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
         }
         updateTopHeadsUpFlow();
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 41b69a7..0b8f18e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -708,7 +708,7 @@
      * Shows the notification keyguard or the bouncer depending on
      * {@link #needsFullscreenBouncer()}.
      */
-    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
+    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
         boolean isDozing = mDozing;
         if (Flags.simPinRaceConditionOnRestart()) {
             KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue()
@@ -734,8 +734,12 @@
                         mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
                     }
                 }
-            } else {
-                Log.e(TAG, "Attempted to show the sim bouncer when it is already showing.");
+            } else if (!isFalsingReset) {
+                // Falsing resets can cause this to flicker, so don't reset in this case
+                Log.i(TAG, "Sim bouncer is already showing, issuing a refresh");
+                mPrimaryBouncerInteractor.hide();
+                mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+
             }
         } else {
             mCentralSurfaces.showKeyguard();
@@ -957,6 +961,10 @@
 
     @Override
     public void reset(boolean hideBouncerWhenShowing) {
+        reset(hideBouncerWhenShowing, /* isFalsingReset= */false);
+    }
+
+    public void reset(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
         if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) {
             final boolean isOccluded = mKeyguardStateController.isOccluded();
             // Hide quick settings.
@@ -968,7 +976,7 @@
                     hideBouncer(false /* destroyView */);
                 }
             } else {
-                showBouncerOrKeyguard(hideBouncerWhenShowing);
+                showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
             }
             if (hideBouncerWhenShowing) {
                 hideAlternateBouncer(true);
@@ -1106,7 +1114,9 @@
                     SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__OCCLUDED);
             if (mCentralSurfaces.isLaunchingActivityOverLockscreen()) {
                 final Runnable postCollapseAction = () -> {
-                    mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
+                    if (!Flags.useTransitionsForKeyguardOccluded()) {
+                        mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
+                    }
                     reset(true /* hideBouncerWhenShowing */);
                 };
                 if (mCentralSurfaces.isDismissingShadeForActivityLaunch()) {
@@ -1122,7 +1132,9 @@
             SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
                     SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__SHOWN);
         }
-        mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
+        if (!Flags.useTransitionsForKeyguardOccluded()) {
+            mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
+        }
 
         // setDozing(false) will call reset once we stop dozing. Also, if we're going away, there's
         // no need to reset the keyguard views as we'll be gone shortly. Resetting now could cause
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index a7c5f78..03ec41d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -20,6 +20,7 @@
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyManager
 import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback
 import android.telephony.satellite.SatelliteManager
 import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
 import android.telephony.satellite.SatelliteModemStateCallback
@@ -37,7 +38,6 @@
 import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.dagger.VerboseDeviceBasedSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
-import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
@@ -60,11 +60,9 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
@@ -122,15 +120,9 @@
 }
 
 /**
- * Basically your everyday run-of-the-mill system service listener, with three notable exceptions.
+ * Basically your everyday run-of-the-mill system service listener, with two notable exceptions.
  *
- * First, there is an availability bit that we are tracking via [SatelliteManager]. See
- * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about
- * this bit is that there is no callback that exists. Therefore we implement a simple polling
- * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see
- * [POLLING_INTERVAL_MS]) and see what the current state is.
- *
- * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See
+ * First, there are cases when simply requesting information from SatelliteManager can fail. See
  * [SatelliteSupport] for details on how we track the state. What's worth noting here is that
  * SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental
  * data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method
@@ -138,7 +130,7 @@
  * [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told
  * us that satellite is supported. Therefore, we don't expect exceptions to be thrown.
  *
- * Lastly, this class is designed to wait a full minute of process uptime before making any requests
+ * Second, this class is designed to wait a full minute of process uptime before making any requests
  * to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that
  * is still booting up or anything like that. We can tune or remove this behavior in the future if
  * necessary.
@@ -158,8 +150,6 @@
 
     private val satelliteManager: SatelliteManager?
 
-    override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean>
-
     // Some calls into satellite manager will throw exceptions if it is not supported.
     // This is never expected to change after boot, but may need to be retried in some cases
     @get:VisibleForTesting
@@ -221,8 +211,6 @@
     init {
         satelliteManager = satelliteManagerOpt.getOrNull()
 
-        isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
-
         if (satelliteManager != null) {
             // Outer scope launch allows us to delay until MIN_UPTIME
             scope.launch {
@@ -233,10 +221,7 @@
                     { "Checked for system support. support=$str1" },
                 )
 
-                // Second, launch a job to poll for service availability based on location
-                scope.launch { pollForAvailabilityBasedOnLocation() }
-
-                // Third, register a listener to let us know if there are changes to support
+                // Second, register a listener to let us know if there are changes to support
                 scope.launch { listenForChangesToSatelliteSupport(satelliteManager) }
             }
         } else {
@@ -259,28 +244,43 @@
         return sm.checkSatelliteSupported()
     }
 
-    /*
-     * As there is no listener available for checking satellite allowed, we must poll the service.
-     * Defaulting to polling at most once every 20m while active. Subsequent OOS events will restart
-     * the job, so a flaky connection might cause more frequent checks.
-     */
-    private suspend fun pollForAvailabilityBasedOnLocation() {
+    override val isSatelliteAllowedForCurrentLocation =
         satelliteSupport
             .whenSupported(
-                supported = ::isSatelliteAllowedHasListener,
+                supported = ::isSatelliteAvailableFlow,
                 orElse = flowOf(false),
                 retrySignal = telephonyProcessCrashedEvent,
             )
-            .collectLatest { hasSubscribers ->
-                if (hasSubscribers) {
-                    while (true) {
-                        logBuffer.i { "requestIsCommunicationAllowedForCurrentLocation" }
-                        checkIsSatelliteAllowed()
-                        delay(POLLING_INTERVAL_MS)
+            .stateIn(scope, SharingStarted.Lazily, false)
+
+    private fun isSatelliteAvailableFlow(sm: SupportedSatelliteManager): Flow<Boolean> =
+        conflatedCallbackFlow {
+                val callback = SatelliteCommunicationAllowedStateCallback { allowed ->
+                    logBuffer.i({ bool1 = allowed }) {
+                        "onSatelliteCommunicationAllowedStateChanged: $bool1"
+                    }
+
+                    trySend(allowed)
+                }
+
+                var registered = false
+                try {
+                    sm.registerForCommunicationAllowedStateChanged(
+                        bgDispatcher.asExecutor(),
+                        callback
+                    )
+                    registered = true
+                } catch (e: Exception) {
+                    logBuffer.e("Error calling registerForCommunicationAllowedStateChanged", e)
+                }
+
+                awaitClose {
+                    if (registered) {
+                        sm.unregisterForCommunicationAllowedStateChanged(callback)
                     }
                 }
             }
-    }
+            .flowOn(bgDispatcher)
 
     /**
      * Register a callback with [SatelliteManager] to let us know if there is a change in satellite
@@ -410,14 +410,6 @@
             }
         }
 
-    /**
-     * Signal that we should start polling [checkIsSatelliteAllowed]. We only need to poll if there
-     * are active listeners to [isSatelliteAllowedForCurrentLocation]
-     */
-    @SuppressWarnings("unused")
-    private fun isSatelliteAllowedHasListener(sm: SupportedSatelliteManager): Flow<Boolean> =
-        isSatelliteAllowedForCurrentLocation.subscriptionCount.map { it > 0 }.distinctUntilChanged()
-
     override val connectionState =
         satelliteSupport
             .whenSupported(
@@ -485,28 +477,6 @@
             }
             .flowOn(bgDispatcher)
 
-    /** Fire off a request to check for satellite availability. Always runs on the bg context */
-    private suspend fun checkIsSatelliteAllowed() =
-        withContext(bgDispatcher) {
-            satelliteManager?.requestIsCommunicationAllowedForCurrentLocation(
-                bgDispatcher.asExecutor(),
-                object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
-                    override fun onError(e: SatelliteManager.SatelliteException) {
-                        logBuffer.e(
-                            "Found exception when checking availability",
-                            e,
-                        )
-                        isSatelliteAllowedForCurrentLocation.value = false
-                    }
-
-                    override fun onResult(allowed: Boolean) {
-                        logBuffer.i { "isSatelliteAllowedForCurrentLocation: $allowed" }
-                        isSatelliteAllowedForCurrentLocation.value = allowed
-                    }
-                }
-            )
-        }
-
     private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport =
         suspendCancellableCoroutine { continuation ->
             val cb =
@@ -546,9 +516,6 @@
         }
 
     companion object {
-        // TTL for satellite polling is twenty minutes
-        const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 20
-
         // Let the system boot up and stabilize before we check for system support
         const val MIN_UPTIME: Long = 1000 * 60
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index e23f6ad..3786958 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -171,7 +171,6 @@
         mLogger.logShowNotificationRequest(entry);
 
         Runnable runnable = () -> {
-            // TODO(b/315362456) log outside runnable too
             mLogger.logShowNotification(entry);
 
             // Add new entry and begin managing it
@@ -244,8 +243,10 @@
             return;
         }
         // TODO(b/328390331) move accessibility events to the view layer
-        headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-
+        if (headsUpEntry.mEntry != null) {
+            headsUpEntry.mEntry.sendAccessibilityEvent(
+                    AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+        }
         if (shouldHeadsUpAgain) {
             headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification");
             if (headsUpEntry != null) {
@@ -334,6 +335,9 @@
     }
 
     protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) {
+        if (entry == null) {
+            return false;
+        }
         final HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
         if (headsUpEntry == null) {
             // This should not happen since shouldHeadsUpBecomePinned is always called after adding
@@ -435,7 +439,7 @@
             onEntryRemoved(finalHeadsUpEntry);
             // TODO(b/328390331) move accessibility events to the view layer
             entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-            if (NotificationsHeadsUpRefactor.isEnabled()) {
+            if (NotificationThrottleHun.isEnabled()) {
                 finalHeadsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
             } else {
                 finalHeadsUpEntry.reset();
@@ -460,6 +464,15 @@
     }
 
     /**
+     * Called to notify the listeners that the HUN animating away animation has ended.
+     */
+    public void onEntryAnimatingAwayEnded(@NonNull NotificationEntry entry) {
+        for (OnHeadsUpChangedListener listener : mListeners) {
+            listener.onHeadsUpAnimatingAwayEnded(entry);
+        }
+    }
+
+    /**
      * Manager-specific logic, that should occur, when the entry is updated, and its posted time has
      * changed.
      *
@@ -508,6 +521,9 @@
         keySet.addAll(mAvalancheController.getWaitingKeys());
         for (String key : keySet) {
             HeadsUpEntry entry = getHeadsUpEntry(key);
+            if (entry.mEntry == null) {
+                continue;
+            }
             String packageName = entry.mEntry.getSbn().getPackageName();
             String snoozeKey = snoozeKey(packageName, mUser);
             mLogger.logPackageSnoozed(snoozeKey);
@@ -575,7 +591,7 @@
         pw.print("  now="); pw.println(mSystemClock.elapsedRealtime());
         pw.print("  mUser="); pw.println(mUser);
         for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
-            pw.print("  HeadsUpEntry="); pw.println(entry.mEntry);
+            pw.println(entry.mEntry == null ? "null" : entry.mEntry);
         }
         int n = mSnoozedPackages.size();
         pw.println("  snoozed packages: " + n);
@@ -595,7 +611,7 @@
     private boolean hasPinnedNotificationInternal() {
         for (String key : mHeadsUpEntryMap.keySet()) {
             HeadsUpEntry entry = getHeadsUpEntry(key);
-            if (entry.mEntry.isRowPinned()) {
+            if (entry.mEntry != null && entry.mEntry.isRowPinned()) {
                 return true;
             }
         }
@@ -620,7 +636,7 @@
                 // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay
                 // on the screen.
                 if (userUnPinned && headsUpEntry.mEntry != null) {
-                    if (headsUpEntry.mEntry.mustStayOnScreen()) {
+                    if (headsUpEntry.mEntry != null && headsUpEntry.mEntry.mustStayOnScreen()) {
                         headsUpEntry.mEntry.setHeadsUpIsVisible();
                     }
                 }
@@ -696,7 +712,7 @@
             return true;
         }
         return headsUpEntry == null || headsUpEntry.wasShownLongEnough()
-                || headsUpEntry.mEntry.isRowDismissed();
+                || (headsUpEntry.mEntry != null && headsUpEntry.mEntry.isRowDismissed());
     }
 
     /**
@@ -752,7 +768,7 @@
         @Nullable private Runnable mCancelRemoveRunnable;
 
         public HeadsUpEntry() {
-            NotificationsHeadsUpRefactor.assertInLegacyMode();
+            NotificationThrottleHun.assertInLegacyMode();
         }
 
         public HeadsUpEntry(NotificationEntry entry) {
@@ -763,7 +779,7 @@
 
         /** Attach a NotificationEntry. */
         public void setEntry(@NonNull final NotificationEntry entry) {
-            NotificationsHeadsUpRefactor.assertInLegacyMode();
+            NotificationThrottleHun.assertInLegacyMode();
             setEntry(entry, createRemoveRunnable(entry));
         }
 
@@ -875,6 +891,14 @@
         }
 
         public int compareNonTimeFields(HeadsUpEntry headsUpEntry) {
+            if (mEntry == null && headsUpEntry.mEntry == null) {
+                return 0;
+            } else if (headsUpEntry.mEntry == null) {
+                return -1;
+            } else if (mEntry == null) {
+                return 1;
+            }
+
             boolean selfFullscreen = hasFullScreenIntent(mEntry);
             boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry);
             if (selfFullscreen && !otherFullscreen) {
@@ -901,6 +925,13 @@
         }
 
         public int compareTo(@NonNull HeadsUpEntry headsUpEntry) {
+            if (mEntry == null && headsUpEntry.mEntry == null) {
+                return 0;
+            } else if (headsUpEntry.mEntry == null) {
+                return -1;
+            } else if (mEntry == null) {
+                return 1;
+            }
             boolean isPinned = mEntry.isRowPinned();
             boolean otherPinned = headsUpEntry.mEntry.isRowPinned();
             if (isPinned && !otherPinned) {
@@ -945,7 +976,7 @@
         }
 
         public void reset() {
-            NotificationsHeadsUpRefactor.assertInLegacyMode();
+            NotificationThrottleHun.assertInLegacyMode();
             cancelAutoRemovalCallbacks("reset()");
             mEntry = null;
             mRemoveRunnable = null;
@@ -965,7 +996,7 @@
                     mLogger.logAutoRemoveCanceled(mEntry, reason);
                 }
             };
-            if (isHeadsUpEntry(this.mEntry.getKey())) {
+            if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) {
                 mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
             } else {
                 // Just removed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
index 28a2a1f..fcf77d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
@@ -42,6 +42,7 @@
      *   should be ranked higher and 0 if they are equal.
      */
     fun compare(a: NotificationEntry?, b: NotificationEntry?): Int
+
     /**
      * Extends the lifetime of the currently showing pulsing notification so that the pulse lasts
      * longer.
@@ -184,6 +185,8 @@
     fun unpinAll(userUnPinned: Boolean)
 
     fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
+
+    fun onEntryAnimatingAwayEnded(entry: NotificationEntry)
 }
 
 /** Sets the animation state of the HeadsUpManager. */
@@ -204,41 +207,77 @@
 /* No op impl of HeadsUpManager. */
 class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager {
     override val allEntries = Stream.empty<NotificationEntry>()
+
     override fun addHeadsUpPhoneListener(listener: OnHeadsUpPhoneListenerChange) {}
+
     override fun addListener(listener: OnHeadsUpChangedListener) {}
+
     override fun addSwipedOutNotification(key: String) {}
+
     override fun canRemoveImmediately(key: String) = false
+
     override fun compare(a: NotificationEntry?, b: NotificationEntry?) = 0
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {}
+
     override fun extendHeadsUp() {}
+
     override fun getEarliestRemovalTime(key: String?) = 0L
+
     override fun getTouchableRegion(): Region? = null
+
     override fun getTopEntry() = null
+
     override fun hasPinnedHeadsUp() = false
+
     override fun isHeadsUpEntry(key: String) = false
+
     override fun isHeadsUpAnimatingAwayValue() = false
+
     override fun isSnoozed(packageName: String) = false
+
     override fun isSticky(key: String?) = false
+
     override fun isTrackingHeadsUp() = false
+
     override fun onExpandingFinished() {}
+
     override fun releaseAllImmediately() {}
+
     override fun removeListener(listener: OnHeadsUpChangedListener) {}
+
     override fun removeNotification(key: String, releaseImmediately: Boolean) = false
+
     override fun removeNotification(key: String, releaseImmediately: Boolean, animate: Boolean) =
         false
+
     override fun setAnimationStateHandler(handler: AnimationStateHandler) {}
+
     override fun setExpanded(entry: NotificationEntry, expanded: Boolean) {}
+
     override fun setGutsShown(entry: NotificationEntry, gutsShown: Boolean) {}
+
     override fun setHeadsUpAnimatingAway(headsUpAnimatingAway: Boolean) {}
+
     override fun setRemoteInputActive(entry: NotificationEntry, remoteInputActive: Boolean) {}
+
     override fun setTrackingHeadsUp(tracking: Boolean) {}
+
     override fun setUser(user: Int) {}
+
     override fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) {}
+
     override fun shouldSwallowClick(key: String): Boolean = false
+
     override fun showNotification(entry: NotificationEntry) {}
+
     override fun snooze() {}
+
     override fun unpinAll(userUnPinned: Boolean) {}
+
     override fun updateNotification(key: String, alert: Boolean) {}
+
+    override fun onEntryAnimatingAwayEnded(entry: NotificationEntry) {}
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
index 86998ab..de3bf04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/OnHeadsUpChangedListener.java
@@ -48,4 +48,9 @@
      * @param isHeadsUp whether the notification is now a headsUp notification
      */
     default void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) {}
+
+    /**
+     * Called on HUN disappearing animation ends
+     */
+    default void onHeadsUpAnimatingAwayEnded(@NonNull NotificationEntry entry) {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index cf9a78f..21ec14f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -20,6 +20,7 @@
 import android.os.UserManager.DISALLOW_CONFIG_LOCATION
 import android.os.UserManager.DISALLOW_MICROPHONE_TOGGLE
 import android.os.UserManager.DISALLOW_SHARE_LOCATION
+import com.android.systemui.Flags
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tileimpl.QSTileImpl
@@ -67,25 +68,18 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy
 import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.qs.tiles.viewmodel.StubQSTileViewModel
 import com.android.systemui.res.R
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import dagger.multibindings.IntoMap
 import dagger.multibindings.StringKey
+import javax.inject.Provider
 
 @Module
 interface PolicyModule {
 
-    /** Inject DndTile into tileMap in QSModule */
-    @Binds @IntoMap @StringKey(DndTile.TILE_SPEC) fun bindDndTile(dndTile: DndTile): QSTileImpl<*>
-
-    /** Inject ModesTile into tileMap in QSModule */
-    @Binds
-    @IntoMap
-    @StringKey(ModesTile.TILE_SPEC)
-    fun bindModesTile(modesTile: ModesTile): QSTileImpl<*>
-
     /** Inject WorkModeTile into tileMap in QSModule */
     @Binds
     @IntoMap
@@ -136,7 +130,19 @@
         const val CAMERA_TOGGLE_TILE_SPEC = "cameratoggle"
         const val MIC_TOGGLE_TILE_SPEC = "mictoggle"
         const val DND_TILE_SPEC = "dnd"
-        const val MODES_TILE_SPEC = "modes"
+
+        /** Inject DndTile or ModesTile into tileMap in QSModule based on feature flag */
+        @Provides
+        @IntoMap
+        @StringKey(DND_TILE_SPEC)
+        fun bindDndOrModesTile(
+            // Using providers to make sure that the unused tile isn't initialised at all if the
+            // flag is off.
+            dndTile: Provider<DndTile>,
+            modesTile: Provider<ModesTile>,
+        ): QSTileImpl<*> {
+            return if (android.app.Flags.modesUi()) modesTile.get() else dndTile.get()
+        }
 
         /** Inject flashlight config */
         @Provides
@@ -283,6 +289,7 @@
                         labelRes = R.string.quick_settings_work_mode_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                autoRemoveOnUnavailable = false,
             )
 
         /** Inject work mode into tileViewModelMap in QSModule */
@@ -386,51 +393,51 @@
             return factory.create(MICROPHONE)
         }
 
-        /** Inject microphone toggle config */
+        /** Inject DND tile or Modes tile config based on feature flag */
         @Provides
         @IntoMap
         @StringKey(DND_TILE_SPEC)
-        fun provideDndTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
-            QSTileConfig(
-                tileSpec = TileSpec.create(DND_TILE_SPEC),
-                uiConfig =
-                    QSTileUIConfig.Resource(
-                        iconRes = R.drawable.qs_dnd_icon_off,
-                        labelRes = R.string.quick_settings_dnd_label,
-                    ),
-                instanceId = uiEventLogger.getNewInstanceId(),
-            )
-
-        @Provides
-        @IntoMap
-        @StringKey(MODES_TILE_SPEC)
-        fun provideModesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
-            QSTileConfig(
-                tileSpec = TileSpec.create(MODES_TILE_SPEC),
-                uiConfig =
-                    QSTileUIConfig.Resource(
-                        iconRes = R.drawable.qs_dnd_icon_off,
-                        labelRes = R.string.quick_settings_modes_label,
-                    ),
-                instanceId = uiEventLogger.getNewInstanceId(),
-            )
+        fun provideDndOrModesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            if (android.app.Flags.modesUi()) {
+                QSTileConfig(
+                    tileSpec = TileSpec.create(DND_TILE_SPEC),
+                    uiConfig =
+                        QSTileUIConfig.Resource(
+                            iconRes = R.drawable.qs_dnd_icon_off,
+                            labelRes = R.string.quick_settings_modes_label,
+                        ),
+                    instanceId = uiEventLogger.getNewInstanceId(),
+                )
+            } else {
+                QSTileConfig(
+                    tileSpec = TileSpec.create(DND_TILE_SPEC),
+                    uiConfig =
+                        QSTileUIConfig.Resource(
+                            iconRes = R.drawable.qs_dnd_icon_off,
+                            labelRes = R.string.quick_settings_dnd_label,
+                        ),
+                    instanceId = uiEventLogger.getNewInstanceId(),
+                )
+            }
 
         /** Inject ModesTile into tileViewModelMap in QSModule */
         @Provides
         @IntoMap
-        @StringKey(MODES_TILE_SPEC)
+        @StringKey(DND_TILE_SPEC)
         fun provideModesTileViewModel(
             factory: QSTileViewModelFactory.Static<ModesTileModel>,
             mapper: ModesTileMapper,
             stateInteractor: ModesTileDataInteractor,
             userActionInteractor: ModesTileUserActionInteractor
         ): QSTileViewModel =
-            factory.create(
-                TileSpec.create(MODES_TILE_SPEC),
-                userActionInteractor,
-                stateInteractor,
-                mapper,
-            )
+            if (android.app.Flags.modesUi() && Flags.qsNewTilesFuture())
+                factory.create(
+                    TileSpec.create(DND_TILE_SPEC),
+                    userActionInteractor,
+                    stateInteractor,
+                    mapper,
+                )
+            else StubQSTileViewModel
     }
 
     /** Inject FlashlightTile into tileMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index 7a521a6..efd60f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -18,11 +18,15 @@
 
 import android.content.Context
 import android.provider.Settings
+import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
+import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
+import android.util.Log
 import androidx.concurrent.futures.await
 import com.android.settingslib.notification.data.repository.ZenModeRepository
 import com.android.settingslib.notification.modes.ZenIconLoader
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
 import java.time.Duration
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -34,11 +38,16 @@
  * An interactor that performs business logic related to the status and configuration of Zen Mode
  * (or Do Not Disturb/DND Mode).
  */
-class ZenModeInteractor @Inject constructor(private val repository: ZenModeRepository) {
+class ZenModeInteractor
+@Inject
+constructor(
+    private val zenModeRepository: ZenModeRepository,
+    private val notificationSettingsRepository: NotificationSettingsRepository,
+) {
     private val iconLoader: ZenIconLoader = ZenIconLoader.getInstance()
 
     val isZenModeEnabled: Flow<Boolean> =
-        repository.globalZenMode
+        zenModeRepository.globalZenMode
             .map {
                 when (it ?: Settings.Global.ZEN_MODE_OFF) {
                     Settings.Global.ZEN_MODE_ALARMS -> true
@@ -51,7 +60,9 @@
             .distinctUntilChanged()
 
     val areNotificationsHiddenInShade: Flow<Boolean> =
-        combine(isZenModeEnabled, repository.consolidatedNotificationPolicy) { dndEnabled, policy ->
+        combine(isZenModeEnabled, zenModeRepository.consolidatedNotificationPolicy) {
+                dndEnabled,
+                policy ->
                 if (!dndEnabled) {
                     false
                 } else {
@@ -61,17 +72,45 @@
             }
             .distinctUntilChanged()
 
-    val modes: Flow<List<ZenMode>> = repository.modes
+    val modes: Flow<List<ZenMode>> = zenModeRepository.modes
 
     suspend fun getModeIcon(mode: ZenMode, context: Context): Icon {
         return Icon.Loaded(mode.getIcon(context, iconLoader).await(), contentDescription = null)
     }
 
-    fun activateMode(zenMode: ZenMode, duration: Duration? = null) {
-        repository.activateMode(zenMode, duration)
+    fun activateMode(zenMode: ZenMode) {
+        if (zenMode.isManualDnd) {
+            val duration =
+                when (zenDuration) {
+                    ZEN_DURATION_PROMPT -> {
+                        Log.e(
+                            TAG,
+                            "Interactor cannot handle showing the zen duration prompt. " +
+                                "Please use EnableZenModeDialog when this setting is active."
+                        )
+                        null
+                    }
+                    ZEN_DURATION_FOREVER -> null
+                    else -> Duration.ofMinutes(zenDuration.toLong())
+                }
+
+            zenModeRepository.activateMode(zenMode, duration)
+        } else {
+            zenModeRepository.activateMode(zenMode)
+        }
     }
 
     fun deactivateMode(zenMode: ZenMode) {
-        repository.deactivateMode(zenMode)
+        zenModeRepository.deactivateMode(zenMode)
+    }
+
+    private val zenDuration
+        get() = notificationSettingsRepository.zenDuration.value
+
+    fun shouldAskForZenDuration(mode: ZenMode): Boolean =
+        mode.isManualDnd && (zenDuration == ZEN_DURATION_PROMPT)
+
+    companion object {
+        private const val TAG = "ZenModeInteractor"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
index 5bd26cc..7c1cb6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
@@ -29,7 +29,6 @@
     val text: String,
     val subtext: String,
     val enabled: Boolean,
-    val contentDescription: String,
     val onClick: () -> Unit,
     val onLongClick: () -> Unit,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
index c4aa03a..9422878f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -16,14 +16,18 @@
 
 package com.android.systemui.statusbar.policy.ui.dialog.viewmodel
 
+import android.app.Dialog
 import android.content.Context
 import android.content.Intent
 import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS
 import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID
+import com.android.settingslib.notification.modes.EnableZenModeDialog
 import com.android.settingslib.notification.modes.ZenMode
+import com.android.settingslib.notification.modes.ZenModeDialogMetricsLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
 import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
 import javax.inject.Inject
@@ -46,6 +50,8 @@
     @Background val bgDispatcher: CoroutineDispatcher,
     private val dialogDelegate: ModesDialogDelegate,
 ) {
+    private val zenDialogMetricsLogger = ZenModeDialogMetricsLogger(context)
+
     // Modes that should be displayed in the dialog
     private val visibleModes: Flow<List<ZenMode>> =
         zenModeInteractor.modes
@@ -84,9 +90,6 @@
                         text = mode.rule.name,
                         subtext = getTileSubtext(mode),
                         enabled = mode.isActive,
-                        // TODO(b/346519570): This should be some combination of the above, e.g.
-                        //  "ON: Do Not Disturb, Until Mon 08:09"; see DndTile.
-                        contentDescription = "",
                         onClick = {
                             if (!mode.rule.isEnabled) {
                                 openSettings(mode)
@@ -94,8 +97,13 @@
                                 zenModeInteractor.deactivateMode(mode)
                             } else {
                                 if (mode.rule.isManualInvocationAllowed) {
-                                    // TODO(b/346519570): Handle duration for DND mode.
-                                    zenModeInteractor.activateMode(mode)
+                                    if (zenModeInteractor.shouldAskForZenDuration(mode)) {
+                                        // NOTE: The dialog handles turning on the mode itself.
+                                        val dialog = makeZenModeDialog()
+                                        dialog.show()
+                                    } else {
+                                        zenModeInteractor.activateMode(mode)
+                                    }
                                 }
                             }
                         },
@@ -125,4 +133,20 @@
         val off = context.resources.getString(R.string.zen_mode_off)
         return mode.rule.triggerDescription ?: if (mode.isActive) on else off
     }
+
+    private fun makeZenModeDialog(): Dialog {
+        val dialog =
+            EnableZenModeDialog(
+                    context,
+                    R.style.Theme_SystemUI_Dialog,
+                    /* cancelIsNeutral= */ true,
+                    zenDialogMetricsLogger
+                )
+                .createDialog()
+        SystemUIDialog.applyFlags(dialog)
+        SystemUIDialog.setShowForAllUsers(dialog, true)
+        SystemUIDialog.registerDismissListener(dialog)
+        SystemUIDialog.setDialogSize(dialog)
+        return dialog
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt
new file mode 100644
index 0000000..c86ac2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad
+
+import com.android.systemui.touchpad.data.repository.TouchpadRepository
+import com.android.systemui.touchpad.data.repository.TouchpadRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class TouchpadModule {
+
+    @Binds
+    abstract fun bindTouchpadRepository(repository: TouchpadRepositoryImpl): TouchpadRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt
new file mode 100644
index 0000000..7131546
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.data.repository
+
+import android.hardware.input.InputManager
+import android.view.InputDevice.SOURCE_TOUCHPAD
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+interface TouchpadRepository {
+    /** Emits true if any touchpad is connected to the device, false otherwise. */
+    val isAnyTouchpadConnected: Flow<Boolean>
+}
+
+@SysUISingleton
+class TouchpadRepositoryImpl
+@Inject
+constructor(
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val inputManager: InputManager,
+    inputDeviceRepository: InputDeviceRepository
+) : TouchpadRepository {
+
+    override val isAnyTouchpadConnected: Flow<Boolean> =
+        inputDeviceRepository.deviceChange
+            .map { (ids, _) -> ids.any { id -> isTouchpad(id) } }
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
+
+    private fun isTouchpad(deviceId: Int): Boolean {
+        val device = inputManager.getInputDevice(deviceId) ?: return false
+        return device.supportsSource(SOURCE_TOUCHPAD)
+    }
+
+    companion object {
+        const val TAG = "TouchpadRepositoryImpl"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index 94ff65e..51dfef0 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -26,6 +26,7 @@
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -49,6 +50,7 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieComposition
 import com.airbnb.lottie.LottieProperty
 import com.airbnb.lottie.compose.LottieAnimation
 import com.airbnb.lottie.compose.LottieCompositionSpec
@@ -61,6 +63,9 @@
 import com.airbnb.lottie.compose.rememberLottieDynamicProperty
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
 
@@ -78,23 +83,49 @@
 ) {
     val screenColors = rememberScreenColors()
     BackHandler(onBack = onBack)
-    var gestureDone by remember { mutableStateOf(false) }
+    var gestureState by remember { mutableStateOf(GestureState.NOT_STARTED) }
     val swipeDistanceThresholdPx =
         LocalContext.current.resources.getDimensionPixelSize(
             com.android.internal.R.dimen.system_gestures_distance_threshold
         )
     val gestureHandler =
         remember(swipeDistanceThresholdPx) {
-            TouchpadGestureHandler(BACK, swipeDistanceThresholdPx, onDone = { gestureDone = true })
+            TouchpadGestureHandler(
+                BACK,
+                swipeDistanceThresholdPx,
+                onGestureStateChanged = { gestureState = it }
+            )
         }
+    TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
+        GestureTutorialContent(gestureState, onDoneButtonClicked, screenColors)
+    }
+}
+
+@Composable
+private fun TouchpadGesturesHandlingBox(
+    gestureHandler: TouchpadGestureHandler,
+    gestureState: GestureState,
+    modifier: Modifier = Modifier,
+    content: @Composable BoxScope.() -> Unit
+) {
     Box(
         modifier =
-            Modifier.fillMaxSize()
+            modifier
+                .fillMaxSize()
                 // we need to use pointerInteropFilter because some info about touchpad gestures is
                 // only available in MotionEvent
-                .pointerInteropFilter(onTouchEvent = gestureHandler::onMotionEvent)
+                .pointerInteropFilter(
+                    onTouchEvent = { event ->
+                        // FINISHED is the final state so we don't need to process touches anymore
+                        if (gestureState != FINISHED) {
+                            gestureHandler.onMotionEvent(event)
+                        } else {
+                            false
+                        }
+                    }
+                )
     ) {
-        GestureTutorialContent(gestureDone, onDoneButtonClicked, screenColors)
+        content()
     }
 }
 
@@ -126,14 +157,14 @@
 
 @Composable
 private fun GestureTutorialContent(
-    gestureDone: Boolean,
+    gestureState: GestureState,
     onDoneButtonClicked: () -> Unit,
     screenColors: TutorialScreenColors
 ) {
     val animatedColor by
         animateColorAsState(
             targetValue =
-                if (gestureDone) screenColors.successBackgroundColor
+                if (gestureState == FINISHED) screenColors.successBackgroundColor
                 else screenColors.backgroundColor,
             animationSpec = tween(durationMillis = 150, easing = LinearEasing),
             label = "backgroundColor"
@@ -148,15 +179,17 @@
         Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
             TutorialDescription(
                 titleTextId =
-                    if (gestureDone) R.string.touchpad_tutorial_gesture_done
+                    if (gestureState == FINISHED) R.string.touchpad_tutorial_gesture_done
                     else R.string.touchpad_back_gesture_action_title,
                 titleColor = screenColors.titleColor,
-                bodyTextId = R.string.touchpad_back_gesture_guidance,
+                bodyTextId =
+                    if (gestureState == FINISHED) R.string.touchpad_back_gesture_finished
+                    else R.string.touchpad_back_gesture_guidance,
                 modifier = Modifier.weight(1f)
             )
             Spacer(modifier = Modifier.width(76.dp))
             TutorialAnimation(
-                gestureDone,
+                gestureState,
                 screenColors.animationProperties,
                 modifier = Modifier.weight(1f).padding(top = 8.dp)
             )
@@ -189,27 +222,38 @@
 
 @Composable
 fun TutorialAnimation(
-    gestureDone: Boolean,
+    gestureState: GestureState,
     animationProperties: LottieDynamicProperties,
     modifier: Modifier = Modifier
 ) {
     Column(modifier = modifier.fillMaxWidth()) {
-        val resId = if (gestureDone) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
+        val resId =
+            if (gestureState == FINISHED) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
         val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(resId))
-        val progress by
-            animateLottieCompositionAsState(
-                composition,
-                iterations = if (gestureDone) 1 else LottieConstants.IterateForever
-            )
+        val progress = progressForGestureState(composition, gestureState)
         LottieAnimation(
             composition = composition,
-            progress = { progress },
+            progress = progress,
             dynamicProperties = animationProperties
         )
     }
 }
 
 @Composable
+private fun progressForGestureState(
+    composition: LottieComposition?,
+    gestureState: GestureState
+): () -> Float {
+    if (gestureState == IN_PROGRESS) {
+        return { 0f } // when gesture is in progress, animation should freeze on 1st frame
+    } else {
+        val iterations = if (gestureState == FINISHED) 1 else LottieConstants.IterateForever
+        val animationState by animateLottieCompositionAsState(composition, iterations = iterations)
+        return { animationState }
+    }
+}
+
+@Composable
 fun rememberColorFilterProperty(
     layerName: String,
     color: Color
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
index 1fa7a0c..6fa9bcd 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -17,23 +17,26 @@
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
 import android.view.MotionEvent
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import kotlin.math.abs
 
 /**
- * Monitor for touchpad gestures that calls [gestureDoneCallback] when gesture was successfully
- * done. All tracked motion events should be passed to [processTouchpadEvent]
+ * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState]
+ * changes. All tracked motion events should be passed to [processTouchpadEvent]
  */
 interface TouchpadGestureMonitor {
 
     val gestureDistanceThresholdPx: Int
-    val gestureDoneCallback: () -> Unit
+    val gestureStateChangedCallback: (GestureState) -> Unit
 
     fun processTouchpadEvent(event: MotionEvent)
 }
 
 class BackGestureMonitor(
     override val gestureDistanceThresholdPx: Int,
-    override val gestureDoneCallback: () -> Unit
+    override val gestureStateChangedCallback: (GestureState) -> Unit
 ) : TouchpadGestureMonitor {
 
     private var xStart = 0f
@@ -44,13 +47,16 @@
             MotionEvent.ACTION_DOWN -> {
                 if (isThreeFingerTouchpadSwipe(event)) {
                     xStart = event.x
+                    gestureStateChangedCallback(IN_PROGRESS)
                 }
             }
             MotionEvent.ACTION_UP -> {
                 if (isThreeFingerTouchpadSwipe(event)) {
                     val distance = abs(event.x - xStart)
                     if (distance >= gestureDistanceThresholdPx) {
-                        gestureDoneCallback()
+                        gestureStateChangedCallback(FINISHED)
+                    } else {
+                        gestureStateChangedCallback(NOT_STARTED)
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
new file mode 100644
index 0000000..446875a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.touchpad.tutorial.ui.gesture
+
+enum class GestureState {
+    NOT_STARTED,
+    IN_PROGRESS,
+    FINISHED
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
index 4ae9c7b..190da62 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
@@ -22,10 +22,10 @@
 
     fun toMonitor(
         swipeDistanceThresholdPx: Int,
-        gestureDoneCallback: () -> Unit
+        onStateChanged: (GestureState) -> Unit
     ): TouchpadGestureMonitor {
         return when (this) {
-            BACK -> BackGestureMonitor(swipeDistanceThresholdPx, gestureDoneCallback)
+            BACK -> BackGestureMonitor(swipeDistanceThresholdPx, onStateChanged)
             else -> throw IllegalArgumentException("Not implemented yet")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
index dc8471c..cac2a99 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -26,11 +26,11 @@
 class TouchpadGestureHandler(
     touchpadGesture: TouchpadGesture,
     swipeDistanceThresholdPx: Int,
-    onDone: () -> Unit
+    onGestureStateChanged: (GestureState) -> Unit
 ) {
 
     private val gestureRecognition =
-        touchpadGesture.toMonitor(swipeDistanceThresholdPx, gestureDoneCallback = onDone)
+        touchpadGesture.toMonitor(swipeDistanceThresholdPx, onStateChanged = onGestureStateChanged)
 
     fun onMotionEvent(event: MotionEvent): Boolean {
         // events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
index b5934ec..9125a91 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
@@ -336,7 +336,7 @@
      * @param name to look up in the table
      * @return the corresponding value, or null if not present
      */
-    fun getString(name: String): String
+    fun getString(name: String): String?
 
     /**
      * Store a name/value pair into the database.
@@ -385,15 +385,15 @@
      * an integer.
      *
      * @param name The name of the setting to retrieve.
-     * @param def Value to return if the setting is not defined.
-     * @return The setting's current value, or 'def' if it is not defined or not a valid integer.
+     * @param default Value to return if the setting is not defined.
+     * @return The setting's current value, or default if it is not defined or not a valid integer.
      */
-    fun getInt(name: String, def: Int): Int {
+    fun getInt(name: String, default: Int): Int {
         val v = getString(name)
         return try {
-            v.toInt()
+            v?.toInt() ?: default
         } catch (e: NumberFormatException) {
-            def
+            default
         }
     }
 
@@ -412,7 +412,7 @@
      */
     @Throws(SettingNotFoundException::class)
     fun getInt(name: String): Int {
-        val v = getString(name)
+        val v = getString(name) ?: throw SettingNotFoundException(name)
         return try {
             v.toInt()
         } catch (e: NumberFormatException) {
@@ -441,11 +441,11 @@
      * boolean.
      *
      * @param name The name of the setting to retrieve.
-     * @param def Value to return if the setting is not defined.
-     * @return The setting's current value, or 'def' if it is not defined or not a valid boolean.
+     * @param default Value to return if the setting is not defined.
+     * @return The setting's current value, or default if it is not defined or not a valid boolean.
      */
-    fun getBool(name: String, def: Boolean): Boolean {
-        return getInt(name, if (def) 1 else 0) != 0
+    fun getBool(name: String, default: Boolean): Boolean {
+        return getInt(name, if (default) 1 else 0) != 0
     }
 
     /**
@@ -579,13 +579,12 @@
     companion object {
         /** Convert a string to a long, or uses a default if the string is malformed or null */
         @JvmStatic
-        fun parseLongOrUseDefault(valString: String, def: Long): Long {
-            val value: Long
-            value =
+        fun parseLongOrUseDefault(valString: String?, default: Long): Long {
+            val value: Long =
                 try {
-                    valString.toLong()
+                    valString?.toLong() ?: default
                 } catch (e: NumberFormatException) {
-                    def
+                    default
                 }
             return value
         }
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
index 848a6e6..ac7c1ce 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
@@ -354,12 +354,12 @@
      * @param name to look up in the table
      * @return the corresponding value, or null if not present
      */
-    override fun getString(name: String): String {
+    override fun getString(name: String): String? {
         return getStringForUser(name, userId)
     }
 
     /** See [getString]. */
-    fun getStringForUser(name: String, userHandle: Int): String
+    fun getStringForUser(name: String, userHandle: Int): String?
 
     /**
      * Store a name/value pair into the database. Values written by this method will be overridden
@@ -388,17 +388,17 @@
         overrideableByRestore: Boolean
     ): Boolean
 
-    override fun getInt(name: String, def: Int): Int {
-        return getIntForUser(name, def, userId)
+    override fun getInt(name: String, default: Int): Int {
+        return getIntForUser(name, default, userId)
     }
 
     /** Similar implementation to [getInt] for the specified [userHandle]. */
-    fun getIntForUser(name: String, def: Int, userHandle: Int): Int {
+    fun getIntForUser(name: String, default: Int, userHandle: Int): Int {
         val v = getStringForUser(name, userHandle)
         return try {
-            v.toInt()
+            v?.toInt() ?: default
         } catch (e: NumberFormatException) {
-            def
+            default
         }
     }
 
@@ -408,7 +408,7 @@
     /** Similar implementation to [getInt] for the specified [userHandle]. */
     @Throws(SettingNotFoundException::class)
     fun getIntForUser(name: String, userHandle: Int): Int {
-        val v = getStringForUser(name, userHandle)
+        val v = getStringForUser(name, userHandle) ?: throw SettingNotFoundException(name)
         return try {
             v.toInt()
         } catch (e: NumberFormatException) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index efaca7a..5d8b6f1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -19,8 +19,8 @@
 import android.content.ContentResolver
 import android.content.Context
 import android.media.AudioManager
-import com.android.settingslib.bluetooth.BluetoothUtils
 import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.flags.Flags
 import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
 import com.android.settingslib.volume.data.repository.AudioRepository
 import com.android.settingslib.volume.data.repository.AudioRepositoryImpl
@@ -80,7 +80,7 @@
             @Application coroutineScope: CoroutineScope,
             @Background coroutineContext: CoroutineContext,
         ): AudioSharingRepository =
-            if (BluetoothUtils.isAudioSharingEnabled() && localBluetoothManager != null) {
+            if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
                 AudioSharingRepositoryImpl(
                     contentResolver,
                     localBluetoothManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
index cbd535b..530ae15 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
@@ -25,6 +25,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -36,7 +37,10 @@
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
+import android.hardware.display.DisplayManager;
 import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.Display;
@@ -54,6 +58,7 @@
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.res.R;
 
@@ -61,6 +66,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -76,6 +82,12 @@
     private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER;
     private static final long ANIMATION_TIMEOUT_MS =
             5L * ANIMATION_DURATION_MS * HW_TIMEOUT_MULTIPLIER;
+
+    private static final String UNIQUE_DISPLAY_ID_PRIMARY = "000";
+    private static final String UNIQUE_DISPLAY_ID_SECONDARY = "111";
+    private static final int CORNER_RADIUS_PRIMARY = 10;
+    private static final int CORNER_RADIUS_SECONDARY = 20;
+
     private FullscreenMagnificationController mFullscreenMagnificationController;
     private SurfaceControlViewHost mSurfaceControlViewHost;
     private ValueAnimator mShowHideBorderAnimator;
@@ -83,10 +95,35 @@
     private TestableWindowManager mWindowManager;
     @Mock
     private IWindowManager mIWindowManager;
+    @Mock
+    private DisplayManager mDisplayManager;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mContext = spy(mContext);
+        Display display = mock(Display.class);
+        when(display.getUniqueId()).thenReturn(UNIQUE_DISPLAY_ID_PRIMARY);
+        when(mContext.getDisplayNoVerify()).thenReturn(display);
+
+        // Override the resources to Display Primary
+        mContext.getOrCreateTestableResources()
+                .addOverride(
+                        com.android.internal.R.dimen.rounded_corner_radius,
+                        CORNER_RADIUS_PRIMARY);
+        mContext.getOrCreateTestableResources()
+                .addOverride(com.android.internal.R.dimen.rounded_corner_radius_adjustment, 0);
+        mContext.getOrCreateTestableResources()
+                .addOverride(com.android.internal.R.dimen.rounded_corner_radius_top, 0);
+        mContext.getOrCreateTestableResources()
+                .addOverride(
+                        com.android.internal.R.dimen.rounded_corner_radius_top_adjustment, 0);
+        mContext.getOrCreateTestableResources()
+                .addOverride(com.android.internal.R.dimen.rounded_corner_radius_bottom, 0);
+        mContext.getOrCreateTestableResources()
+                .addOverride(
+                        com.android.internal.R.dimen.rounded_corner_radius_bottom_adjustment, 0);
+
         getInstrumentation().runOnMainSync(() -> mSurfaceControlViewHost =
                 spy(new SurfaceControlViewHost(mContext, mContext.getDisplay(),
                         new InputTransferToken(), "FullscreenMagnification")));
@@ -101,6 +138,7 @@
                 mContext,
                 mContext.getMainThreadHandler(),
                 mContext.getMainExecutor(),
+                mDisplayManager,
                 mContext.getSystemService(AccessibilityManager.class),
                 mContext.getSystemService(WindowManager.class),
                 mIWindowManager,
@@ -259,6 +297,87 @@
         verify(mSurfaceControlViewHost).relayout(newWidth, newHeight);
     }
 
+    @EnableFlags(Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED)
+    @Test
+    public void enableFullscreenMagnification_applyPrimaryCornerRadius()
+            throws InterruptedException {
+        CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
+        CountDownLatch animationEndLatch = new CountDownLatch(1);
+        mTransaction.addTransactionCommittedListener(
+                Runnable::run, transactionCommittedLatch::countDown);
+        mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                animationEndLatch.countDown();
+            }
+        });
+
+        getInstrumentation().runOnMainSync(() ->
+                //Enable fullscreen magnification
+                mFullscreenMagnificationController
+                        .onFullscreenMagnificationActivationChanged(true));
+        assertWithMessage("Failed to wait for transaction committed")
+                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
+                .isTrue();
+        assertWithMessage("Failed to wait for animation to be finished")
+                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+                .isTrue();
+
+        // Verify the initial corner radius is applied
+        GradientDrawable backgroundDrawable =
+                (GradientDrawable) mSurfaceControlViewHost.getView().getBackground();
+        assertThat(backgroundDrawable.getCornerRadius()).isEqualTo(CORNER_RADIUS_PRIMARY);
+    }
+
+    @EnableFlags(Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED)
+    @Test
+    public void onDisplayChanged_updateCornerRadiusToSecondary() throws InterruptedException {
+        CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
+        CountDownLatch animationEndLatch = new CountDownLatch(1);
+        mTransaction.addTransactionCommittedListener(
+                Runnable::run, transactionCommittedLatch::countDown);
+        mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                animationEndLatch.countDown();
+            }
+        });
+
+        getInstrumentation().runOnMainSync(() ->
+                //Enable fullscreen magnification
+                mFullscreenMagnificationController
+                        .onFullscreenMagnificationActivationChanged(true));
+        assertWithMessage("Failed to wait for transaction committed")
+                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
+                .isTrue();
+        assertWithMessage("Failed to wait for animation to be finished")
+                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
+                .isTrue();
+
+        ArgumentCaptor<DisplayManager.DisplayListener> displayListenerCaptor =
+                ArgumentCaptor.forClass(DisplayManager.DisplayListener.class);
+        verify(mDisplayManager).registerDisplayListener(displayListenerCaptor.capture(), any());
+
+        Display newDisplay = mock(Display.class);
+        when(newDisplay.getUniqueId()).thenReturn(UNIQUE_DISPLAY_ID_SECONDARY);
+        when(mContext.getDisplayNoVerify()).thenReturn(newDisplay);
+        // Override the resources to Display Secondary
+        mContext.getOrCreateTestableResources()
+                .removeOverride(com.android.internal.R.dimen.rounded_corner_radius);
+        mContext.getOrCreateTestableResources()
+                .addOverride(
+                        com.android.internal.R.dimen.rounded_corner_radius,
+                        CORNER_RADIUS_SECONDARY);
+        getInstrumentation().runOnMainSync(() ->
+                displayListenerCaptor.getValue().onDisplayChanged(Display.DEFAULT_DISPLAY));
+        waitForIdleSync();
+        // Verify the corner radius is updated
+        GradientDrawable backgroundDrawable2 =
+                (GradientDrawable) mSurfaceControlViewHost.getView().getBackground();
+        assertThat(backgroundDrawable2.getCornerRadius()).isEqualTo(CORNER_RADIUS_SECONDARY);
+    }
+
+
     private ValueAnimator newNullTargetObjectAnimator() {
         final ValueAnimator animator =
                 ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegateTest.kt
new file mode 100644
index 0000000..b80836d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegateTest.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.accessibility.extradim
+
+import android.content.DialogInterface
+import android.testing.TestableLooper
+import android.view.accessibility.AccessibilityManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.accessibility.AccessibilityShortcutController
+import com.android.internal.accessibility.common.ShortcutConstants
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.model.SysUiState
+import com.android.systemui.res.R
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+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` as whenever
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.verify
+
+/** Tests for [ExtraDimDialogDelegate]. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class ExtraDimDialogDelegateTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    private lateinit var extraDimDialogDelegate: ExtraDimDialogDelegate
+
+    private val kosmos = Kosmos().also { it.testCase = this }
+    private val testScope = kosmos.testScope
+
+    @Mock private lateinit var dialog: SystemUIDialog
+    @Mock private lateinit var accessibilityManager: AccessibilityManager
+    @Mock private lateinit var dialogFactory: SystemUIDialog.Factory
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var sysuiState: SysUiState
+
+    @Before
+    fun setUp() {
+        whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
+        whenever(dialog.context).thenReturn(context)
+
+        extraDimDialogDelegate =
+            ExtraDimDialogDelegate(
+                context,
+                testScope.backgroundScope,
+                kosmos.testDispatcher,
+                dialogFactory,
+                accessibilityManager,
+                userTracker
+            )
+    }
+
+    @Test
+    fun clickButton_removeExtraDimShortcuts() =
+        kosmos.testScope.runTest {
+            extraDimDialogDelegate.beforeCreate(dialog, /* savedInstanceState= */ null)
+
+            val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+
+            // Verify the button has the right text
+            verify(dialog)
+                .setPositiveButton(
+                    eq(R.string.accessibility_deprecate_extra_dim_dialog_button),
+                    clickListener.capture()
+                )
+
+            clickListener.firstValue.onClick(dialog, 0)
+            advanceUntilIdle()
+            runCurrent()
+            verify(accessibilityManager)
+                .enableShortcutsForTargets(
+                    eq(false),
+                    eq(ShortcutConstants.UserShortcutType.ALL),
+                    eq(
+                        setOf(
+                            AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME
+                                .flattenToString()
+                        )
+                    ),
+                    anyInt()
+                )
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt
new file mode 100644
index 0000000..1386092
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.accessibility.extradim
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.ActivityStarter
+import javax.inject.Provider
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.verify
+
+/** Tests for [ExtraDimDialogManager]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ExtraDimDialogManagerTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    private lateinit var extraDimDialogManager: ExtraDimDialogManager
+
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var dialogProvider: Provider<ExtraDimDialogDelegate>
+
+    @Before
+    fun setUp() {
+        extraDimDialogManager = ExtraDimDialogManager(dialogProvider, activityStarter)
+    }
+
+    @Test
+    fun dismissKeyguardIfNeededAndShowDialog_executeRunnableDismissingKeyguard() {
+        extraDimDialogManager.dismissKeyguardIfNeededAndShowDialog()
+        verify(activityStarter)
+            .executeRunnableDismissingKeyguard(
+                any(),
+                /* cancelAction= */ eq(null),
+                /* dismissShade= */ eq(false),
+                /* afterKeyguardGone= */ eq(true),
+                /* deferred= */ eq(false)
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiverTest.kt
new file mode 100644
index 0000000..ebe7500
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiverTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.accessibility.extradim
+
+import android.content.Intent
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.server.display.feature.flags.Flags
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+/** Tests for [ExtraDimDialogReceiver]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ExtraDimDialogReceiverTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    private lateinit var extraDimDialogReceiver: ExtraDimDialogReceiver
+
+    @Mock private lateinit var extraDimDialogManager: ExtraDimDialogManager
+
+    @Before
+    fun setUp() {
+        extraDimDialogReceiver = ExtraDimDialogReceiver(extraDimDialogManager)
+        mContext
+            .getOrCreateTestableResources()
+            .addOverride(com.android.internal.R.bool.config_evenDimmerEnabled, true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_EVEN_DIMMER)
+    fun receiveAction_flagEvenDimmerEnabled_showDialog() {
+        extraDimDialogReceiver.onReceive(mContext, Intent(ExtraDimDialogReceiver.ACTION))
+        verify(extraDimDialogManager).dismissKeyguardIfNeededAndShowDialog()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_EVEN_DIMMER)
+    fun receiveAction_flagEvenDimmerDisabled_neverShowDialog() {
+        extraDimDialogReceiver.onReceive(mContext, Intent(ExtraDimDialogReceiver.ACTION))
+        verify(extraDimDialogManager, never()).dismissKeyguardIfNeededAndShowDialog()
+    }
+}
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 5ea5c21..d3b7d22 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
@@ -52,6 +52,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.settingslib.bluetooth.BluetoothEventManager;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
@@ -123,6 +124,8 @@
     @Mock
     private AudioManager mAudioManager;
     @Mock
+    private UiEventLogger mUiEventLogger;
+    @Mock
     private CachedBluetoothDevice mCachedDevice;
     @Mock
     private BluetoothDevice mDevice;
@@ -179,6 +182,7 @@
                 anyInt(), any());
         assertThat(intentCaptor.getValue().getAction()).isEqualTo(
                 Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
+        verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR);
     }
 
     @Test
@@ -192,7 +196,7 @@
                 anyInt(), any());
         assertThat(intentCaptor.getValue().getAction()).isEqualTo(
                 HearingDevicesDialogDelegate.ACTION_BLUETOOTH_DEVICE_DETAILS);
-
+        verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK);
     }
 
     @Test
@@ -200,9 +204,10 @@
         setUpDeviceListDialog();
         when(mHearingDeviceItem.getType()).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE);
 
-        mDialogDelegate.onDeviceItemOnClicked(mHearingDeviceItem, new View(mContext));
+        mDialogDelegate.onDeviceItemClicked(mHearingDeviceItem, new View(mContext));
 
         verify(mCachedDevice).disconnect();
+        verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_DISCONNECT);
     }
 
     @Test
@@ -304,7 +309,8 @@
                 mDialogTransitionAnimator,
                 mLocalBluetoothManager,
                 new Handler(mTestableLooper.getLooper()),
-                mAudioManager
+                mAudioManager,
+                mUiEventLogger
         );
 
         mDialog = mDialogDelegate.createDialog();
@@ -326,7 +332,8 @@
                 mDialogTransitionAnimator,
                 mLocalBluetoothManager,
                 new Handler(mTestableLooper.getLooper()),
-                mAudioManager
+                mAudioManager,
+                mUiEventLogger
         );
 
         mDialog = mDialogDelegate.createDialog();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 9df653f..e603db4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -491,7 +491,7 @@
                         assertThat(iconAsset)
                             .isEqualTo(R.raw.fingerprint_dialogue_error_to_unlock_lottie)
                         assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation)
+                            .isEqualTo(R.string.biometric_dialog_confirm)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
 
                         // Confirm authentication
@@ -601,7 +601,7 @@
                             .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie)
                         assertThat(iconOverlayAsset).isEqualTo(-1)
                         assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation)
+                            .isEqualTo(R.string.biometric_dialog_confirm)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                         assertThat(shouldAnimateIconOverlay).isEqualTo(false)
                     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
index aa5edae..4818119 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
@@ -42,13 +42,21 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Intent;
+import android.hardware.display.DisplayManager;
 import android.os.PowerManager;
 import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
+import android.view.Display;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.display.BrightnessSynchronizer;
+import com.android.server.display.feature.flags.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -62,6 +70,7 @@
 import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -74,10 +83,15 @@
 @RunWith(AndroidJUnit4.class)
 public class DozeScreenBrightnessTest extends SysuiTestCase {
 
-    private static final int DEFAULT_BRIGHTNESS = 10;
-    private static final int DIM_BRIGHTNESS = 1;
-    private static final int[] SENSOR_TO_BRIGHTNESS = new int[]{-1, 1, 2, 3, 4};
+    private static final int DEFAULT_BRIGHTNESS_INT = 10;
+    private static final float DEFAULT_BRIGHTNESS_FLOAT = 0.1f;
+    private static final int DIM_BRIGHTNESS_INT = 1;
+    private static final float DIM_BRIGHTNESS_FLOAT = 0.05f;
+    private static final int[] SENSOR_TO_BRIGHTNESS_INT = new int[]{-1, 1, 2, 3, 4};
+    private static final float[] SENSOR_TO_BRIGHTNESS_FLOAT =
+            new float[]{-1, 0.01f, 0.05f, 0.7f, 0.1f};
     private static final int[] SENSOR_TO_OPACITY = new int[]{-1, 10, 0, 0, 0};
+    private static final float DELTA = BrightnessSynchronizer.EPSILON;
 
     private DozeServiceFake mServiceFake;
     private FakeSensorManager.FakeGenericSensor mSensor;
@@ -98,16 +112,23 @@
     DozeLog mDozeLog;
     @Mock
     SystemSettings mSystemSettings;
+    @Mock
+    DisplayManager mDisplayManager;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private FakeThreadFactory mFakeThreadFactory = new FakeThreadFactory(mFakeExecutor);
 
     private DozeScreenBrightness mScreen;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
-                eq(UserHandle.USER_CURRENT))).thenReturn(DEFAULT_BRIGHTNESS);
+                eq(UserHandle.USER_CURRENT))).thenReturn(PowerManager.BRIGHTNESS_ON);
+        when(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY))
+                .thenReturn(PowerManager.BRIGHTNESS_MAX);
         doAnswer(invocation -> {
             ((Runnable) invocation.getArgument(0)).run();
             return null;
@@ -117,9 +138,14 @@
         mSensorManager = new AsyncSensorManager(fakeSensorManager, mFakeThreadFactory, null);
 
         mAlwaysOnDisplayPolicy = new AlwaysOnDisplayPolicy(mContext);
-        mAlwaysOnDisplayPolicy.defaultDozeBrightness = DEFAULT_BRIGHTNESS;
-        mAlwaysOnDisplayPolicy.screenBrightnessArray = SENSOR_TO_BRIGHTNESS;
-        mAlwaysOnDisplayPolicy.dimBrightness = DIM_BRIGHTNESS;
+        mAlwaysOnDisplayPolicy.defaultDozeBrightness = DEFAULT_BRIGHTNESS_INT;
+        when(mDisplayManager.getDefaultDozeBrightness(Display.DEFAULT_DISPLAY))
+                .thenReturn(DEFAULT_BRIGHTNESS_FLOAT);
+        mAlwaysOnDisplayPolicy.screenBrightnessArray = SENSOR_TO_BRIGHTNESS_INT;
+        when(mDisplayManager.getDozeBrightnessSensorValueToBrightness(Display.DEFAULT_DISPLAY))
+                .thenReturn(SENSOR_TO_BRIGHTNESS_FLOAT);
+        mAlwaysOnDisplayPolicy.dimBrightness = DIM_BRIGHTNESS_INT;
+        mAlwaysOnDisplayPolicy.dimBrightnessFloat = DIM_BRIGHTNESS_FLOAT;
         mAlwaysOnDisplayPolicy.dimmingScrimArray = SENSOR_TO_OPACITY;
         mSensor = fakeSensorManager.getFakeLightSensor();
         mSensorInner = fakeSensorManager.getFakeLightSensor2();
@@ -135,19 +161,35 @@
                 mDozeParameters,
                 mDevicePostureController,
                 mDozeLog,
-                mSystemSettings);
+                mSystemSettings,
+                mDisplayManager);
     }
 
     @Test
-    public void testInitialize_setsScreenBrightnessToValidValue() throws Exception {
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testInitialize_setsScreenBrightnessToValidValue_Int() {
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
 
-        assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness);
-        assertTrue(mServiceFake.screenBrightness <= PowerManager.BRIGHTNESS_ON);
+        assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt);
+        assertTrue(mServiceFake.screenBrightnessInt >= PowerManager.BRIGHTNESS_OFF + 1);
+        assertTrue(mServiceFake.screenBrightnessInt <= PowerManager.BRIGHTNESS_ON);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void testAod_usesDebugValue() throws Exception {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testInitialize_setsScreenBrightnessToValidValue_Float() {
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+
+        assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA);
+        assertTrue(mServiceFake.screenBrightnessFloat >= PowerManager.BRIGHTNESS_MIN);
+        assertTrue(mServiceFake.screenBrightnessFloat <= PowerManager.BRIGHTNESS_MAX);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testAod_usesDebugValue_Int() {
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
         waitForSensorManager();
@@ -157,11 +199,29 @@
         mScreen.onReceive(mContext, intent);
         mSensor.sendSensorEvent(3);
 
-        assertEquals(1, mServiceFake.screenBrightness);
+        assertEquals(SENSOR_TO_BRIGHTNESS_INT[1], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void testAod_usesLightSensorRespectingUserSetting() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testAod_usesDebugValue_Float() {
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        Intent intent = new Intent(DozeScreenBrightness.ACTION_AOD_BRIGHTNESS);
+        intent.putExtra(DozeScreenBrightness.BRIGHTNESS_BUCKET, 1);
+        mScreen.onReceive(mContext, intent);
+        mSensor.sendSensorEvent(3);
+
+        assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[1], mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testAod_usesLightSensorRespectingUserSetting_Int() {
         int maxBrightness = 3;
         when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
                 eq(UserHandle.USER_CURRENT))).thenReturn(maxBrightness);
@@ -170,11 +230,27 @@
                 .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
-        assertEquals(maxBrightness, mServiceFake.screenBrightness);
+        assertEquals(maxBrightness, mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void testAod_usesLightSensorNotClampingToAutoBrightnessValue() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testAod_usesLightSensorRespectingUserSetting_Float() {
+        float maxBrightness = DEFAULT_BRIGHTNESS_FLOAT / 2;
+        when(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY)).thenReturn(maxBrightness);
+        when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS_MODE), anyInt(),
+                eq(UserHandle.USER_CURRENT)))
+                .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
+
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        assertEquals(maxBrightness, mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testAod_usesLightSensorNotClampingToAutoBrightnessValue_Int() {
         int maxBrightness = 3;
         when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
                 eq(UserHandle.USER_CURRENT))).thenReturn(maxBrightness);
@@ -183,11 +259,27 @@
                 .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
-        assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness);
+        assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void doze_doesNotUseLightSensor() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testAod_usesLightSensorNotClampingToAutoBrightnessValue_Float() {
+        float maxBrightness = DEFAULT_BRIGHTNESS_FLOAT / 2;
+        when(mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY)).thenReturn(maxBrightness);
+        when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS_MODE), anyInt(),
+                eq(UserHandle.USER_CURRENT)))
+                .thenReturn(Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void doze_doesNotUseLightSensor_Int() {
         // GIVEN the device is DOZE and the display state changes to ON
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
@@ -197,12 +289,31 @@
         mSensor.sendSensorEvent(3);
 
         // THEN brightness is NOT changed, it's set to the default brightness
-        assertNotSame(3, mServiceFake.screenBrightness);
-        assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness);
+        assertNotSame(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt);
+        assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void dozeSuspendTriggers_doesNotUseLightSensor() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void doze_doesNotUseLightSensor_Float() {
+        // GIVEN the device is DOZE and the display state changes to ON
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE);
+        waitForSensorManager();
+
+        // WHEN new sensor event sent
+        mSensor.sendSensorEvent(3);
+
+        // THEN brightness is NOT changed, it's set to the default brightness
+        assertNotSame(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessInt);
+        assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void dozeSuspendTriggers_doesNotUseLightSensor_Int() {
         // GIVEN the device is DOZE and the display state changes to ON
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
@@ -212,12 +323,31 @@
         mSensor.sendSensorEvent(3);
 
         // THEN brightness is NOT changed, it's set to the default brightness
-        assertNotSame(3, mServiceFake.screenBrightness);
-        assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness);
+        assertNotSame(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt);
+        assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void aod_usesLightSensor() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void dozeSuspendTriggers_doesNotUseLightSensor_Float() {
+        // GIVEN the device is DOZE and the display state changes to ON
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
+        waitForSensorManager();
+
+        // WHEN new sensor event sent
+        mSensor.sendSensorEvent(3);
+
+        // THEN brightness is NOT changed, it's set to the default brightness
+        assertNotSame(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat);
+        assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void aod_usesLightSensor_Int() {
         // GIVEN the device is DOZE_AOD and the display state changes to ON
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
@@ -227,11 +357,29 @@
         mSensor.sendSensorEvent(3);
 
         // THEN brightness is updated
-        assertEquals(3, mServiceFake.screenBrightness);
+        assertEquals(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void docked_usesLightSensor() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void aod_usesLightSensor_Float() {
+        // GIVEN the device is DOZE_AOD and the display state changes to ON
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN new sensor event sent
+        mSensor.sendSensorEvent(3);
+
+        // THEN brightness is updated
+        assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void docked_usesLightSensor_Int() {
         // GIVEN the device is docked and the display state changes to ON
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
@@ -242,11 +390,29 @@
         mSensor.sendSensorEvent(3);
 
         // THEN brightness is updated
-        assertEquals(3, mServiceFake.screenBrightness);
+        assertEquals(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void testPulsing_withoutLightSensor_setsAoDDimmingScrimTransparent() throws Exception {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void docked_usesLightSensor_Float() {
+        // GIVEN the device is docked and the display state changes to ON
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        mScreen.transitionTo(DOZE_AOD, DOZE_AOD_DOCKED);
+        waitForSensorManager();
+
+        // WHEN new sensor event sent
+        mSensor.sendSensorEvent(3);
+
+        // THEN brightness is updated
+        assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    public void testPulsing_withoutLightSensor_setsAoDDimmingScrimTransparent() {
         mScreen = new DozeScreenBrightness(
                 mContext,
                 mServiceFake,
@@ -258,7 +424,8 @@
                 mDozeParameters,
                 mDevicePostureController,
                 mDozeLog,
-                mSystemSettings);
+                mSystemSettings,
+                mDisplayManager);
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
         reset(mDozeHost);
@@ -269,7 +436,8 @@
     }
 
     @Test
-    public void testScreenOffAfterPulsing_pausesLightSensor() throws Exception {
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testScreenOffAfterPulsing_pausesLightSensor_Int() {
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
         mScreen.transitionTo(DOZE, DOZE_REQUEST_PULSE);
@@ -280,11 +448,29 @@
 
         mSensor.sendSensorEvent(1);
 
-        assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness);
+        assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void testNullSensor() throws Exception {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testScreenOffAfterPulsing_pausesLightSensor_Float() {
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE);
+        mScreen.transitionTo(DOZE, DOZE_REQUEST_PULSE);
+        mScreen.transitionTo(DOZE_REQUEST_PULSE, DOZE_PULSING);
+        mScreen.transitionTo(DOZE_PULSING, DOZE_PULSE_DONE);
+        mScreen.transitionTo(DOZE_PULSE_DONE, DOZE);
+        waitForSensorManager();
+
+        mSensor.sendSensorEvent(1);
+
+        assertEquals(DEFAULT_BRIGHTNESS_FLOAT, mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    public void testNullSensor() {
         mScreen = new DozeScreenBrightness(
                 mContext,
                 mServiceFake,
@@ -296,7 +482,8 @@
                 mDozeParameters,
                 mDevicePostureController,
                 mDozeLog,
-                mSystemSettings);
+                mSystemSettings,
+                mDisplayManager);
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
@@ -305,7 +492,8 @@
     }
 
     @Test
-    public void testSensorsSupportPostures_closed() throws Exception {
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testSensorsSupportPostures_closed_Int() {
         // GIVEN the device is CLOSED
         when(mDevicePostureController.getDevicePosture()).thenReturn(
                 DevicePostureController.DEVICE_POSTURE_CLOSED);
@@ -328,7 +516,8 @@
                 mDozeParameters,
                 mDevicePostureController,
                 mDozeLog,
-                mSystemSettings);
+                mSystemSettings,
+                mDisplayManager);
 
         // GIVEN the device is in AOD
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
@@ -340,11 +529,56 @@
         mSensorInner.sendSensorEvent(4); // OPENED sensor
 
         // THEN brightness is updated according to the sensor for CLOSED
-        assertEquals(3, mServiceFake.screenBrightness);
+        assertEquals(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void testSensorsSupportPostures_open() throws Exception {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testSensorsSupportPostures_closed_Float() {
+        // GIVEN the device is CLOSED
+        when(mDevicePostureController.getDevicePosture()).thenReturn(
+                DevicePostureController.DEVICE_POSTURE_CLOSED);
+
+        // GIVEN closed and opened postures use different light sensors
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[]{
+                        Optional.empty() /* unknown */,
+                        Optional.of(mSensor.getSensor()) /* closed */,
+                        Optional.of(mSensorInner.getSensor()) /* half-opened */,
+                        Optional.of(mSensorInner.getSensor()) /* opened */,
+                        Optional.empty() /* flipped */
+                },
+                mDozeHost, null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog,
+                mSystemSettings,
+                mDisplayManager);
+
+        // GIVEN the device is in AOD
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN new different events are sent from the inner and outer sensors
+        mSensor.sendSensorEvent(3); // CLOSED sensor
+        mSensorInner.sendSensorEvent(4); // OPENED sensor
+
+        // THEN brightness is updated according to the sensor for CLOSED
+        assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat,
+                DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testSensorsSupportPostures_open_Int() {
         // GIVEN the device is OPENED
         when(mDevicePostureController.getDevicePosture()).thenReturn(
                 DevicePostureController.DEVICE_POSTURE_OPENED);
@@ -367,7 +601,8 @@
                 mDozeParameters,
                 mDevicePostureController,
                 mDozeLog,
-                mSystemSettings);
+                mSystemSettings,
+                mDisplayManager);
 
         // GIVEN device is in AOD
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
@@ -379,11 +614,55 @@
         mSensor.sendSensorEvent(3); // CLOSED sensor
 
         // THEN brightness is updated according to the sensor for OPENED
-        assertEquals(4, mServiceFake.screenBrightness);
+        assertEquals(SENSOR_TO_BRIGHTNESS_INT[4], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void testSensorsSupportPostures_swapPostures() throws Exception {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testSensorsSupportPostures_open_Float() {
+        // GIVEN the device is OPENED
+        when(mDevicePostureController.getDevicePosture()).thenReturn(
+                DevicePostureController.DEVICE_POSTURE_OPENED);
+
+        // GIVEN closed and opened postures use different light sensors
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[]{
+                        Optional.empty() /* unknown */,
+                        Optional.of(mSensor.getSensor()) /* closed */,
+                        Optional.of(mSensorInner.getSensor()) /* half-opened */,
+                        Optional.of(mSensorInner.getSensor()) /* opened */,
+                        Optional.empty() /* flipped */
+                },
+                mDozeHost, null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog,
+                mSystemSettings,
+                mDisplayManager);
+
+        // GIVEN device is in AOD
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN new different events are sent from the inner and outer sensors
+        mSensorInner.sendSensorEvent(4); // OPENED sensor
+        mSensor.sendSensorEvent(3); // CLOSED sensor
+
+        // THEN brightness is updated according to the sensor for OPENED
+        assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[4], mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testSensorsSupportPostures_swapPostures_Int() {
         ArgumentCaptor<DevicePostureController.Callback> postureCallbackCaptor =
                 ArgumentCaptor.forClass(DevicePostureController.Callback.class);
         reset(mDevicePostureController);
@@ -410,7 +689,8 @@
                 mDozeParameters,
                 mDevicePostureController,
                 mDozeLog,
-                mSystemSettings);
+                mSystemSettings,
+                mDisplayManager);
         verify(mDevicePostureController).addCallback(postureCallbackCaptor.capture());
 
         // GIVEN device is in AOD
@@ -428,11 +708,65 @@
         mSensorInner.sendSensorEvent(4); // OPENED sensor
 
         // THEN brightness is updated according to the sensor for CLOSED
-        assertEquals(3, mServiceFake.screenBrightness);
+        assertEquals(SENSOR_TO_BRIGHTNESS_INT[3], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void testNoBrightnessDeliveredAfterFinish() throws Exception {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testSensorsSupportPostures_swapPostures_Float() {
+        ArgumentCaptor<DevicePostureController.Callback> postureCallbackCaptor =
+                ArgumentCaptor.forClass(DevicePostureController.Callback.class);
+        reset(mDevicePostureController);
+
+        // GIVEN the device starts up AOD OPENED
+        when(mDevicePostureController.getDevicePosture()).thenReturn(
+                DevicePostureController.DEVICE_POSTURE_OPENED);
+
+        // GIVEN closed and opened postures use different light sensors
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[]{
+                        Optional.empty() /* unknown */,
+                        Optional.of(mSensor.getSensor()) /* closed */,
+                        Optional.of(mSensorInner.getSensor()) /* half-opened */,
+                        Optional.of(mSensorInner.getSensor()) /* opened */,
+                        Optional.empty() /* flipped */
+                },
+                mDozeHost, null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog,
+                mSystemSettings,
+                mDisplayManager);
+        verify(mDevicePostureController).addCallback(postureCallbackCaptor.capture());
+
+        // GIVEN device is in AOD
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN the posture changes to CLOSED
+        postureCallbackCaptor.getValue().onPostureChanged(
+                DevicePostureController.DEVICE_POSTURE_CLOSED);
+        waitForSensorManager();
+
+        // WHEN new different events are sent from the inner and outer sensors
+        mSensor.sendSensorEvent(3); // CLOSED sensor
+        mSensorInner.sendSensorEvent(4); // OPENED sensor
+
+        // THEN brightness is updated according to the sensor for CLOSED
+        assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[3], mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testNoBrightnessDeliveredAfterFinish_Int() {
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
         mScreen.transitionTo(DOZE_AOD, FINISH);
@@ -440,11 +774,27 @@
 
         mSensor.sendSensorEvent(1);
 
-        assertNotEquals(1, mServiceFake.screenBrightness);
+        assertNotEquals(SENSOR_TO_BRIGHTNESS_INT[1], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void testNonPositiveBrightness_keepsPreviousBrightnessAndScrim() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testNoBrightnessDeliveredAfterFinish_Float() {
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        mScreen.transitionTo(DOZE_AOD, FINISH);
+        waitForSensorManager();
+
+        mSensor.sendSensorEvent(1);
+
+        assertNotEquals(SENSOR_TO_BRIGHTNESS_FLOAT[1], mServiceFake.screenBrightnessFloat);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testNonPositiveBrightness_keepsPreviousBrightnessAndScrim_Int() {
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
         waitForSensorManager();
@@ -452,7 +802,23 @@
         mSensor.sendSensorEvent(1);
         mSensor.sendSensorEvent(0);
 
-        assertEquals(1, mServiceFake.screenBrightness);
+        assertEquals(SENSOR_TO_BRIGHTNESS_INT[1], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
+        verify(mDozeHost).setAodDimmingScrim(eq(10f / 255f));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void testNonPositiveBrightness_keepsPreviousBrightnessAndScrim_Float() {
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        waitForSensorManager();
+
+        mSensor.sendSensorEvent(1);
+        mSensor.sendSensorEvent(0);
+
+        assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[1], mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
         verify(mDozeHost).setAodDimmingScrim(eq(10f / 255f));
     }
 
@@ -473,7 +839,8 @@
     }
 
     @Test
-    public void transitionToDoze_shouldClampBrightness_afterTimeout_clampsToDim() {
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_shouldClampBrightness_afterTimeout_clampsToDim_Int() {
         when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
                 PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
         when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(true);
@@ -482,15 +849,38 @@
 
         // If we're dozing after a timeout, and playing the unlocked screen animation, we should
         // stay at or below dim brightness, because the screen dims just before timeout.
-        assertTrue(mServiceFake.screenBrightness <= DIM_BRIGHTNESS);
+        assertTrue(mServiceFake.screenBrightnessInt <= DIM_BRIGHTNESS_INT);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
 
         // Once we transition to Doze, use the doze brightness
         mScreen.transitionTo(INITIALIZED, DOZE);
-        assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS);
+        assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void transitionToDoze_shouldClampBrightness_notAfterTimeout_doesNotClampToDim() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_shouldClampBrightness_afterTimeout_clampsToDim_Float() {
+        when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
+                PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
+        when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(true);
+
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+
+        // If we're dozing after a timeout, and playing the unlocked screen animation, we should
+        // stay at or below dim brightness, because the screen dims just before timeout.
+        assertTrue(mServiceFake.screenBrightnessFloat <= DIM_BRIGHTNESS_FLOAT);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+
+        // Once we transition to Doze, use the doze brightness
+        mScreen.transitionTo(INITIALIZED, DOZE);
+        assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_shouldClampBrightness_notAfterTimeout_doesNotClampToDim_Int() {
         when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
                 PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON);
         when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(true);
@@ -499,14 +889,36 @@
 
         // If we're playing the unlocked screen off animation after a power button press, we should
         // leave the brightness alone.
-        assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS);
+        assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
 
         mScreen.transitionTo(INITIALIZED, DOZE);
-        assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS);
+        assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void transitionToDoze_noClampBrightness_afterTimeout_noScreenOff_doesNotClampToDim() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_shouldClampBrightness_notAfterTimeout_doesNotClampToDim_Float() {
+        when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
+                PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON);
+        when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(true);
+
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+
+        // If we're playing the unlocked screen off animation after a power button press, we should
+        // leave the brightness alone.
+        assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+
+        mScreen.transitionTo(INITIALIZED, DOZE);
+        assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_noClamp_afterTimeout_noScreenOff_doesNotClampToDim_Int() {
         when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
                 PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
         when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(false);
@@ -515,11 +927,28 @@
         mScreen.transitionTo(INITIALIZED, DOZE);
 
         // If we aren't controlling the screen off animation, we should leave the brightness alone.
-        assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS);
+        assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void transitionToDoze_noClampBrightness_afterTimeout_clampsToDim() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_noClamp_afterTimeout_noScreenOff_doesNotClampToDim_Float() {
+        when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
+                PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
+        when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(false);
+
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE);
+
+        // If we aren't controlling the screen off animation, we should leave the brightness alone.
+        assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_noClampBrightness_afterTimeout_clampsToDim_Int() {
         when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
                 PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
         when(mWakefulnessLifecycle.getWakefulness()).thenReturn(
@@ -528,11 +957,28 @@
 
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
 
-        assertTrue(mServiceFake.screenBrightness <= DIM_BRIGHTNESS);
+        assertTrue(mServiceFake.screenBrightnessInt <= DIM_BRIGHTNESS_INT);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void transitionToDoze_noClampBrigthness_notAfterTimeout_doesNotClampToDim() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_noClampBrightness_afterTimeout_clampsToDim_Float() {
+        when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
+                PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
+        when(mWakefulnessLifecycle.getWakefulness()).thenReturn(
+                WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP);
+        when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(false);
+
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+
+        assertTrue(mServiceFake.screenBrightnessFloat <= DIM_BRIGHTNESS_FLOAT);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_noClampBrigthness_notAfterTimeout_doesNotClampToDim_Int() {
         when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
                 PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON);
         when(mWakefulnessLifecycle.getWakefulness()).thenReturn(
@@ -542,11 +988,29 @@
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE);
 
-        assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS);
+        assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void transitionToAodPaused_lightSensorDisabled() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToDoze_noClampBrigthness_notAfterTimeout_doesNotClampToDim_Float() {
+        when(mWakefulnessLifecycle.getLastSleepReason()).thenReturn(
+                PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON);
+        when(mWakefulnessLifecycle.getWakefulness()).thenReturn(
+                WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP);
+        when(mDozeParameters.shouldClampToDimBrightness()).thenReturn(false);
+
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE);
+
+        assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToAodPaused_lightSensorDisabled_Int() {
         // GIVEN AOD
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
@@ -558,11 +1022,31 @@
 
         // THEN new light events don't update brightness since the light sensor was unregistered
         mSensor.sendSensorEvent(1);
-        assertEquals(mServiceFake.screenBrightness, DEFAULT_BRIGHTNESS);
+        assertEquals(mServiceFake.screenBrightnessInt, DEFAULT_BRIGHTNESS_INT);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     @Test
-    public void transitionFromAodPausedToAod_lightSensorEnabled() {
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionToAodPaused_lightSensorDisabled_Float() {
+        // GIVEN AOD
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+
+        // WHEN AOD is paused
+        mScreen.transitionTo(DOZE_AOD, DOZE_AOD_PAUSING);
+        mScreen.transitionTo(DOZE_AOD, DOZE_AOD_PAUSED);
+        waitForSensorManager();
+
+        // THEN new light events don't update brightness since the light sensor was unregistered
+        mSensor.sendSensorEvent(1);
+        assertEquals(mServiceFake.screenBrightnessFloat, DEFAULT_BRIGHTNESS_FLOAT, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionFromAodPausedToAod_lightSensorEnabled_Int() {
         // GIVEN AOD paused
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
         mScreen.transitionTo(INITIALIZED, DOZE_AOD);
@@ -577,7 +1061,54 @@
         mSensor.sendSensorEvent(1);
 
         // THEN aod brightness is updated
-        assertEquals(mServiceFake.screenBrightness, 1);
+        assertEquals(SENSOR_TO_BRIGHTNESS_INT[1], mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void transitionFromAodPausedToAod_lightSensorEnabled_Float() {
+        // GIVEN AOD paused
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_AOD);
+        mScreen.transitionTo(DOZE_AOD, DOZE_AOD_PAUSING);
+        mScreen.transitionTo(DOZE_AOD, DOZE_AOD_PAUSED);
+
+        // WHEN device transitions back to AOD
+        mScreen.transitionTo(DOZE_AOD_PAUSED, DOZE_AOD);
+        waitForSensorManager();
+
+        // WHEN there are brightness changes
+        mSensor.sendSensorEvent(1);
+
+        // THEN aod brightness is updated
+        assertEquals(SENSOR_TO_BRIGHTNESS_FLOAT[1], mServiceFake.screenBrightnessFloat, DELTA);
+        assertEquals(PowerManager.BRIGHTNESS_DEFAULT, mServiceFake.screenBrightnessInt);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DOZE_BRIGHTNESS_FLOAT)
+    public void fallBackToIntIfFloatBrightnessUndefined() {
+        when(mDisplayManager.getDozeBrightnessSensorValueToBrightness(Display.DEFAULT_DISPLAY))
+                .thenReturn(null);
+        mScreen = new DozeScreenBrightness(
+                mContext,
+                mServiceFake,
+                mSensorManager,
+                new Optional[]{Optional.of(mSensor.getSensor())},
+                mDozeHost,
+                null /* handler */,
+                mAlwaysOnDisplayPolicy,
+                mWakefulnessLifecycle,
+                mDozeParameters,
+                mDevicePostureController,
+                mDozeLog,
+                mSystemSettings,
+                mDisplayManager);
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+
+        assertEquals(DEFAULT_BRIGHTNESS_INT, mServiceFake.screenBrightnessInt);
+        assertTrue(Float.isNaN(mServiceFake.screenBrightnessFloat));
     }
 
     private void waitForSensorManager() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java
index 928b314..f55c2b7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java
@@ -30,7 +30,8 @@
     public int screenState;
     public boolean screenStateSet;
     public boolean requestedWakeup;
-    public int screenBrightness;
+    public int screenBrightnessInt;
+    public float screenBrightnessFloat;
 
     public DozeServiceFake() {
         reset();
@@ -54,7 +55,12 @@
 
     @Override
     public void setDozeScreenBrightness(int brightness) {
-        screenBrightness = brightness;
+        screenBrightnessInt = brightness;
+    }
+
+    @Override
+    public void setDozeScreenBrightnessFloat(float brightness) {
+        screenBrightnessFloat = brightness;
     }
 
     public void reset() {
@@ -62,6 +68,7 @@
         screenState = Display.STATE_UNKNOWN;
         screenStateSet = false;
         requestedWakeup = false;
-        screenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
+        screenBrightnessInt = PowerManager.BRIGHTNESS_DEFAULT;
+        screenBrightnessFloat = PowerManager.BRIGHTNESS_INVALID_FLOAT;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 9de7528..ff65887 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -111,6 +111,7 @@
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shade.ShadeWindowLogger;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -225,6 +226,7 @@
     private @Mock DreamViewModel mDreamViewModel;
     private @Mock CommunalTransitionViewModel mCommunalTransitionViewModel;
     private @Mock SystemPropertiesHelper mSystemPropertiesHelper;
+    @Mock private NotificationShadeWindowModel mNotificationShadeWindowModel;
 
     private FakeFeatureFlags mFeatureFlags;
     private final int mDefaultUserId = 100;
@@ -272,6 +274,7 @@
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
                 mUserTracker,
+                mNotificationShadeWindowModel,
                 mKosmos::getCommunalInteractor);
         mFeatureFlags = new FakeFeatureFlags();
         mSetFlagsRule.enableFlags(FLAG_REFACTOR_GET_CURRENT_USER);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
index e01744e..6a43a61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
@@ -20,6 +20,7 @@
 import android.content.ContextWrapper
 import android.content.SharedPreferences
 import android.os.Handler
+import android.platform.test.annotations.DisableFlags
 import android.provider.Settings
 import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS
 import android.provider.Settings.Global.ZEN_MODE_OFF
@@ -61,6 +62,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
+@DisableFlags(android.app.Flags.FLAG_MODES_UI)
 class DndTileTest : SysuiTestCase() {
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
index 74d9692..a5de7cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
@@ -47,8 +47,8 @@
 import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.After
@@ -82,9 +82,12 @@
 
     @Mock private lateinit var dialogDelegate: ModesDialogDelegate
 
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
     private val inputHandler = FakeQSTileIntentUserInputHandler()
     private val zenModeRepository = FakeZenModeRepository()
-    private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository)
+    private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository, testDispatcher)
     private val mapper =
         ModesTileMapper(
             context.orCreateTestableResources
@@ -96,9 +99,6 @@
             context.theme,
         )
 
-    private val testDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testDispatcher)
-
     private lateinit var userActionInteractor: ModesTileUserActionInteractor
     private lateinit var secureSettings: SecureSettings
     private lateinit var testableLooper: TestableLooper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
index 69cc9d5..35f2e6e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
@@ -17,11 +17,13 @@
 package com.android.systemui.shared.notifications.data.repository
 
 import android.provider.Settings
+import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.shared.settings.data.repository.FakeSecureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.FakeSystemSettingsRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
@@ -38,23 +40,26 @@
 
     private lateinit var testScope: TestScope
     private lateinit var secureSettingsRepository: FakeSecureSettingsRepository
+    private lateinit var systemSettingsRepository: FakeSystemSettingsRepository
 
     @Before
     fun setUp() {
         val testDispatcher = StandardTestDispatcher()
         testScope = TestScope(testDispatcher)
         secureSettingsRepository = FakeSecureSettingsRepository()
+        systemSettingsRepository = FakeSystemSettingsRepository()
 
         underTest =
             NotificationSettingsRepository(
-                scope = testScope.backgroundScope,
+                backgroundScope = testScope.backgroundScope,
                 backgroundDispatcher = testDispatcher,
                 secureSettingsRepository = secureSettingsRepository,
+                systemSettingsRepository = systemSettingsRepository,
             )
     }
 
     @Test
-    fun testGetIsShowNotificationsOnLockscreenEnabled() =
+    fun getIsShowNotificationsOnLockscreenEnabled() =
         testScope.runTest {
             val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled())
 
@@ -72,7 +77,7 @@
         }
 
     @Test
-    fun testSetIsShowNotificationsOnLockscreenEnabled() =
+    fun setIsShowNotificationsOnLockscreenEnabled() =
         testScope.runTest {
             val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled())
 
@@ -84,7 +89,7 @@
         }
 
     @Test
-    fun testGetIsNotificationHistoryEnabled() =
+    fun getIsNotificationHistoryEnabled() =
         testScope.runTest {
             val historyEnabled by collectLastValue(underTest.isNotificationHistoryEnabled)
 
@@ -100,4 +105,40 @@
             )
             assertThat(historyEnabled).isEqualTo(false)
         }
+
+    @Test
+    fun testGetIsCooldownEnabled() =
+        testScope.runTest {
+            val cooldownEnabled by collectLastValue(underTest.isCooldownEnabled)
+
+            systemSettingsRepository.setInt(
+                name = Settings.System.NOTIFICATION_COOLDOWN_ENABLED,
+                value = 1,
+            )
+            assertThat(cooldownEnabled).isEqualTo(true)
+
+            systemSettingsRepository.setInt(
+                name = Settings.System.NOTIFICATION_COOLDOWN_ENABLED,
+                value = 0,
+            )
+            assertThat(cooldownEnabled).isEqualTo(false)
+        }
+
+    @Test
+    fun zenDuration() =
+        testScope.runTest {
+            val zenDuration by collectLastValue(underTest.zenDuration)
+
+            secureSettingsRepository.setInt(
+                name = Settings.Secure.ZEN_DURATION,
+                value = 60,
+            )
+            assertThat(zenDuration).isEqualTo(60)
+
+            secureSettingsRepository.setInt(
+                name = Settings.Secure.ZEN_DURATION,
+                value = ZEN_DURATION_FOREVER,
+            )
+            assertThat(zenDuration).isEqualTo(ZEN_DURATION_FOREVER)
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
index 0505727..689fc7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
@@ -28,7 +28,11 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.RankingBuilder
 import com.android.systemui.statusbar.StatusBarState
@@ -45,6 +49,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
+import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -52,6 +57,7 @@
 import com.android.systemui.util.mockito.withArgCaptor
 import dagger.BindsInstance
 import dagger.Component
+import kotlinx.coroutines.CoroutineScope
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
@@ -64,6 +70,8 @@
 @RunWith(AndroidJUnit4::class)
 class SensitiveContentCoordinatorTest : SysuiTestCase() {
 
+    val kosmos = testKosmos()
+
     val dynamicPrivacyController: DynamicPrivacyController = mock()
     val lockscreenUserManager: NotificationLockscreenUserManager = mock()
     val pipeline: NotifPipeline = mock()
@@ -73,6 +81,8 @@
     val mSelectedUserInteractor: SelectedUserInteractor = mock()
     val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController =
         mock()
+    val deviceEntryInteractor: DeviceEntryInteractor = mock()
+    val sceneInteractor: SceneInteractor = mock()
 
     val coordinator: SensitiveContentCoordinator =
         DaggerTestSensitiveContentCoordinatorComponent.factory()
@@ -83,7 +93,10 @@
                 statusBarStateController,
                 keyguardStateController,
                 mSelectedUserInteractor,
-                sensitiveNotificationProtectionController
+                sensitiveNotificationProtectionController,
+                deviceEntryInteractor,
+                sceneInteractor,
+                kosmos.applicationCoroutineScope,
             )
             .coordinator
 
@@ -136,8 +149,7 @@
     @Test
     @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
     fun screenshareSecretFilter_sensitiveInctive_noFiltersSecret() {
-        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive)
-            .thenReturn(false)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(false)
 
         coordinator.attach(pipeline)
         val filter = withArgCaptor<NotifFilter> { verify(pipeline).addFinalizeFilter(capture()) }
@@ -683,10 +695,11 @@
         val mockSbn: StatusBarNotification =
             mock<StatusBarNotification>().apply { whenever(user).thenReturn(mockUserHandle) }
         val mockRow: ExpandableNotificationRow = mock<ExpandableNotificationRow>()
-        val mockEntry = mock<NotificationEntry>().apply {
-            whenever(sbn).thenReturn(mockSbn)
-            whenever(row).thenReturn(mockRow)
-        }
+        val mockEntry =
+            mock<NotificationEntry>().apply {
+                whenever(sbn).thenReturn(mockSbn)
+                whenever(row).thenReturn(mockRow)
+            }
         whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction)
         whenever(mockEntry.rowExists()).thenReturn(true)
         return object : ListEntry("key", 0) {
@@ -737,6 +750,9 @@
             @BindsInstance selectedUserInteractor: SelectedUserInteractor,
             @BindsInstance
             sensitiveNotificationProtectionController: SensitiveNotificationProtectionController,
+            @BindsInstance deviceEntryInteractor: DeviceEntryInteractor,
+            @BindsInstance sceneInteractor: SceneInteractor,
+            @BindsInstance @Application scope: CoroutineScope,
         ): TestSensitiveContentCoordinatorComponent
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index a7f36c3..f9509d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -66,7 +66,8 @@
             packageManager,
             Optional.of(bubbles),
             context,
-            notificationManager
+            notificationManager,
+            settingsInteractor
         )
     }
 
@@ -101,7 +102,7 @@
         whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
 
         val avalancheSuppressor = AvalancheSuppressor(
-            avalancheProvider, systemClock, systemSettings, packageManager,
+            avalancheProvider, systemClock, settingsInteractor, packageManager,
             uiEventLogger, context, notificationManager
         )
         avalancheSuppressor.hasSeenEdu = false
@@ -125,7 +126,7 @@
         whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
 
         val avalancheSuppressor = AvalancheSuppressor(
-            avalancheProvider, systemClock, systemSettings, packageManager,
+            avalancheProvider, systemClock, settingsInteractor, packageManager,
             uiEventLogger, context, notificationManager
         )
         avalancheSuppressor.hasSeenEdu = true
@@ -147,7 +148,7 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
                     uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
@@ -167,7 +168,7 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
                     uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
@@ -187,7 +188,7 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
                     uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
@@ -205,7 +206,7 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
                     uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
@@ -223,7 +224,7 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
                     uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
@@ -241,7 +242,7 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
                     uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
@@ -259,7 +260,7 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
                     uiEventLogger, context, notificationManager)
         ) {
             assertFsiNotSuppressed()
@@ -271,7 +272,7 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
                     uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
@@ -300,7 +301,7 @@
         setAllowedEmergencyPkg(true)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
                     uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index d5ab62b..9d3d9c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -61,6 +61,7 @@
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.res.R
 import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
 import com.android.systemui.statusbar.FakeStatusBarStateController
 import com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking
 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
@@ -88,6 +89,7 @@
 import com.android.wm.shell.bubbles.Bubbles
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.flow.MutableStateFlow
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
@@ -134,6 +136,7 @@
     protected val avalancheProvider: AvalancheProvider = mock()
     protected val bubbles: Bubbles = mock()
     lateinit var systemSettings: SystemSettings
+    protected val settingsInteractor: NotificationSettingsInteractor = mock()
     protected val packageManager: PackageManager = mock()
     protected val notificationManager: NotificationManager = mock()
     protected abstract val provider: VisualInterruptionDecisionProvider
@@ -164,7 +167,7 @@
         userTracker.set(listOf(user), /* currentUserIndex = */ 0)
         systemSettings = FakeSettings()
         whenever(bubbles.canShowBubbleNotification()).thenReturn(true)
-
+        whenever(settingsInteractor.isCooldownEnabled).thenReturn(MutableStateFlow(true))
         provider.start()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
index f4cebd7..7fd9c9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -61,7 +62,8 @@
         packageManager: PackageManager,
         bubbles: Optional<Bubbles>,
         context: Context,
-        notificationManager: NotificationManager
+        notificationManager: NotificationManager,
+        settingsInteractor: NotificationSettingsInteractor
     ): VisualInterruptionDecisionProvider {
         return if (VisualInterruptionRefactor.isEnabled) {
             VisualInterruptionDecisionProviderImpl(
@@ -85,7 +87,8 @@
                 packageManager,
                 bubbles,
                 context,
-                notificationManager
+                notificationManager,
+                settingsInteractor
             )
         } else {
             NotificationInterruptStateProviderWrapper(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index c36a046..3df4a67 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -20,6 +20,7 @@
 import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
 
 import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
@@ -96,9 +97,12 @@
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent;
 import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
+import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -192,6 +196,8 @@
 
     private NotificationStackScrollLayoutController mController;
 
+    private NotificationTestHelper mNotificationTestHelper;
+
     @Before
     public void setUp() {
         allowTestableLooperAsMainThread();
@@ -199,6 +205,11 @@
 
         when(mNotificationSwipeHelperBuilder.build()).thenReturn(mNotificationSwipeHelper);
         when(mKeyguardTransitionRepo.getTransitions()).thenReturn(emptyFlow());
+        mNotificationTestHelper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
     }
 
     @Test
@@ -222,6 +233,42 @@
     }
 
     @Test
+    @EnableFlags(GroupHunAnimationFix.FLAG_NAME)
+    public void changeHeadsUpAnimatingAwayToTrue_onEntryAnimatingAwayEndedNotCalled()
+            throws Exception {
+        // Before: bind an ExpandableNotificationRow,
+        initController(/* viewIsAttached= */ true);
+        mController.setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+        NotificationListContainer listContainer = mController.getNotificationListContainer();
+        ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        listContainer.bindRow(row);
+
+        // When: call setHeadsUpAnimatingAway to change set mHeadsupDisappearRunning to true
+        row.setHeadsUpAnimatingAway(true);
+
+        // Then: mHeadsUpManager.onEntryAnimatingAwayEnded is not called
+        verify(mHeadsUpManager, never()).onEntryAnimatingAwayEnded(row.getEntry());
+    }
+
+    @Test
+    @EnableFlags(GroupHunAnimationFix.FLAG_NAME)
+    public void changeHeadsUpAnimatingAwayToFalse_onEntryAnimatingAwayEndedCalled()
+            throws Exception {
+        // Before: bind an ExpandableNotificationRow, set its mHeadsupDisappearRunning to true
+        initController(/* viewIsAttached= */ true);
+        mController.setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+        NotificationListContainer listContainer = mController.getNotificationListContainer();
+        ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        listContainer.bindRow(row);
+        row.setHeadsUpAnimatingAway(true);
+
+        // When: call setHeadsUpAnimatingAway to change set mHeadsupDisappearRunning to false
+        row.setHeadsUpAnimatingAway(false);
+
+        // Then: mHeadsUpManager.onEntryAnimatingAwayEnded is called
+        verify(mHeadsUpManager).onEntryAnimatingAwayEnded(row.getEntry());
+    }
+    @Test
     public void testOnDensityOrFontScaleChanged_reInflatesFooterViews() {
         initController(/* viewIsAttached= */ true);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index d2540a6..bd9cccd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -150,6 +150,7 @@
 import com.android.systemui.shade.ShadeControllerImpl;
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shade.ShadeLogger;
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyboardShortcutListSearch;
 import com.android.systemui.statusbar.KeyboardShortcuts;
@@ -343,7 +344,7 @@
     @Mock private NotificationManager mNotificationManager;
     @Mock private GlanceableHubContainerController mGlanceableHubContainerController;
     @Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
-
+    @Mock private NotificationSettingsInteractor mNotificationSettingsInteractor;
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
@@ -403,7 +404,8 @@
                         mPackageManager,
                         Optional.of(mBubbles),
                         mContext,
-                        mNotificationManager);
+                        mNotificationManager,
+                        mNotificationSettingsInteractor);
         mVisualInterruptionDecisionProvider.start();
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index cf87afb..7f33c23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -299,27 +299,7 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
-    public void notifPaddingMakesUpToFullMarginInSplitShade_refactorFlagOff_usesResource() {
-        int keyguardSplitShadeTopMargin = 100;
-        int largeScreenHeaderHeightResource = 70;
-        when(mResources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin))
-                .thenReturn(keyguardSplitShadeTopMargin);
-        when(mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height))
-                .thenReturn(largeScreenHeaderHeightResource);
-        mClockPositionAlgorithm.loadDimens(mContext, mResources);
-        givenLockScreen();
-        mIsSplitShade = true;
-        // WHEN the position algorithm is run
-        positionClock();
-        // THEN the notif padding makes up lacking margin (margin - header height).
-        int expectedPadding = keyguardSplitShadeTopMargin - largeScreenHeaderHeightResource;
-        assertThat(mClockPosition.stackScrollerPadding).isEqualTo(expectedPadding);
-    }
-
-    @Test
-    @EnableFlags(Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX)
-    public void notifPaddingMakesUpToFullMarginInSplitShade_refactorFlagOn_usesHelper() {
+    public void notifPaddingMakesUpToFullMarginInSplitShade_usesHelper() {
         int keyguardSplitShadeTopMargin = 100;
         int largeScreenHeaderHeightHelper = 50;
         int largeScreenHeaderHeightResource = 70;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index af5e60e..9b61105 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1068,7 +1068,7 @@
     public void testShowBouncerOrKeyguard_needsFullScreen() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
         verify(mCentralSurfaces).hideKeyguard();
         verify(mPrimaryBouncerInteractor).show(true);
     }
@@ -1084,7 +1084,7 @@
                 .thenReturn(KeyguardState.LOCKSCREEN);
 
         reset(mCentralSurfaces);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
         verify(mPrimaryBouncerInteractor).show(true);
         verify(mCentralSurfaces).showKeyguard();
     }
@@ -1092,11 +1092,26 @@
     @Test
     @DisableSceneContainer
     public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
+        boolean isFalsingReset = false;
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
         verify(mCentralSurfaces, never()).hideKeyguard();
+        verify(mPrimaryBouncerInteractor).show(true);
+    }
+
+    @Test
+    @DisableSceneContainer
+    public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing_onFalsing() {
+        boolean isFalsingReset = true;
+        when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+                KeyguardSecurityModel.SecurityMode.SimPin);
+        when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+        verify(mCentralSurfaces, never()).hideKeyguard();
+
+        // Do not refresh the full screen bouncer if the call is from falsing
         verify(mPrimaryBouncerInteractor, never()).show(true);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 73ac6e3..af4f647 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -22,6 +22,7 @@
 import android.telephony.TelephonyManager
 import android.telephony.satellite.NtnSignalStrength
 import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback
 import android.telephony.satellite.SatelliteManager
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING
@@ -44,7 +45,6 @@
 import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
-import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
@@ -54,11 +54,8 @@
 import java.util.Optional
 import kotlin.test.Test
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -71,6 +68,7 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doThrow
 
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -186,152 +184,86 @@
         }
 
     @Test
-    fun isSatelliteAllowed_readsSatelliteManagerState_enabled() =
+    fun isSatelliteAllowed_listensToSatelliteManagerCallback() =
         testScope.runTest {
             setupDefaultRepo()
-            // GIVEN satellite is allowed in this location
-            val allowed = true
-
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onResult(allowed)
-                    null
-                }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
 
             val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+            runCurrent()
 
-            assertThat(latest).isTrue()
-        }
-
-    @Test
-    fun isSatelliteAllowed_readsSatelliteManagerState_disabled() =
-        testScope.runTest {
-            setupDefaultRepo()
-            // GIVEN satellite is not allowed in this location
-            val allowed = false
-
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onResult(allowed)
-                    null
+            val callback =
+                withArgCaptor<SatelliteCommunicationAllowedStateCallback> {
+                    verify(satelliteManager)
+                        .registerForCommunicationAllowedStateChanged(any(), capture())
                 }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
 
-            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+            // WHEN satellite manager says it's not available
+            callback.onSatelliteCommunicationAllowedStateChanged(false)
 
-            assertThat(latest).isFalse()
-        }
-
-    @Test
-    fun isSatelliteAllowed_pollsOnTimeout() =
-        testScope.runTest {
-            setupDefaultRepo()
-            // GIVEN satellite is not allowed in this location
-            var allowed = false
-
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onResult(allowed)
-                    null
-                }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
-
-            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
-
+            // THEN it's not!
             assertThat(latest).isFalse()
 
-            // WHEN satellite becomes enabled
-            allowed = true
+            // WHEN satellite manager says it's changed to available
+            callback.onSatelliteCommunicationAllowedStateChanged(true)
 
-            // WHEN the timeout has not yet been reached
-            advanceTimeBy(POLLING_INTERVAL_MS / 2)
-
-            // THEN the value is still false
-            assertThat(latest).isFalse()
-
-            // WHEN time advances beyond the polling interval
-            advanceTimeBy(POLLING_INTERVAL_MS / 2 + 1)
-
-            // THEN then new value is emitted
+            // THEN it is!
             assertThat(latest).isTrue()
         }
 
     @Test
-    fun isSatelliteAllowed_pollingRestartsWhenCollectionRestarts() =
-        testScope.runTest {
-            setupDefaultRepo()
-            // Use the old school launch/cancel so we can simulate subscribers arriving and leaving
-
-            var latest: Boolean? = false
-            var job =
-                underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
-
-            // GIVEN satellite is not allowed in this location
-            var allowed = false
-
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onResult(allowed)
-                    null
-                }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
-
-            assertThat(latest).isFalse()
-
-            // WHEN satellite becomes enabled
-            allowed = true
-
-            // WHEN the job is restarted
-            advanceTimeBy(POLLING_INTERVAL_MS / 2)
-
-            job.cancel()
-            job =
-                underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
-
-            // THEN the value is re-fetched
-            assertThat(latest).isTrue()
-
-            job.cancel()
-        }
-
-    @Test
     fun isSatelliteAllowed_falseWhenErrorOccurs() =
         testScope.runTest {
             setupDefaultRepo()
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onError(SatelliteException(1 /* unused */))
-                    null
-                }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
 
+            // GIVEN SatelliteManager gon' throw exceptions when we ask to register the callback
+            doThrow(RuntimeException("Test exception"))
+                .`when`(satelliteManager)
+                .registerForCommunicationAllowedStateChanged(any(), any())
+
+            // WHEN the latest value is requested (and thus causes an exception to be thrown)
             val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
 
+            // THEN the value is just false, and we didn't crash!
             assertThat(latest).isFalse()
         }
 
     @Test
+    fun isSatelliteAllowed_reRegistersOnTelephonyProcessCrash() =
+        testScope.runTest {
+            setupDefaultRepo()
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+            runCurrent()
+
+            val callback =
+                withArgCaptor<SatelliteCommunicationAllowedStateCallback> {
+                    verify(satelliteManager)
+                        .registerForCommunicationAllowedStateChanged(any(), capture())
+                }
+
+            val telephonyCallback =
+                MobileTelephonyHelpers.getTelephonyCallbackForType<
+                    TelephonyCallback.RadioPowerStateListener
+                >(
+                    telephonyManager
+                )
+
+            // GIVEN satellite is currently provisioned
+            callback.onSatelliteCommunicationAllowedStateChanged(true)
+
+            assertThat(latest).isTrue()
+
+            // WHEN a crash event happens (detected by radio state change)
+            telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON)
+            runCurrent()
+            telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF)
+            runCurrent()
+
+            // THEN listener is re-registered
+            verify(satelliteManager, times(2))
+                .registerForCommunicationAllowedStateChanged(any(), any())
+        }
+
+    @Test
     fun satelliteProvisioned_notSupported_defaultFalse() =
         testScope.runTest {
             // GIVEN satellite is not supported
@@ -363,24 +295,21 @@
         testScope.runTest {
             // GIVEN satellite is supported on device
             doAnswer {
-                val callback: OutcomeReceiver<Boolean, SatelliteException> =
-                    it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
-                callback.onResult(true)
-            }
+                    val callback: OutcomeReceiver<Boolean, SatelliteException> =
+                        it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+                    callback.onResult(true)
+                }
                 .whenever(satelliteManager)
                 .requestIsSupported(any(), any())
 
             // GIVEN satellite returns an error when asked if provisioned
             doAnswer {
-                val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                receiver.onError(SatelliteException(SATELLITE_RESULT_ERROR))
-                null
-            }
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onError(SatelliteException(SATELLITE_RESULT_ERROR))
+                    null
+                }
                 .whenever(satelliteManager)
-                .requestIsProvisioned(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
+                .requestIsProvisioned(any(), any<OutcomeReceiver<Boolean, SatelliteException>>())
 
             // GIVEN we've been up long enough to start querying
             systemClock.setUptimeMillis(Process.getStartUptimeMillis() + MIN_UPTIME)
@@ -409,10 +338,10 @@
         testScope.runTest {
             // GIVEN satellite is supported on device
             doAnswer {
-                val callback: OutcomeReceiver<Boolean, SatelliteException> =
-                    it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
-                callback.onResult(true)
-            }
+                    val callback: OutcomeReceiver<Boolean, SatelliteException> =
+                        it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+                    callback.onResult(true)
+                }
                 .whenever(satelliteManager)
                 .requestIsSupported(any(), any())
 
@@ -779,10 +708,10 @@
             .requestIsSupported(any(), any())
 
         doAnswer {
-            val callback: OutcomeReceiver<Boolean, SatelliteException> =
-                it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
-            callback.onResult(initialSatelliteIsProvisioned)
-        }
+                val callback: OutcomeReceiver<Boolean, SatelliteException> =
+                    it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+                callback.onResult(initialSatelliteIsProvisioned)
+            }
             .whenever(satelliteManager)
             .requestIsProvisioned(any(), any())
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt
new file mode 100644
index 0000000..3783af5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.touchpad.data.repository
+
+import android.hardware.input.FakeInputManager
+import android.hardware.input.InputManager.InputDeviceListener
+import android.hardware.input.fakeInputManager
+import android.testing.TestableLooper
+import android.view.InputDevice
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidJUnit4::class)
+class TouchpadRepositoryTest : SysuiTestCase() {
+
+    @Captor private lateinit var deviceListenerCaptor: ArgumentCaptor<InputDeviceListener>
+    private lateinit var fakeInputManager: FakeInputManager
+
+    private lateinit var underTest: TouchpadRepository
+    private lateinit var dispatcher: CoroutineDispatcher
+    private lateinit var inputDeviceRepo: InputDeviceRepository
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        fakeInputManager = testKosmos().fakeInputManager
+        dispatcher = StandardTestDispatcher()
+        testScope = TestScope(dispatcher)
+        val handler = FakeHandler(TestableLooper.get(this).looper)
+        inputDeviceRepo =
+            InputDeviceRepository(handler, testScope.backgroundScope, fakeInputManager.inputManager)
+        underTest =
+            TouchpadRepositoryImpl(dispatcher, fakeInputManager.inputManager, inputDeviceRepo)
+    }
+
+    @Test
+    fun emitsDisconnected_ifNothingIsConnected() =
+        testScope.runTest {
+            val initialState = underTest.isAnyTouchpadConnected.first()
+            assertThat(initialState).isFalse()
+        }
+
+    @Test
+    fun emitsConnected_ifTouchpadAlreadyConnectedAtTheStart() =
+        testScope.runTest {
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+            val initialValue = underTest.isAnyTouchpadConnected.first()
+            assertThat(initialValue).isTrue()
+        }
+
+    @Test
+    fun emitsConnected_whenNewTouchpadConnects() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+
+            assertThat(isTouchpadConnected).isTrue()
+        }
+
+    @Test
+    fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+            whenever(fakeInputManager.inputManager.getInputDevice(eq(NULL_DEVICE_ID)))
+                .thenReturn(null)
+            fakeInputManager.addDevice(NULL_DEVICE_ID, InputDevice.SOURCE_UNKNOWN)
+            assertThat(isTouchpadConnected).isFalse()
+        }
+
+    @Test
+    fun emitsDisconnected_whenTouchpadDisconnects() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+            assertThat(isTouchpadConnected).isTrue()
+
+            fakeInputManager.removeDevice(TOUCHPAD_ID)
+            assertThat(isTouchpadConnected).isFalse()
+        }
+
+    private suspend fun captureDeviceListener() {
+        underTest.isAnyTouchpadConnected.first()
+        Mockito.verify(fakeInputManager.inputManager)
+            .registerInputDeviceListener(deviceListenerCaptor.capture(), anyOrNull())
+        fakeInputManager.registerInputDeviceListener(deviceListenerCaptor.value)
+    }
+
+    @Test
+    fun emitsDisconnected_whenNonTouchpadConnects() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(NON_TOUCHPAD_ID, InputDevice.SOURCE_KEYBOARD)
+            assertThat(isTouchpadConnected).isFalse()
+        }
+
+    @Test
+    fun emitsDisconnected_whenTouchpadDisconnectsAndWasAlreadyConnectedAtTheStart() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.removeDevice(TOUCHPAD_ID)
+            assertThat(isTouchpadConnected).isFalse()
+        }
+
+    @Test
+    fun emitsConnected_whenAnotherDeviceDisconnects() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+            fakeInputManager.removeDevice(NON_TOUCHPAD_ID)
+
+            assertThat(isTouchpadConnected).isTrue()
+        }
+
+    @Test
+    fun emitsConnected_whenOneTouchpadDisconnectsButAnotherRemainsConnected() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+            fakeInputManager.addDevice(ANOTHER_TOUCHPAD_ID, TOUCHPAD)
+            fakeInputManager.removeDevice(TOUCHPAD_ID)
+
+            assertThat(isTouchpadConnected).isTrue()
+        }
+
+    private companion object {
+        private const val TOUCHPAD_ID = 1
+        private const val NON_TOUCHPAD_ID = 2
+        private const val ANOTHER_TOUCHPAD_ID = 3
+        private const val NULL_DEVICE_ID = 4
+
+        private const val TOUCHPAD = InputDevice.SOURCE_TOUCHPAD
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index cf0db7b..8875b84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -28,6 +28,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,16 +39,19 @@
 @RunWith(AndroidJUnit4::class)
 class BackGestureMonitorTest : SysuiTestCase() {
 
-    private var gestureDoneWasCalled = false
-    private val gestureDoneCallback = { gestureDoneWasCalled = true }
-    private val gestureMonitor = BackGestureMonitor(SWIPE_DISTANCE.toInt(), gestureDoneCallback)
+    private var gestureState = NOT_STARTED
+    private val gestureMonitor =
+        BackGestureMonitor(
+            gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
+            gestureStateChangedCallback = { gestureState = it }
+        )
 
     companion object {
         const val SWIPE_DISTANCE = 100f
     }
 
     @Test
-    fun triggersGestureDoneForThreeFingerGestureRight() {
+    fun triggersGestureFinishedForThreeFingerGestureRight() {
         val events =
             listOf(
                 threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -59,11 +65,11 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isTrue()
+        assertThat(gestureState).isEqualTo(FINISHED)
     }
 
     @Test
-    fun triggersGestureDoneForThreeFingerGestureLeft() {
+    fun triggersGestureFinishedForThreeFingerGestureLeft() {
         val events =
             listOf(
                 threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
@@ -77,7 +83,21 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isTrue()
+        assertThat(gestureState).isEqualTo(FINISHED)
+    }
+
+    @Test
+    fun triggersGestureProgressForThreeFingerGestureStarted() {
+        val events =
+            listOf(
+                threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
+                threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+                threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+            )
+
+        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+        assertThat(gestureState).isEqualTo(IN_PROGRESS)
     }
 
     private fun threeFingerEvent(action: Int, x: Float, y: Float): MotionEvent {
@@ -91,7 +111,7 @@
     }
 
     @Test
-    fun doesntTriggerGestureDone_onThreeFingersSwipeUp() {
+    fun doesntTriggerGestureFinished_onThreeFingersSwipeUp() {
         val events =
             listOf(
                 threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -105,11 +125,11 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isFalse()
+        assertThat(gestureState).isEqualTo(NOT_STARTED)
     }
 
     @Test
-    fun doesntTriggerGestureDone_onTwoFingersSwipe() {
+    fun doesntTriggerGestureFinished_onTwoFingersSwipe() {
         fun twoFingerEvent(action: Int, x: Float, y: Float) =
             motionEvent(
                 action = action,
@@ -127,11 +147,11 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isFalse()
+        assertThat(gestureState).isEqualTo(NOT_STARTED)
     }
 
     @Test
-    fun doesntTriggerGestureDone_onFourFingersSwipe() {
+    fun doesntTriggerGestureFinished_onFourFingersSwipe() {
         fun fourFingerEvent(action: Int, x: Float, y: Float) =
             motionEvent(
                 action = action,
@@ -155,6 +175,6 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isFalse()
+        assertThat(gestureState).isEqualTo(NOT_STARTED)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 769f264..dc4d5f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -32,6 +32,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -41,8 +43,8 @@
 @RunWith(AndroidJUnit4::class)
 class TouchpadGestureHandlerTest : SysuiTestCase() {
 
-    private var gestureDone = false
-    private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureDone = true }
+    private var gestureState = NOT_STARTED
+    private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureState = it }
 
     companion object {
         const val SWIPE_DISTANCE = 100
@@ -84,7 +86,7 @@
     fun triggersGestureDoneForThreeFingerGesture() {
         backGestureEvents().forEach { handler.onMotionEvent(it) }
 
-        assertThat(gestureDone).isTrue()
+        assertThat(gestureState).isEqualTo(FINISHED)
     }
 
     private fun backGestureEvents(): List<MotionEvent> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
index 5ac6110..b0acd03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
@@ -275,6 +275,18 @@
     }
 
     @Test
+    fun getInt_keyMalformed_returnDefaultValue() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThat(mSettings.getInt(TEST_SETTING, 5)).isEqualTo(5)
+    }
+
+    @Test
+    fun getInt_keyMalformed_throwException() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getInt(TEST_SETTING) }
+    }
+
+    @Test
     fun getBool_keyPresent_returnValidValue() {
         mSettings.putBool(TEST_SETTING, true)
         assertThat(mSettings.getBool(TEST_SETTING)).isTrue()
@@ -323,6 +335,18 @@
     }
 
     @Test
+    fun getLong_keyMalformed_throwException() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getLong(TEST_SETTING) }
+    }
+
+    @Test
+    fun getLong_keyMalformed_returnDefaultValue() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThat(mSettings.getLong(TEST_SETTING, 2L)).isEqualTo(2L)
+    }
+
+    @Test
     fun getFloat_keyPresent_returnValidValue() {
         mSettings.putFloat(TEST_SETTING, 2.5F)
         assertThat(mSettings.getFloat(TEST_SETTING)).isEqualTo(2.5F)
@@ -346,6 +370,18 @@
         assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F)
     }
 
+    @Test
+    fun getFloat_keyMalformed_throwException() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getFloat(TEST_SETTING) }
+    }
+
+    @Test
+    fun getFloat_keyMalformed_returnDefaultValue() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F)
+    }
+
     private class FakeSettingsProxy(val testDispatcher: CoroutineDispatcher) : SettingsProxy {
 
         private val mContentResolver = mock(ContentResolver::class.java)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
index 5f7420d..ead9939 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
@@ -23,6 +23,7 @@
 import android.os.Handler
 import android.os.Looper
 import android.provider.Settings
+import android.provider.Settings.SettingNotFoundException
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -433,6 +434,18 @@
     }
 
     @Test
+    fun getInt_keyMalformed_returnDefaultValue() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThat(mSettings.getInt(TEST_SETTING, 5)).isEqualTo(5)
+    }
+
+    @Test
+    fun getInt_keyMalformed_throwException() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getInt(TEST_SETTING) }
+    }
+
+    @Test
     fun getIntForUser_multipleUsers__validResult() {
         mSettings.putIntForUser(TEST_SETTING, 1, MAIN_USER_ID)
         mSettings.putIntForUser(TEST_SETTING, 2, SECONDARY_USER_ID)
@@ -501,6 +514,18 @@
     }
 
     @Test
+    fun getLong_keyMalformed_throwException() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getLong(TEST_SETTING) }
+    }
+
+    @Test
+    fun getLong_keyMalformed_returnDefaultValue() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThat(mSettings.getLong(TEST_SETTING, 2L)).isEqualTo(2L)
+    }
+
+    @Test
     fun getLongForUser_multipleUsers__validResult() {
         mSettings.putLongForUser(TEST_SETTING, 1L, MAIN_USER_ID)
         mSettings.putLongForUser(TEST_SETTING, 2L, SECONDARY_USER_ID)
@@ -535,6 +560,18 @@
     }
 
     @Test
+    fun getFloat_keyMalformed_throwException() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getFloat(TEST_SETTING) }
+    }
+
+    @Test
+    fun getFloat_keyMalformed_returnDefaultValue() {
+        mSettings.putString(TEST_SETTING, "nan")
+        assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F)
+    }
+
+    @Test
     fun getFloatForUser_multipleUsers__validResult() {
         mSettings.putFloatForUser(TEST_SETTING, 1F, MAIN_USER_ID)
         mSettings.putFloatForUser(TEST_SETTING, 2F, SECONDARY_USER_ID)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index d6b4d2b..7f964d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -123,6 +123,8 @@
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeWindowLogger;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel;
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.NotificationEntryHelper;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -347,6 +349,7 @@
     private Display mDefaultDisplay;
     @Mock
     private Lazy<ViewCapture> mLazyViewCapture;
+    @Mock private NotificationShadeWindowModel mNotificationShadeWindowModel;
 
     private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
     private ShadeInteractor mShadeInteractor;
@@ -430,6 +433,7 @@
                 mShadeWindowLogger,
                 () -> mSelectedUserInteractor,
                 mUserTracker,
+                mNotificationShadeWindowModel,
                 mKosmos::getCommunalInteractor
         );
         mNotificationShadeWindowController.fetchWindowRootView();
@@ -486,7 +490,8 @@
                         mock(PackageManager.class),
                         Optional.of(mock(Bubbles.class)),
                         mContext,
-                        mock(NotificationManager.class)
+                        mock(NotificationManager.class),
+                        mock(NotificationSettingsInteractor.class)
                         );
         interruptionDecisionProvider.start();
 
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
index 6e7c05c..ee36cad 100644
--- a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
@@ -16,6 +16,7 @@
 
 package android.hardware.input
 
+import android.hardware.input.InputManager.InputDeviceListener
 import android.view.InputDevice
 import android.view.KeyCharacterMap
 import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD
@@ -47,6 +48,8 @@
             VIRTUAL_KEYBOARD to allKeyCodes.toMutableSet()
         )
 
+    private var inputDeviceListener: InputDeviceListener? = null
+
     val inputManager =
         mock<InputManager> {
             whenever(getInputDevice(anyInt())).thenAnswer { invocation ->
@@ -84,6 +87,11 @@
         addPhysicalKeyboard(deviceId, enabled)
     }
 
+    fun registerInputDeviceListener(listener: InputDeviceListener) {
+        // TODO (b/355422259): handle this by listening to inputManager.registerInputDeviceListener
+        inputDeviceListener = listener
+    }
+
     fun addPhysicalKeyboard(id: Int, enabled: Boolean = true) {
         check(id > 0) { "Physical keyboard ids have to be > 0" }
         addKeyboard(id, enabled)
@@ -106,6 +114,16 @@
         supportedKeyCodesByDeviceId[id] = allKeyCodes.toMutableSet()
     }
 
+    fun addDevice(id: Int, sources: Int) {
+        devices[id] = InputDevice.Builder().setId(id).setSources(sources).build()
+        inputDeviceListener?.onInputDeviceAdded(id)
+    }
+
+    fun removeDevice(id: Int) {
+        devices.remove(id)
+        inputDeviceListener?.onInputDeviceRemoved(id)
+    }
+
     private fun InputDevice.copy(
         id: Int = getId(),
         type: Int = keyboardType,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
index eff99e04..ca748b66 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt
@@ -18,7 +18,14 @@
 
 import com.android.systemui.haptics.vibratorHelper
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.statusbar.policy.keyguardStateController
 
 val Kosmos.qsLongPressEffect by
-    Kosmos.Fixture { QSLongPressEffect(vibratorHelper, keyguardStateController) }
+    Kosmos.Fixture {
+        QSLongPressEffect(
+            vibratorHelper,
+            keyguardStateController,
+            FakeLogBuffer.Factory.create(),
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
new file mode 100644
index 0000000..9bd346e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.gesture.data
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.navigationbar.gestural.data.respository.GestureRepositoryImpl
+
+val Kosmos.gestureRepository: GestureRepository by
+    Kosmos.Fixture { GestureRepositoryImpl(testDispatcher) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
new file mode 100644
index 0000000..658aaa6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.gesture.domain
+
+import com.android.systemui.keyguard.gesture.data.gestureRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+
+val Kosmos.gestureInteractor: GestureInteractor by
+    Kosmos.Fixture {
+        GestureInteractor(gestureRepository = gestureRepository, scope = applicationCoroutineScope)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt
index 6eb8a49..2919d3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
 import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.dismissCallbackRegistry
 import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel
@@ -49,6 +50,7 @@
             alternateBouncerDependencies = { alternateBouncerDependencies },
             windowManager = { windowManager },
             layoutInflater = { mockedLayoutInflater },
+            dismissCallbackRegistry = dismissCallbackRegistry,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..6c644ee
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.alternateBouncerToLockscreenTransitionViewModel by Fixture {
+    AlternateBouncerToLockscreenTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 3c5baa5..82860fc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -40,6 +40,8 @@
         keyguardTransitionInteractor = keyguardTransitionInteractor,
         notificationsKeyguardInteractor = notificationsKeyguardInteractor,
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
+        alternateBouncerToLockscreenTransitionViewModel =
+            alternateBouncerToLockscreenTransitionViewModel,
         aodToGoneTransitionViewModel = aodToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
         aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index 2ca338a..f3d5b7d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -33,7 +33,7 @@
     private var _userHandle: UserHandle = UserHandle.of(_userId),
     private var _userInfo: UserInfo = mock(),
     private var _userProfiles: List<UserInfo> = emptyList(),
-    userContentResolver: ContentResolver = MockContentResolver(),
+    userContentResolverProvider: () -> ContentResolver = { MockContentResolver() },
     userContext: Context = mock(),
     private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
 ) : UserTracker {
@@ -41,14 +41,19 @@
 
     override val userId: Int
         get() = _userId
+
     override val userHandle: UserHandle
         get() = _userHandle
+
     override val userInfo: UserInfo
         get() = _userInfo
+
     override val userProfiles: List<UserInfo>
         get() = _userProfiles
 
-    override val userContentResolver: ContentResolver = userContentResolver
+    // userContentResolver is lazy because Ravenwood doesn't support MockContentResolver()
+    // and we still want to allow people use this class for tests that don't use it.
+    override val userContentResolver: ContentResolver by lazy { userContentResolverProvider() }
     override val userContext: Context = userContext
 
     override fun addCallback(callback: UserTracker.Callback, executor: Executor) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
index 79b80bc..a1f157f1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shade.domain.startable
 
 import android.content.applicationContext
+import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
 import com.android.systemui.common.ui.data.repository.configurationRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -28,6 +29,7 @@
 import com.android.systemui.shade.domain.interactor.panelExpansionInteractor
 import com.android.systemui.shade.transition.ScrimShadeTransitionController
 import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
+import com.android.systemui.statusbar.phone.scrimController
 import com.android.systemui.statusbar.policy.splitShadeStateController
 import com.android.systemui.statusbar.pulseExpansionHandler
 import com.android.systemui.util.mockito.mock
@@ -48,6 +50,8 @@
         panelExpansionInteractorProvider = { panelExpansionInteractor },
         shadeExpansionStateManager = shadeExpansionStateManager,
         pulseExpansionHandler = pulseExpansionHandler,
+        displayStateInteractor = displayStateInteractor,
         nsslc = notificationStackScrollLayoutController,
+        scrimController = scrimController,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeWindowModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeWindowModelKosmos.kt
new file mode 100644
index 0000000..cd4fab8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeWindowModelKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.notificationShadeWindowModel: NotificationShadeWindowModel by
+    Kosmos.Fixture { NotificationShadeWindowModel(keyguardTransitionInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
index a75d2bc..6373851 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
@@ -20,12 +20,14 @@
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.systemSettingsRepository
 
 val Kosmos.notificationSettingsRepository by
     Kosmos.Fixture {
         NotificationSettingsRepository(
-            scope = testScope.backgroundScope,
+            backgroundScope = testScope.backgroundScope,
             backgroundDispatcher = testDispatcher,
             secureSettingsRepository = secureSettingsRepository,
+            systemSettingsRepository = systemSettingsRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryKosmos.kt
new file mode 100644
index 0000000..01f19ae
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.shared.settings.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.systemSettingsRepository: SystemSettingsRepository by
+    Kosmos.Fixture { fakeSystemSettingsRepository }
+val Kosmos.fakeSystemSettingsRepository by Kosmos.Fixture { FakeSystemSettingsRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
index 78242b6..aef0828 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
@@ -18,10 +18,12 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository
 import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 
 val Kosmos.zenModeInteractor by Fixture {
     ZenModeInteractor(
-        repository = zenModeRepository,
+        zenModeRepository = zenModeRepository,
+        notificationSettingsRepository = notificationSettingsRepository,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettings.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettings.java
index 6417779..65f4122 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettings.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettings.java
@@ -51,6 +51,7 @@
         mDispatcher = dispatcher;
     }
 
+    @NonNull
     @Override
     public ContentResolver getContentResolver() {
         throw new UnsupportedOperationException(
@@ -58,6 +59,7 @@
                         + "GlobalSettings.registerContentObserver helpful instead.");
     }
 
+    @NonNull
     @Override
     public CoroutineDispatcher getBackgroundDispatcher() {
         return mDispatcher;
@@ -65,7 +67,7 @@
 
     @Override
     public void registerContentObserverSync(Uri uri, boolean notifyDescendants,
-            ContentObserver settingsObserver) {
+            @NonNull ContentObserver settingsObserver) {
         List<ContentObserver> observers;
         mContentObserversAllUsers.putIfAbsent(uri.toString(), new ArrayList<>());
         observers = mContentObserversAllUsers.get(uri.toString());
@@ -73,25 +75,26 @@
     }
 
     @Override
-    public void unregisterContentObserverSync(ContentObserver settingsObserver) {
+    public void unregisterContentObserverSync(@NonNull ContentObserver settingsObserver) {
         for (Map.Entry<String, List<ContentObserver>> entry :
                 mContentObserversAllUsers.entrySet()) {
             entry.getValue().remove(settingsObserver);
         }
     }
 
+    @NonNull
     @Override
-    public Uri getUriFor(String name) {
+    public Uri getUriFor(@NonNull String name) {
         return Uri.withAppendedPath(CONTENT_URI, name);
     }
 
     @Override
-    public String getString(String name) {
+    public String getString(@NonNull String name) {
         return mValues.get(getUriFor(name).toString());
     }
 
     @Override
-    public boolean putString(String name, String value) {
+    public boolean putString(@NonNull String name, @NonNull String value) {
         return putString(name, value, null, false);
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
index e4e2481..3f0318b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
@@ -26,7 +26,9 @@
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
+import com.android.systemui.settings.FakeUserTracker;
 import com.android.systemui.settings.UserTracker;
 
 import kotlinx.coroutines.CoroutineDispatcher;
@@ -42,6 +44,7 @@
             new HashMap<>();
     private final Map<String, List<ContentObserver>> mContentObserversAllUsers = new HashMap<>();
     private final CoroutineDispatcher mDispatcher;
+    private final UserTracker mUserTracker;
 
     public static final Uri CONTENT_URI = Uri.parse("content://settings/fake");
     @UserIdInt
@@ -54,42 +57,55 @@
     @Deprecated
     public FakeSettings() {
         mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null);
+        mUserTracker = new FakeUserTracker();
     }
 
     public FakeSettings(CoroutineDispatcher dispatcher) {
         mDispatcher = dispatcher;
+        mUserTracker = new FakeUserTracker();
     }
 
-    public FakeSettings(String initialKey, String initialValue) {
-        mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null);
+    public FakeSettings(CoroutineDispatcher dispatcher, UserTracker userTracker) {
+        mDispatcher = dispatcher;
+        mUserTracker = userTracker;
+    }
+
+    @VisibleForTesting
+    FakeSettings(String initialKey, String initialValue) {
+        this();
         putString(initialKey, initialValue);
     }
 
-    public FakeSettings(Map<String, String> initialValues) {
-        mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null);
+    @VisibleForTesting
+    FakeSettings(Map<String, String> initialValues) {
+        this();
         for (Map.Entry<String, String> kv : initialValues.entrySet()) {
             putString(kv.getKey(), kv.getValue());
         }
     }
 
     @Override
+    @NonNull
     public ContentResolver getContentResolver() {
-        return null;
+        throw new UnsupportedOperationException(
+                "FakeSettings.getContentResolver is not implemented");
     }
 
+    @NonNull
     @Override
     public UserTracker getUserTracker() {
-        return null;
+        return mUserTracker;
     }
 
+    @NonNull
     @Override
     public CoroutineDispatcher getBackgroundDispatcher() {
         return mDispatcher;
     }
 
     @Override
-    public void registerContentObserverForUserSync(Uri uri, boolean notifyDescendants,
-            ContentObserver settingsObserver, int userHandle) {
+    public void registerContentObserverForUserSync(@NonNull Uri uri, boolean notifyDescendants,
+            @NonNull ContentObserver settingsObserver, int userHandle) {
         List<ContentObserver> observers;
         if (userHandle == UserHandle.USER_ALL) {
             mContentObserversAllUsers.putIfAbsent(uri.toString(), new ArrayList<>());
@@ -103,19 +119,18 @@
     }
 
     @Override
-    public void unregisterContentObserverSync(ContentObserver settingsObserver) {
-        for (SettingsKey key : mContentObservers.keySet()) {
-            List<ContentObserver> observers = mContentObservers.get(key);
+    public void unregisterContentObserverSync(@NonNull ContentObserver settingsObserver) {
+        for (List<ContentObserver> observers : mContentObservers.values()) {
             observers.remove(settingsObserver);
         }
-        for (String key : mContentObserversAllUsers.keySet()) {
-            List<ContentObserver> observers = mContentObserversAllUsers.get(key);
+        for (List<ContentObserver> observers : mContentObserversAllUsers.values()) {
             observers.remove(settingsObserver);
         }
     }
 
+    @NonNull
     @Override
-    public Uri getUriFor(String name) {
+    public Uri getUriFor(@NonNull String name) {
         return Uri.withAppendedPath(CONTENT_URI, name);
     }
 
@@ -129,33 +144,34 @@
     }
 
     @Override
-    public String getString(String name) {
+    public String getString(@NonNull String name) {
         return getStringForUser(name, getUserId());
     }
 
     @Override
-    public String getStringForUser(String name, int userHandle) {
+    public String getStringForUser(@NonNull String name, int userHandle) {
         return mValues.get(new SettingsKey(userHandle, getUriFor(name).toString()));
     }
 
     @Override
-    public boolean putString(String name, String value, boolean overrideableByRestore) {
+    public boolean putString(@NonNull String name, @NonNull String value,
+            boolean overrideableByRestore) {
         return putStringForUser(name, value, null, false, getUserId(), overrideableByRestore);
     }
 
     @Override
-    public boolean putString(String name, String value) {
+    public boolean putString(@NonNull String name, @NonNull String value) {
         return putString(name, value, false);
     }
 
     @Override
-    public boolean putStringForUser(String name, String value, int userHandle) {
+    public boolean putStringForUser(@NonNull String name, @NonNull String value, int userHandle) {
         return putStringForUser(name, value, null, false, userHandle, false);
     }
 
     @Override
-    public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
-            int userHandle, boolean overrideableByRestore) {
+    public boolean putStringForUser(@NonNull String name, @NonNull String value, String tag,
+            boolean makeDefault, int userHandle, boolean overrideableByRestore) {
         SettingsKey key = new SettingsKey(userHandle, getUriFor(name).toString());
         mValues.put(key, value);
 
@@ -171,7 +187,8 @@
     }
 
     @Override
-    public boolean putString(@NonNull String name, String value, String tag, boolean makeDefault) {
+    public boolean putString(@NonNull String name, @NonNull String value, @NonNull String tag,
+            boolean makeDefault) {
         return putString(name, value);
     }
 
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 8d89cc1..7c8fd42 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -68,7 +68,10 @@
     srcs: [
         "runtime-common-ravenwood-src/**/*.java",
     ],
-    visibility: ["//frameworks/base"],
+    visibility: [
+        // Some tests need to access the utilities.
+        ":__subpackages__",
+    ],
 }
 
 java_library {
@@ -318,6 +321,9 @@
 
 android_ravenwood_libgroup {
     name: "ravenwood-runtime",
+    data: [
+        "framework-res",
+    ],
     libs: [
         "100-framework-minus-apex.ravenwood",
         "200-kxml2-android",
@@ -330,6 +336,11 @@
         "services.core.ravenwood-jarjar",
         "services.fakes.ravenwood-jarjar",
 
+        // ICU
+        "core-icu4j-for-host.ravenwood",
+        "icu4j-icudata-jarjar",
+        "icu4j-icutzdata-jarjar",
+
         // Provide runtime versions of utils linked in below
         "junit",
         "truth",
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index f6885e1..fbf27fa 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -12,6 +12,9 @@
     {
       "name": "RavenwoodBivalentTest_device"
     },
+    {
+      "name": "RavenwoodResApkTest"
+    },
     // The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING
     {
       "name": "SystemUIGoogleTests",
diff --git a/ravenwood/resapk_test/Android.bp b/ravenwood/resapk_test/Android.bp
new file mode 100644
index 0000000..c145765
--- /dev/null
+++ b/ravenwood/resapk_test/Android.bp
@@ -0,0 +1,30 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_ravenwood_test {
+    name: "RavenwoodResApkTest",
+
+    resource_apk: "RavenwoodResApkTest-apk",
+
+    libs: [
+        // Normally, tests shouldn't directly access it, but we need to access RavenwoodCommonUtils
+        // in this test.
+        "ravenwood-runtime-common-ravenwood",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+    ],
+    srcs: [
+        "test/**/*.java",
+    ],
+    sdk_version: "test_current",
+    auto_gen_config: true,
+}
diff --git a/ravenwood/resapk_test/apk/Android.bp b/ravenwood/resapk_test/apk/Android.bp
new file mode 100644
index 0000000..10ed5e2
--- /dev/null
+++ b/ravenwood/resapk_test/apk/Android.bp
@@ -0,0 +1,14 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_app {
+    name: "RavenwoodResApkTest-apk",
+
+    sdk_version: "current",
+}
diff --git a/ravenwood/resapk_test/apk/AndroidManifest.xml b/ravenwood/resapk_test/apk/AndroidManifest.xml
new file mode 100644
index 0000000..f34d8b2
--- /dev/null
+++ b/ravenwood/resapk_test/apk/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.ravenwood.restest_apk">
+</manifest>
diff --git a/ravenwood/resapk_test/apk/res/values/strings.xml b/ravenwood/resapk_test/apk/res/values/strings.xml
new file mode 100644
index 0000000..23d4c0f
--- /dev/null
+++ b/ravenwood/resapk_test/apk/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Test string 1 -->
+    <string name="test_string_1" translatable="false" >Test String 1</string>
+</resources>
diff --git a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java b/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
new file mode 100644
index 0000000..1029ed2
--- /dev/null
+++ b/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ravenwood.resapk_test;
+
+
+import static junit.framework.TestCase.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodResApkTest {
+    /**
+     * Ensure the file "ravenwood-res.apk" exists.
+     * TODO Check the content of it, once Ravenwood supports resources. The file should
+     * be a copy of RavenwoodResApkTest-apk.apk
+     */
+    @Test
+    public void testResApkExists() {
+        var file = "ravenwood-res-apks/ravenwood-res.apk";
+
+        assertTrue(new File(file).exists());
+    }
+
+    @Test
+    public void testFrameworkResExists() {
+        var file = "ravenwood-data/framework-res.apk";
+
+        assertTrue(new File(
+                RavenwoodCommonUtils.getRavenwoodRuntimePath() + "/" + file).exists());
+    }
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
index 8fe6853..1a15d7a 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
@@ -25,19 +25,24 @@
 import static android.os.ParcelFileDescriptor.MODE_WORLD_WRITEABLE;
 import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
 
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
 import com.android.internal.annotations.GuardedBy;
 import com.android.ravenwood.common.JvmWorkaround;
 
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.util.HashMap;
 import java.util.Map;
 
 public class ParcelFileDescriptor_host {
+    private static final String TAG = "ParcelFileDescriptor_host";
+
     /**
      * Since we don't have a great way to keep an unmanaged {@code FileDescriptor} reference
      * alive, we keep a strong reference to the {@code RandomAccessFile} we used to open it. This
@@ -98,16 +103,18 @@
         synchronized (sActive) {
             raf = sActive.remove(fd);
         }
+        int fdInt = JvmWorkaround.getInstance().getFdInt(fd);
         try {
             if (raf != null) {
                 raf.close();
             } else {
-                // Odd, we don't remember opening this ourselves, but let's release the
-                // underlying resource as requested
-                System.err.println("Closing unknown FileDescriptor: " + fd);
-                new FileOutputStream(fd).close();
+                // This FD wasn't created by native_open$ravenwood().
+                // The FD was passed to the PFD ctor. Just close it.
+                Os.close(fd);
             }
-        } catch (IOException ignored) {
+        } catch (IOException | ErrnoException e) {
+            Log.w(TAG, "Exception thrown while closing fd " + fdInt, e);
         }
     }
 }
+;
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
index 22e11e1..2df93cd 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
@@ -15,6 +15,11 @@
  */
 package com.android.platform.test.ravenwood.nativesubstitution;
 
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
@@ -31,6 +36,8 @@
  * {@link ByteBuffer} wouldn't allow...)
  */
 public class Parcel_host {
+    private static final String TAG = "Parcel";
+
     private Parcel_host() {
     }
 
@@ -50,6 +57,11 @@
     // TODO Use the actual value from Parcel.java.
     private static final int OK = 0;
 
+    private final Map<Integer, FileDescriptor> mFdMap = new ConcurrentHashMap<>();
+
+    private static final int FD_PLACEHOLDER = 0xDEADBEEF;
+    private static final int FD_PAYLOAD_SIZE = 8;
+
     private void validate() {
         if (mDeleted) {
             // TODO: Put more info
@@ -67,6 +79,7 @@
         return p;
     }
 
+    /** Native method substitution */
     public static long nativeCreate() {
         final long id = sNextId.getAndIncrement();
         final Parcel_host p = new Parcel_host();
@@ -80,7 +93,8 @@
         mSize = 0;
         mPos = 0;
         mSensitive = false;
-        mAllowFds = false;
+        mAllowFds = true;
+        mFdMap.clear();
     }
 
     private void updateSize() {
@@ -89,16 +103,19 @@
         }
     }
 
+    /** Native method substitution */
     public static void nativeDestroy(long nativePtr) {
         getInstance(nativePtr).mDeleted = true;
         sInstances.remove(nativePtr);
     }
 
+    /** Native method substitution */
     public static void nativeFreeBuffer(long nativePtr) {
         getInstance(nativePtr).freeBuffer();
     }
 
-    public void freeBuffer() {
+    /** Native method substitution */
+    private void freeBuffer() {
         init();
     }
 
@@ -137,32 +154,47 @@
         }
     }
 
+    /** Native method substitution */
     public static void nativeMarkSensitive(long nativePtr) {
         getInstance(nativePtr).mSensitive = true;
     }
+
+    /** Native method substitution */
     public static int nativeDataSize(long nativePtr) {
         return getInstance(nativePtr).mSize;
     }
+
+    /** Native method substitution */
     public static int nativeDataAvail(long nativePtr) {
         var p = getInstance(nativePtr);
         return p.mSize - p.mPos;
     }
+
+    /** Native method substitution */
     public static int nativeDataPosition(long nativePtr) {
         return getInstance(nativePtr).mPos;
     }
+
+    /** Native method substitution */
     public static int nativeDataCapacity(long nativePtr) {
         return getInstance(nativePtr).mBuffer.length;
     }
+
+    /** Native method substitution */
     public static void nativeSetDataSize(long nativePtr, int size) {
         var p = getInstance(nativePtr);
         p.ensureCapacity(size);
         getInstance(nativePtr).mSize = size;
     }
+
+    /** Native method substitution */
     public static void nativeSetDataPosition(long nativePtr, int pos) {
         var p = getInstance(nativePtr);
         // TODO: Should this change the size or the capacity??
         p.mPos = pos;
     }
+
+    /** Native method substitution */
     public static void nativeSetDataCapacity(long nativePtr, int size) {
         if (size < 0) {
             throw new IllegalArgumentException("size < 0: size=" + size);
@@ -173,20 +205,25 @@
         }
     }
 
+    /** Native method substitution */
     public static boolean nativePushAllowFds(long nativePtr, boolean allowFds) {
         var p = getInstance(nativePtr);
         var prev = p.mAllowFds;
         p.mAllowFds = allowFds;
         return prev;
     }
+
+    /** Native method substitution */
     public static void nativeRestoreAllowFds(long nativePtr, boolean lastValue) {
         getInstance(nativePtr).mAllowFds = lastValue;
     }
 
+    /** Native method substitution */
     public static void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len) {
         nativeWriteBlob(nativePtr, b, offset, len);
     }
 
+    /** Native method substitution */
     public static void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len) {
         var p = getInstance(nativePtr);
 
@@ -205,6 +242,7 @@
         }
     }
 
+    /** Native method substitution */
     public static int nativeWriteInt(long nativePtr, int value) {
         var p = getInstance(nativePtr);
         p.ensureMoreCapacity(Integer.BYTES);
@@ -219,14 +257,19 @@
         return OK;
     }
 
+    /** Native method substitution */
     public static int nativeWriteLong(long nativePtr, long value) {
         nativeWriteInt(nativePtr, (int) (value >>> 32));
         nativeWriteInt(nativePtr, (int) (value));
         return OK;
     }
+
+    /** Native method substitution */
     public static int nativeWriteFloat(long nativePtr, float val) {
         return nativeWriteInt(nativePtr, Float.floatToIntBits(val));
     }
+
+    /** Native method substitution */
     public static int nativeWriteDouble(long nativePtr, double val) {
         return nativeWriteLong(nativePtr, Double.doubleToLongBits(val));
     }
@@ -235,6 +278,7 @@
         return ((val + 3) / 4) * 4;
     }
 
+    /** Native method substitution */
     public static void nativeWriteString8(long nativePtr, String val) {
         if (val == null) {
             nativeWriteBlob(nativePtr, null, 0, 0);
@@ -243,15 +287,19 @@
             nativeWriteBlob(nativePtr, bytes, 0, bytes.length);
         }
     }
+
+    /** Native method substitution */
     public static void nativeWriteString16(long nativePtr, String val) {
         // Just reuse String8
         nativeWriteString8(nativePtr, val);
     }
 
+    /** Native method substitution */
     public static byte[] nativeCreateByteArray(long nativePtr) {
         return nativeReadBlob(nativePtr);
     }
 
+    /** Native method substitution */
     public static boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen) {
         if (dest == null) {
             return false;
@@ -271,6 +319,7 @@
         return true;
     }
 
+    /** Native method substitution */
     public static byte[] nativeReadBlob(long nativePtr) {
         var p = getInstance(nativePtr);
         if (p.mSize - p.mPos < 4) {
@@ -295,6 +344,8 @@
 
         return bytes;
     }
+
+    /** Native method substitution */
     public static int nativeReadInt(long nativePtr) {
         var p = getInstance(nativePtr);
 
@@ -310,19 +361,24 @@
 
         return ret;
     }
+
+    /** Native method substitution */
     public static long nativeReadLong(long nativePtr) {
         return (((long) nativeReadInt(nativePtr)) << 32)
                 | (((long) nativeReadInt(nativePtr)) & 0xffff_ffffL);
     }
 
+    /** Native method substitution */
     public static float nativeReadFloat(long nativePtr) {
         return Float.intBitsToFloat(nativeReadInt(nativePtr));
     }
 
+    /** Native method substitution */
     public static double nativeReadDouble(long nativePtr) {
         return Double.longBitsToDouble(nativeReadLong(nativePtr));
     }
 
+    /** Native method substitution */
     public static String nativeReadString8(long nativePtr) {
         final var bytes = nativeReadBlob(nativePtr);
         if (bytes == null) {
@@ -334,10 +390,13 @@
         return nativeReadString8(nativePtr);
     }
 
+    /** Native method substitution */
     public static byte[] nativeMarshall(long nativePtr) {
         var p = getInstance(nativePtr);
         return Arrays.copyOf(p.mBuffer, p.mSize);
     }
+
+    /** Native method substitution */
     public static void nativeUnmarshall(
             long nativePtr, byte[] data, int offset, int length) {
         var p = getInstance(nativePtr);
@@ -346,6 +405,8 @@
         p.mPos += length;
         p.updateSize();
     }
+
+    /** Native method substitution */
     public static int nativeCompareData(long thisNativePtr, long otherNativePtr) {
         var a = getInstance(thisNativePtr);
         var b = getInstance(otherNativePtr);
@@ -355,6 +416,8 @@
             return -1;
         }
     }
+
+    /** Native method substitution */
     public static boolean nativeCompareDataInRange(
             long ptrA, int offsetA, long ptrB, int offsetB, int length) {
         var a = getInstance(ptrA);
@@ -368,6 +431,8 @@
         return Arrays.equals(Arrays.copyOfRange(a.mBuffer, offsetA, offsetA + length),
                 Arrays.copyOfRange(b.mBuffer, offsetB, offsetB + length));
     }
+
+    /** Native method substitution */
     public static void nativeAppendFrom(
             long thisNativePtr, long otherNativePtr, int srcOffset, int length) {
         var dst = getInstance(thisNativePtr);
@@ -382,25 +447,83 @@
         // TODO: Update the other's position?
     }
 
-    public static boolean nativeHasFileDescriptors(long nativePtr) {
-        // Assume false for now, because we don't support writing FDs yet.
-        return false;
-    }
-
-    public static boolean nativeHasFileDescriptorsInRange(
-            long nativePtr, int offset, int length) {
-        // Assume false for now, because we don't support writing FDs yet.
-        return false;
-    }
-
+    /** Native method substitution */
     public static boolean nativeHasBinders(long nativePtr) {
         // Assume false for now, because we don't support adding binders.
         return false;
     }
 
+    /** Native method substitution */
     public static boolean nativeHasBindersInRange(
             long nativePtr, int offset, int length) {
         // Assume false for now, because we don't support writing FDs yet.
         return false;
     }
-}
+
+    /** Native method substitution */
+    public static void nativeWriteFileDescriptor(long nativePtr, java.io.FileDescriptor val) {
+        var p = getInstance(nativePtr);
+
+        if (!p.mAllowFds) {
+            // Simulate the FDS_NOT_ALLOWED case in frameworks/base/core/jni/android_util_Binder.cpp
+            throw new RuntimeException("Not allowed to write file descriptors here");
+        }
+
+        FileDescriptor dup = null;
+        try {
+            dup = Os.dup(val);
+        } catch (ErrnoException e) {
+            throw new RuntimeException(e);
+        }
+        p.mFdMap.put(p.mPos, dup);
+
+        // Parcel.cpp writes two int32s for a FD.
+        // Make sure FD_PAYLOAD_SIZE is in sync with this code.
+        nativeWriteInt(nativePtr, FD_PLACEHOLDER);
+        nativeWriteInt(nativePtr, FD_PLACEHOLDER);
+    }
+
+    /** Native method substitution */
+    public static java.io.FileDescriptor nativeReadFileDescriptor(long nativePtr) {
+        var p = getInstance(nativePtr);
+
+        var pos = p.mPos;
+        var fd = p.mFdMap.get(pos);
+
+        if (fd == null) {
+            Log.w(TAG, "nativeReadFileDescriptor: Not a FD at pos #" + pos);
+            return null;
+        }
+        nativeReadInt(nativePtr);
+        return fd;
+    }
+
+    /** Native method substitution */
+    public static boolean nativeHasFileDescriptors(long nativePtr) {
+        var p = getInstance(nativePtr);
+        return p.mFdMap.size() > 0;
+    }
+
+    /** Native method substitution */
+    public static boolean nativeHasFileDescriptorsInRange(long nativePtr, int offset, int length) {
+        var p = getInstance(nativePtr);
+
+        // Original code: hasFileDescriptorsInRange() in frameworks/native/libs/binder/Parcel.cpp
+        if (offset < 0 || length < 0) {
+            throw new IllegalArgumentException("Negative value not allowed: offset=" + offset
+                    + " length=" + length);
+        }
+        long limit = (long) offset + (long) length;
+        if (limit > p.mSize) {
+            throw new IllegalArgumentException("Out of range: offset=" + offset
+                    + " length=" + length + " dataSize=" + p.mSize);
+        }
+
+        for (var pos : p.mFdMap.keySet()) {
+            if (offset <= pos && (pos + FD_PAYLOAD_SIZE - 1) < (offset + length)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
index 8a1fe62..825ab72 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -53,4 +53,9 @@
     public static StructStat stat(String path) throws ErrnoException {
         return RavenwoodRuntimeNative.stat(path);
     }
+
+    /** Ravenwood version of the OS API. */
+    public static void close(FileDescriptor fd) throws ErrnoException {
+        RavenwoodRuntimeNative.close(fd);
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
index e9b305e..2bc8e71 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
@@ -48,6 +48,8 @@
 
     public static native StructStat stat(String path) throws ErrnoException;
 
+    private static native void nClose(int fd) throws ErrnoException;
+
     public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
         return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
     }
@@ -83,4 +85,11 @@
 
         return nFstat(fdInt);
     }
+
+    /** See close(2) */
+    public static void close(FileDescriptor fd) throws ErrnoException {
+        var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
+
+        nClose(fdInt);
+    }
 }
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index e0a3e1c..ee84954 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -167,6 +167,11 @@
     return doStat(env, javaPath, false);
 }
 
+static void nClose(JNIEnv* env, jclass, jint fd) {
+    // Don't use TEMP_FAILURE_RETRY() on close(): https://lkml.org/lkml/2005/9/10/129
+    throwIfMinusOne(env, "close", close(fd));
+}
+
 // ---- Registration ----
 
 static const JNINativeMethod sMethods[] =
@@ -179,6 +184,7 @@
     { "nFstat", "(I)Landroid/system/StructStat;", (void*)nFstat },
     { "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat },
     { "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat },
+    { "nClose", "(I)V", (void*)nClose },
 };
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 30c743e..45fcf6b 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -41,6 +41,7 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED;
 import static android.view.accessibility.AccessibilityManager.FlashNotificationReason;
 
+import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
@@ -57,7 +58,6 @@
 import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain;
-import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 
 import android.accessibilityservice.AccessibilityGestureEvent;
@@ -825,25 +825,27 @@
     @VisibleForTesting
     boolean onPackagesForceStoppedLocked(
             String[] packages, AccessibilityUserState userState) {
-        final List<String> continuousServicePackages =
+        final Set<String> packageSet = new HashSet<>(List.of(packages));
+        final ArrayList<ComponentName> continuousServices = new ArrayList<>(
                 userState.mInstalledServices.stream().filter(service ->
                         (service.flags & FLAG_REQUEST_ACCESSIBILITY_BUTTON)
                                 == FLAG_REQUEST_ACCESSIBILITY_BUTTON
-                ).map(service -> service.getComponentName().flattenToString()).toList();
+                ).map(AccessibilityServiceInfo::getComponentName).toList());
+
+        // Filter out continuous packages that are not from the array of stopped packages.
+        continuousServices.removeIf(
+                continuousName -> !packageSet.contains(continuousName.getPackageName()));
 
         boolean enabledServicesChanged = false;
         final Iterator<ComponentName> it = userState.mEnabledServices.iterator();
         while (it.hasNext()) {
             final ComponentName comp = it.next();
             final String compPkg = comp.getPackageName();
-            for (String pkg : packages) {
-                if (compPkg.equals(pkg)) {
-                    it.remove();
-                    userState.getBindingServicesLocked().remove(comp);
-                    userState.getCrashedServicesLocked().remove(comp);
-                    enabledServicesChanged = true;
-                    break;
-                }
+            if (packageSet.contains(compPkg)) {
+                it.remove();
+                userState.getBindingServicesLocked().remove(comp);
+                userState.getCrashedServicesLocked().remove(comp);
+                enabledServicesChanged = true;
             }
         }
         if (enabledServicesChanged) {
@@ -855,8 +857,8 @@
         // Remove any button targets that match any stopped continuous services
         Set<String> buttonTargets = userState.getShortcutTargetsLocked(SOFTWARE);
         boolean buttonTargetsChanged = buttonTargets.removeIf(
-                target -> continuousServicePackages.stream().anyMatch(
-                        pkg -> Objects.equals(target, pkg)));
+                target -> continuousServices.stream().anyMatch(
+                        continuousName -> continuousName.flattenToString().equals(target)));
         if (buttonTargetsChanged) {
             userState.updateShortcutTargetsLocked(buttonTargets, SOFTWARE);
             persistColonDelimitedSetToSettingLocked(
@@ -2641,7 +2643,8 @@
         }
     }
 
-    private <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId,
+    @VisibleForTesting
+    <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId,
             Set<T> set, Function<T, String> toString) {
         persistColonDelimitedSetToSettingLocked(settingName, userId, set,
                 toString, /* defaultEmptyString= */ null);
@@ -4087,11 +4090,7 @@
             boolean enable, @UserShortcutType int shortcutTypes,
             @NonNull List<String> shortcutTargets, @UserIdInt int userId) {
         enableShortcutsForTargets_enforcePermission();
-        if ((shortcutTypes & GESTURE) == GESTURE
-                && !android.provider.Flags.a11yStandaloneGestureEnabled()) {
-            throw new IllegalArgumentException(
-                    "GESTURE type shortcuts are disabled by feature flag");
-        }
+
         for (int shortcutType : USER_SHORTCUT_TYPES) {
             if ((shortcutTypes & shortcutType) == shortcutType) {
                 enableShortcutForTargets(enable, shortcutType, shortcutTargets, userId);
@@ -4102,6 +4101,13 @@
     private void enableShortcutForTargets(
             boolean enable, @UserShortcutType int shortcutType,
             @NonNull List<String> shortcutTargets, @UserIdInt int userId) {
+        if (shortcutType == UserShortcutType.GESTURE
+                && !android.provider.Flags.a11yStandaloneGestureEnabled()) {
+            Slog.w(LOG_TAG,
+                    "GESTURE type shortcuts are disabled by feature flag");
+            return;
+        }
+
         final String shortcutTypeSettingKey = ShortcutUtils.convertToKey(shortcutType);
         if (shortcutType == UserShortcutType.TRIPLETAP
                 || shortcutType == UserShortcutType.TWOFINGER_DOUBLETAP) {
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index e9c3fbd..0ee5896 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -523,6 +523,7 @@
             mScaleGestureDetector = new ScaleGestureDetector(context, this, Handler.getMain());
             mScaleGestureDetector.setQuickScaleEnabled(false);
             mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
+            mScrollGestureDetector.setIsLongpressEnabled(false);
         }
 
         @Override
@@ -1658,11 +1659,12 @@
         }
         float dX = event.getX() - firstPointerDownLocation.x;
         float dY = event.getY() - firstPointerDownLocation.y;
-        if (isAtLeftEdge() && dX > 0) {
+        if (isAtLeftEdge() && isScrollingLeft(dX, dY)) {
             return OVERSCROLL_LEFT_EDGE;
-        } else if (isAtRightEdge() && dX < 0) {
+        } else if (isAtRightEdge() && isScrollingRight(dX, dY)) {
             return OVERSCROLL_RIGHT_EDGE;
-        } else if ((isAtTopEdge() && dY > 0) || (isAtBottomEdge() && dY < 0)) {
+        } else if ((isAtTopEdge() && isScrollingUp(dX, dY))
+                || (isAtBottomEdge() && isScrollingDown(dX, dY))) {
             return OVERSCROLL_VERTICAL_EDGE;
         }
         return OVERSCROLL_NONE;
@@ -1672,18 +1674,34 @@
         return mFullScreenMagnificationController.isAtLeftEdge(mDisplayId, mOverscrollEdgeSlop);
     }
 
+    private static boolean isScrollingLeft(float dX, float dY) {
+        return Math.abs(dX) > Math.abs(dY) && dX > 0;
+    }
+
     private boolean isAtRightEdge() {
         return mFullScreenMagnificationController.isAtRightEdge(mDisplayId, mOverscrollEdgeSlop);
     }
 
+    private static boolean isScrollingRight(float dX, float dY) {
+        return Math.abs(dX) > Math.abs(dY) && dX < 0;
+    }
+
     private boolean isAtTopEdge() {
         return mFullScreenMagnificationController.isAtTopEdge(mDisplayId, mOverscrollEdgeSlop);
     }
 
+    private static boolean isScrollingUp(float dX, float dY) {
+        return Math.abs(dX) < Math.abs(dY) && dY > 0;
+    }
+
     private boolean isAtBottomEdge() {
         return mFullScreenMagnificationController.isAtBottomEdge(mDisplayId, mOverscrollEdgeSlop);
     }
 
+    private static boolean isScrollingDown(float dX, float dY) {
+        return Math.abs(dX) < Math.abs(dY) && dY < 0;
+    }
+
     private boolean pointerValid(PointF pointerDownLocation) {
         return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(pointerDownLocation.y));
     }
@@ -1876,6 +1894,7 @@
         private MotionEventInfo mEvent;
         SinglePanningState(Context context) {
             mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
+            mScrollGestureDetector.setIsLongpressEnabled(false);
         }
 
         @Override
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index eae516e..9f7fb57 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -1985,12 +1985,13 @@
         }
 
         @Override
-        public void setAutofillFailure(int sessionId, @NonNull List<AutofillId> ids, int userId) {
+        public void setAutofillFailure(
+                int sessionId, @NonNull List<AutofillId> ids, boolean isRefill, int userId) {
             synchronized (mLock) {
                 final AutofillManagerServiceImpl service =
                         peekServiceForUserWithLocalBinderIdentityLocked(userId);
                 if (service != null) {
-                    service.setAutofillFailureLocked(sessionId, getCallingUid(), ids);
+                    service.setAutofillFailureLocked(sessionId, getCallingUid(), ids, isRefill);
                 } else if (sVerbose) {
                     Slog.v(TAG, "setAutofillFailure(): no service for " + userId);
                 }
@@ -2011,6 +2012,46 @@
         }
 
         @Override
+        public void notifyNotExpiringResponseDuringAuth(int sessionId, int userId) {
+            synchronized (mLock) {
+                final AutofillManagerServiceImpl service =
+                        peekServiceForUserWithLocalBinderIdentityLocked(userId);
+                if (service != null) {
+                    service.notifyNotExpiringResponseDuringAuth(sessionId, getCallingUid());
+                } else if (sVerbose) {
+                    Slog.v(TAG, "notifyNotExpiringResponseDuringAuth(): no service for " + userId);
+                }
+            }
+        }
+
+        @Override
+        public void notifyViewEnteredIgnoredDuringAuthCount(int sessionId, int userId) {
+            synchronized (mLock) {
+                final AutofillManagerServiceImpl service =
+                        peekServiceForUserWithLocalBinderIdentityLocked(userId);
+                if (service != null) {
+                    service.notifyViewEnteredIgnoredDuringAuthCount(sessionId, getCallingUid());
+                } else if (sVerbose) {
+                    Slog.v(TAG, "notifyNotExpiringResponseDuringAuth(): no service for " + userId);
+                }
+            }
+        }
+
+        @Override
+        public void setAutofillIdsAttemptedForRefill(
+                int sessionId, @NonNull List<AutofillId> ids, int userId) {
+            synchronized (mLock) {
+                final AutofillManagerServiceImpl service =
+                        peekServiceForUserWithLocalBinderIdentityLocked(userId);
+                if (service != null) {
+                    service.setAutofillIdsAttemptedForRefill(sessionId, ids, getCallingUid());
+                } else if (sVerbose) {
+                    Slog.v(TAG, "setAutofillIdsAttemptedForRefill(): no service for " + userId);
+                }
+            }
+        }
+
+        @Override
         public void finishSession(int sessionId, int userId,
                 @AutofillCommitReason int commitReason) {
             synchronized (mLock) {
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 2bf319e..c9f8929 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -464,7 +464,8 @@
     }
 
     @GuardedBy("mLock")
-    void setAutofillFailureLocked(int sessionId, int uid, @NonNull List<AutofillId> ids) {
+    void setAutofillFailureLocked(
+            int sessionId, int uid, @NonNull List<AutofillId> ids, boolean isRefill) {
         if (!isEnabledLocked()) {
             Slog.wtf(TAG, "Service not enabled");
             return;
@@ -474,7 +475,7 @@
             Slog.v(TAG, "setAutofillFailure(): no session for " + sessionId + "(" + uid + ")");
             return;
         }
-        session.setAutofillFailureLocked(ids);
+        session.setAutofillFailureLocked(ids, isRefill);
     }
 
     @GuardedBy("mLock")
@@ -492,6 +493,52 @@
     }
 
     @GuardedBy("mLock")
+    void notifyNotExpiringResponseDuringAuth(int sessionId, int uid) {
+        if (!isEnabledLocked()) {
+            Slog.wtf(TAG, "Service not enabled");
+            return;
+        }
+        final Session session = mSessions.get(sessionId);
+        if (session == null || uid != session.uid) {
+            Slog.v(TAG, "notifyNotExpiringResponseDuringAuth(): no session for "
+                    + sessionId + "(" + uid + ")");
+            return;
+        }
+        session.setNotifyNotExpiringResponseDuringAuth();
+    }
+
+    @GuardedBy("mLock")
+    void notifyViewEnteredIgnoredDuringAuthCount(int sessionId, int uid) {
+        if (!isEnabledLocked()) {
+            Slog.wtf(TAG, "Service not enabled");
+            return;
+        }
+        final Session session = mSessions.get(sessionId);
+        if (session == null || uid != session.uid) {
+            Slog.v(TAG, "notifyViewEnteredIgnoredDuringAuthCount(): no session for "
+                    + sessionId + "(" + uid + ")");
+            return;
+        }
+        session.setLogViewEnteredIgnoredDuringAuth();
+    }
+
+    @GuardedBy("mLock")
+    public void setAutofillIdsAttemptedForRefill(
+            int sessionId, @NonNull List<AutofillId> ids, int uid) {
+        if (!isEnabledLocked()) {
+            Slog.wtf(TAG, "Service not enabled");
+            return;
+        }
+        final Session session = mSessions.get(sessionId);
+        if (session == null || uid != session.uid) {
+            Slog.v(TAG, "setAutofillIdsAttemptedForRefill(): no session for "
+                    + sessionId + "(" + uid + ")");
+            return;
+        }
+        session.setAutofillIdsAttemptedForRefillLocked(ids);
+    }
+
+    @GuardedBy("mLock")
     void finishSessionLocked(int sessionId, int uid, @AutofillCommitReason int commitReason) {
         if (!isEnabledLocked()) {
             Slog.wtf(TAG, "Service not enabled");
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 49ca297..930af5e 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -58,6 +58,7 @@
 import static com.android.server.autofill.Helper.sVerbose;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
@@ -685,6 +686,19 @@
     }
 
     /**
+     * Set views_fillable_total_count as long as mEventInternal presents.
+     */
+    public void maybeUpdateViewFillablesForRefillAttempt(List<AutofillId> autofillIds) {
+        mEventInternal.ifPresent(event -> {
+            // These autofill ids would be the ones being re-attempted.
+            event.mAutofillIdsAttemptedAutofill = new ArraySet<>(autofillIds);
+            // These autofill id's are being refilled, so they had failed previously.
+            // Note that these autofillIds correspond to the new autofill ids after relayout.
+            event.mFailedAutofillIds = new ArraySet<>(autofillIds);
+        });
+    }
+
+    /**
      * Set how many views are filtered from fill because they are not in current session
      */
     public void maybeSetFilteredFillableViewsCount(int filteredViewsCount) {
@@ -697,9 +711,16 @@
      * Set views_filled_failure_count using failure count as long as mEventInternal
      * presents.
      */
-    public void maybeSetViewFillFailureCounts(int failureCount) {
+    public void maybeSetViewFillFailureCounts(@NonNull List<AutofillId> ids, boolean isRefill) {
         mEventInternal.ifPresent(event -> {
-            event.mViewFillFailureCount = failureCount;
+            int failureCount = ids.size();
+            if (isRefill) {
+                event.mViewFailedOnRefillCount = failureCount;
+            } else {
+                event.mViewFillFailureCount = failureCount;
+                event.mViewFailedPriorToRefillCount = failureCount;
+                event.mFailedAutofillIds = new ArraySet<>(ids);
+            }
         });
     }
 
@@ -719,7 +740,7 @@
      * Set views_filled_failure_count using failure count as long as mEventInternal
      * presents.
      */
-    public void maybeAddSuccessId(AutofillId autofillId) {
+    public synchronized void maybeAddSuccessId(AutofillId autofillId) {
         mEventInternal.ifPresent(event -> {
             ArraySet<AutofillId> autofillIds = event.mAutofillIdsAttemptedAutofill;
             if (autofillIds == null) {
@@ -727,9 +748,21 @@
                         + " successfully filled");
                 event.mViewFilledButUnexpectedCount++;
             } else if (autofillIds.contains(autofillId)) {
-                if (sVerbose) {
-                    Slog.v(TAG, "Logging autofill for id:" + autofillId);
+                ArraySet<AutofillId> failedIds = event.mFailedAutofillIds;
+                if (failedIds.contains(autofillId)) {
+                    if (sVerbose) {
+                        Slog.v(TAG, "Logging autofill refill of id:" + autofillId);
+                    }
+                    // This indicates the success after refill attempt
+                    event.mViewFilledSuccessfullyOnRefillCount++;
+                    // Remove so if we don't reprocess duplicate requests
+                    failedIds.remove(autofillId);
+                } else {
+                    if (sVerbose) {
+                        Slog.v(TAG, "Logging autofill for id:" + autofillId);
+                    }
                 }
+                // Common actions to take irrespective of being filled by refill attempt or not.
                 event.mViewFillSuccessCount++;
                 autofillIds.remove(autofillId);
                 event.mAlreadyFilledAutofillIds.add(autofillId);
@@ -746,6 +779,23 @@
         });
     }
 
+    /**
+     * Set how many views are filtered from fill because they are not in current session
+     */
+    public void maybeSetNotifyNotExpiringResponseDuringAuth() {
+        mEventInternal.ifPresent(event -> {
+            event.mFixExpireResponseDuringAuthCount++;
+        });
+    }
+    /**
+     * Set how many views are filtered from fill because they are not in current session
+     */
+    public void notifyViewEnteredIgnoredDuringAuthCount() {
+        mEventInternal.ifPresent(event -> {
+            event.mNotifyViewEnteredIgnoredDuringAuthCount++;
+        });
+    }
+
     public void logAndEndEvent() {
         if (!mEventInternal.isPresent()) {
             Slog.w(TAG, "Shouldn't be logging AutofillPresentationEventReported again for same "
@@ -933,6 +983,7 @@
         int mNotifyViewEnteredIgnoredDuringAuthCount = 0;
 
         ArraySet<AutofillId> mAutofillIdsAttemptedAutofill;
+        ArraySet<AutofillId> mFailedAutofillIds = new ArraySet<>();
         ArraySet<AutofillId> mAlreadyFilledAutofillIds = new ArraySet<>();
 
         // Not logged - used for internal logic
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 21df7a5..b7508b4 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -5360,6 +5360,8 @@
             saveTriggerId = null;
         }
 
+        boolean hasAuthentication = (response.getAuthentication() != null);
+
         // Must also track that are part of datasets, otherwise the FillUI won't be hidden when
         // they go away (if they're not savable).
 
@@ -5379,6 +5381,9 @@
                         }
                     }
                 }
+                if (dataset.getAuthentication() != null) {
+                    hasAuthentication = true;
+                }
             }
         }
 
@@ -5390,7 +5395,7 @@
                         + " hasSaveInfo: " + (saveInfo != null));
             }
             mClient.setTrackedViews(id, toArray(trackedViews), mSaveOnAllViewsInvisible,
-                    saveOnFinish, toArray(fillableIds), saveTriggerId);
+                    saveOnFinish, toArray(fillableIds), saveTriggerId, hasAuthentication);
         } catch (RemoteException e) {
             Slog.w(TAG, "Cannot set tracked ids", e);
         }
@@ -5400,7 +5405,7 @@
      * Sets the state of views that failed to autofill.
      */
     @GuardedBy("mLock")
-    void setAutofillFailureLocked(@NonNull List<AutofillId> ids) {
+    void setAutofillFailureLocked(@NonNull List<AutofillId> ids, boolean isRefill) {
         if (sVerbose && !ids.isEmpty()) {
             Slog.v(TAG, "Total views that failed to populate: " + ids.size());
         }
@@ -5418,7 +5423,7 @@
                 Slog.v(TAG, "Changed state of " + id + " to " + viewState.getStateAsString());
             }
         }
-        mPresentationStatsEventLogger.maybeSetViewFillFailureCounts(ids.size());
+        mPresentationStatsEventLogger.maybeSetViewFillFailureCounts(ids, isRefill);
     }
 
     /**
@@ -5435,6 +5440,23 @@
         mPresentationStatsEventLogger.maybeAddSuccessId(id);
     }
 
+    /**
+     * Sets the state of views that failed to autofill.
+     */
+    void setNotifyNotExpiringResponseDuringAuth() {
+        synchronized (mLock) {
+            mPresentationStatsEventLogger.maybeSetNotifyNotExpiringResponseDuringAuth();
+        }
+    }
+    /**
+     * Sets the state of views that failed to autofill.
+     */
+    void setLogViewEnteredIgnoredDuringAuth() {
+        synchronized (mLock) {
+            mPresentationStatsEventLogger.notifyViewEnteredIgnoredDuringAuthCount();
+        }
+    }
+
     @GuardedBy("mLock")
     private void replaceResponseLocked(@NonNull FillResponse oldResponse,
             @NonNull FillResponse newResponse, @Nullable Bundle newClientState) {
@@ -6665,6 +6687,11 @@
         }
     }
 
+    @GuardedBy("mLock")
+    public void setAutofillIdsAttemptedForRefillLocked(@NonNull List<AutofillId> ids) {
+        mPresentationStatsEventLogger.maybeUpdateViewFillablesForRefillAttempt(ids);
+    }
+
     private AutoFillUI getUiForShowing() {
         synchronized (mLock) {
             mUi.setCallback(this);
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 0fa5260..2e1416b 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -6653,9 +6653,10 @@
 
             // If unbound while waiting to start and there is no connection left in this service,
             // remove the pending service
-            if (s.getConnections().isEmpty()) {
+            if (s.getConnections().isEmpty() && !s.startRequested) {
                 mPendingServices.remove(s);
                 mPendingBringups.remove(s);
+                if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Removed pending service: " + s);
             }
 
             if (c.hasFlag(Context.BIND_AUTO_CREATE)) {
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index e46ab8f..03fbfd37 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -144,6 +144,7 @@
 import com.android.server.power.stats.PowerStatsStore;
 import com.android.server.power.stats.PowerStatsUidResolver;
 import com.android.server.power.stats.ScreenPowerStatsProcessor;
+import com.android.server.power.stats.SensorPowerStatsProcessor;
 import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
 import com.android.server.power.stats.VideoPowerStatsProcessor;
 import com.android.server.power.stats.WifiPowerStatsProcessor;
@@ -595,6 +596,17 @@
                 .setProcessor(
                         new GnssPowerStatsProcessor(mPowerProfile, mPowerStatsUidResolver));
 
+        config.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_SENSORS)
+                .trackDeviceStates(
+                        AggregatedPowerStatsConfig.STATE_POWER,
+                        AggregatedPowerStatsConfig.STATE_SCREEN)
+                .trackUidStates(
+                        AggregatedPowerStatsConfig.STATE_POWER,
+                        AggregatedPowerStatsConfig.STATE_SCREEN,
+                        AggregatedPowerStatsConfig.STATE_PROCESS_STATE)
+                .setProcessor(new SensorPowerStatsProcessor(
+                        () -> mContext.getSystemService(SensorManager.class)));
+
         config.trackCustomPowerComponents(CustomEnergyConsumerPowerStatsProcessor::new)
                 .trackDeviceStates(
                         AggregatedPowerStatsConfig.STATE_POWER,
@@ -706,6 +718,10 @@
                 BatteryConsumer.POWER_COMPONENT_GNSS,
                 Flags.streamlinedMiscBatteryStats());
 
+        mBatteryUsageStatsProvider.setPowerStatsExporterEnabled(
+                BatteryConsumer.POWER_COMPONENT_SENSORS,
+                Flags.streamlinedMiscBatteryStats());
+
         mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CAMERA,
                 Flags.streamlinedMiscBatteryStats());
         mBatteryUsageStatsProvider.setPowerStatsExporterEnabled(
diff --git a/services/core/java/com/android/server/am/PendingIntentController.java b/services/core/java/com/android/server/am/PendingIntentController.java
index f336120..3b0147c 100644
--- a/services/core/java/com/android/server/am/PendingIntentController.java
+++ b/services/core/java/com/android/server/am/PendingIntentController.java
@@ -149,21 +149,6 @@
                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
             }
 
-            if (opts != null && opts.isPendingIntentBackgroundActivityLaunchAllowedByPermission()) {
-                Slog.wtf(TAG,
-                        "Resetting option pendingIntentBackgroundActivityLaunchAllowedByPermission"
-                                + " which is set by the pending intent creator ("
-                                + packageName
-                                + ") because this option is meant for the pending intent sender");
-                if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK,
-                        callingUid)) {
-                    throw new IllegalArgumentException(
-                            "pendingIntentBackgroundActivityLaunchAllowedByPermission "
-                                    + "can not be set by creator of a PendingIntent");
-                }
-                opts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(false);
-            }
-
             final boolean noCreate = (flags & PendingIntent.FLAG_NO_CREATE) != 0;
             final boolean cancelCurrent = (flags & PendingIntent.FLAG_CANCEL_CURRENT) != 0;
             final boolean updateCurrent = (flags & PendingIntent.FLAG_UPDATE_CURRENT) != 0;
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7f43fae..3123268 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -174,6 +174,7 @@
         "haptics",
         "hardware_backed_security_mainline",
         "input",
+        "incremental",
         "llvm_and_toolchains",
         "lse_desktop_experience",
         "machine_learning",
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
index 8cf47d0..430be03 100644
--- a/services/core/java/com/android/server/appop/AttributedOp.java
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
@@ -109,7 +109,7 @@
                 uidState, flags);
 
         mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
-                parent.packageName, tag, uidState, flags, accessTime,
+                parent.packageName, persistentDeviceId, tag, uidState, flags, accessTime,
                 AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
     }
 
@@ -253,8 +253,8 @@
 
         if (isStarted) {
             mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
-                    parent.packageName, tag, uidState, flags, startTime, attributionFlags,
-                    attributionChainId);
+                    parent.packageName, persistentDeviceId, tag, uidState, flags, startTime,
+                    attributionFlags, attributionChainId);
         }
     }
 
@@ -333,7 +333,7 @@
                     finishedEvent);
 
             mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, parent.uid,
-                    parent.packageName, tag, event.getUidState(),
+                    parent.packageName, persistentDeviceId, tag, event.getUidState(),
                     event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(),
                     event.getAttributionFlags(), event.getAttributionChainId());
 
@@ -441,8 +441,9 @@
             event.setStartElapsedTime(SystemClock.elapsedRealtime());
             event.setStartTime(startTime);
             mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
-                    parent.packageName, tag, event.getUidState(), event.getFlags(), startTime,
-                    event.getAttributionFlags(), event.getAttributionChainId());
+                    parent.packageName, persistentDeviceId, tag, event.getUidState(),
+                    event.getFlags(), startTime, event.getAttributionFlags(),
+                    event.getAttributionChainId());
             if (shouldSendActive) {
                 mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
                         parent.packageName, tag, event.getVirtualDeviceId(), true,
diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java
index 5d83ad6..539dbca 100644
--- a/services/core/java/com/android/server/appop/DiscreteRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java
@@ -41,6 +41,7 @@
 import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING;
 import static android.app.AppOpsManager.flagsToString;
 import static android.app.AppOpsManager.getUidStateName;
+import static android.companion.virtual.VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT;
 
 import static java.lang.Long.min;
 import static java.lang.Math.max;
@@ -52,6 +53,7 @@
 import android.os.Build;
 import android.os.Environment;
 import android.os.FileUtils;
+import android.permission.flags.Flags;
 import android.provider.DeviceConfig;
 import android.util.ArrayMap;
 import android.util.AtomicFile;
@@ -76,7 +78,7 @@
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 import java.util.Objects;
@@ -164,6 +166,8 @@
     private static final String TAG_OP = "o";
     private static final String ATTR_OP_ID = "op";
 
+    private static final String ATTR_DEVICE_ID = "di";
+
     private static final String TAG_TAG = "a";
     private static final String ATTR_TAG = "at";
 
@@ -196,11 +200,14 @@
     private boolean mDebugMode = false;
 
     DiscreteRegistry(Object inMemoryLock) {
+        this(inMemoryLock, new File(new File(Environment.getDataSystemDirectory(), "appops"),
+                "discrete"));
+    }
+
+    DiscreteRegistry(Object inMemoryLock, File discreteAccessDir) {
         mInMemoryLock = inMemoryLock;
         synchronized (mOnDiskLock) {
-            mDiscreteAccessDir = new File(
-                    new File(Environment.getDataSystemDirectory(), "appops"),
-                    "discrete");
+            mDiscreteAccessDir = discreteAccessDir;
             createDiscreteAccessDirLocked();
             int largestChainId = readLargestChainIdFromDiskLocked();
             synchronized (mInMemoryLock) {
@@ -245,16 +252,24 @@
                 DEFAULT_DISCRETE_OPS);
     }
 
-    void recordDiscreteAccess(int uid, String packageName, int op, @Nullable String attributionTag,
-            @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, long accessTime,
-            long accessDuration, @AppOpsManager.AttributionFlags int attributionFlags,
-            int attributionChainId) {
+    void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op,
+            @Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
+            @AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
+            @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
         if (!isDiscreteOp(op, flags)) {
             return;
         }
         synchronized (mInMemoryLock) {
-            mDiscreteOps.addDiscreteAccess(op, uid, packageName, attributionTag, flags, uidState,
-                    accessTime, accessDuration, attributionFlags, attributionChainId);
+            if (Flags.deviceAwareAppOpNewSchemaEnabled()) {
+                mDiscreteOps.addDiscreteAccess(op, uid, packageName, deviceId, attributionTag,
+                        flags, uidState, accessTime, accessDuration, attributionFlags,
+                        attributionChainId);
+            } else {
+                mDiscreteOps.addDiscreteAccess(op, uid, packageName, PERSISTENT_DEVICE_ID_DEFAULT,
+                        attributionTag, flags, uidState, accessTime, accessDuration,
+                        attributionFlags, attributionChainId);
+            }
+
         }
     }
 
@@ -294,7 +309,6 @@
         discreteOps.filter(beginTimeMillis, endTimeMillis, filter, uidFilter, packageNameFilter,
                 opNamesFilter, attributionTagFilter, flagsFilter, attributionChains);
         discreteOps.applyToHistoricalOps(result, attributionChains);
-        return;
     }
 
     private int readLargestChainIdFromDiskLocked() {
@@ -355,28 +369,36 @@
                 String pkg = pkgs.keyAt(pkgNum);
                 int nOps = ops.size();
                 for (int opNum = 0; opNum < nOps; opNum++) {
-                    ArrayMap<String, List<DiscreteOpEvent>> attrOps =
-                            ops.valueAt(opNum).mAttributedOps;
                     int op = ops.keyAt(opNum);
-                    int nAttrOps = attrOps.size();
-                    for (int attrOpNum = 0; attrOpNum < nAttrOps; attrOpNum++) {
-                        List<DiscreteOpEvent> opEvents = attrOps.valueAt(attrOpNum);
-                        String attributionTag = attrOps.keyAt(attrOpNum);
-                        int nOpEvents = opEvents.size();
-                        for (int opEventNum = 0; opEventNum < nOpEvents; opEventNum++) {
-                            DiscreteOpEvent event = opEvents.get(opEventNum);
-                            if (event == null
-                                    || event.mAttributionChainId == ATTRIBUTION_CHAIN_ID_NONE
-                                    || (event.mAttributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) {
-                                continue;
-                            }
+                    ArrayMap<String, DiscreteDeviceOp> deviceOps =
+                            ops.valueAt(opNum).mDeviceAttributedOps;
 
-                            if (!chains.containsKey(event.mAttributionChainId)) {
-                                chains.put(event.mAttributionChainId,
-                                        new AttributionChain(attributionExemptPkgs));
+                    int nDeviceOps = deviceOps.size();
+                    for (int deviceNum = 0; deviceNum < nDeviceOps; deviceNum++) {
+                        ArrayMap<String, List<DiscreteOpEvent>> attrOps =
+                                deviceOps.valueAt(deviceNum).mAttributedOps;
+
+                        int nAttrOps = attrOps.size();
+                        for (int attrOpNum = 0; attrOpNum < nAttrOps; attrOpNum++) {
+                            List<DiscreteOpEvent> opEvents = attrOps.valueAt(attrOpNum);
+                            String attributionTag = attrOps.keyAt(attrOpNum);
+                            int nOpEvents = opEvents.size();
+                            for (int opEventNum = 0; opEventNum < nOpEvents; opEventNum++) {
+                                DiscreteOpEvent event = opEvents.get(opEventNum);
+                                if (event == null
+                                        || event.mAttributionChainId == ATTRIBUTION_CHAIN_ID_NONE
+                                        || (event.mAttributionFlags & ATTRIBUTION_FLAG_TRUSTED)
+                                                == 0) {
+                                    continue;
+                                }
+
+                                if (!chains.containsKey(event.mAttributionChainId)) {
+                                    chains.put(event.mAttributionChainId,
+                                            new AttributionChain(attributionExemptPkgs));
+                                }
+                                chains.get(event.mAttributionChainId)
+                                        .addEvent(pkg, uid, attributionTag, op, event);
                             }
-                            chains.get(event.mAttributionChainId)
-                                    .addEvent(pkg, uid, attributionTag, op, event);
                         }
                     }
                 }
@@ -464,7 +486,7 @@
         createDiscreteAccessDir();
     }
 
-    private DiscreteOps getAllDiscreteOps() {
+    DiscreteOps getAllDiscreteOps() {
         DiscreteOps discreteOps = new DiscreteOps(0);
 
         synchronized (mOnDiskLock) {
@@ -608,7 +630,7 @@
         }
     }
 
-    private final class DiscreteOps {
+    static final class DiscreteOps {
         ArrayMap<Integer, DiscreteUidOps> mUids;
         int mChainIdOffset;
         int mLargestChainId;
@@ -634,8 +656,9 @@
         }
 
         void addDiscreteAccess(int op, int uid, @NonNull String packageName,
-                @Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
-                @AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
+                @NonNull String deviceId, @Nullable String attributionTag,
+                @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState,
+                long accessTime, long accessDuration,
                 @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
             int offsetChainId = attributionChainId;
             if (attributionChainId != ATTRIBUTION_CHAIN_ID_NONE) {
@@ -649,8 +672,9 @@
                     mChainIdOffset = -1 * attributionChainId;
                 }
             }
-            getOrCreateDiscreteUidOps(uid).addDiscreteAccess(op, packageName, attributionTag, flags,
-                    uidState, accessTime, accessDuration, attributionFlags, offsetChainId);
+            getOrCreateDiscreteUidOps(uid).addDiscreteAccess(op, packageName, deviceId,
+                    attributionTag, flags, uidState, accessTime, accessDuration, attributionFlags,
+                    offsetChainId);
         }
 
         private void filter(long beginTimeMillis, long endTimeMillis,
@@ -837,7 +861,7 @@
         }
     }
 
-    private final class DiscreteUidOps {
+    static final class DiscreteUidOps {
         ArrayMap<String, DiscretePackageOps> mPackages;
 
         DiscreteUidOps() {
@@ -889,12 +913,13 @@
             mPackages.remove(packageName);
         }
 
-        void addDiscreteAccess(int op, @NonNull String packageName, @Nullable String attributionTag,
-                @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState,
-                long accessTime, long accessDuration,
+        void addDiscreteAccess(int op, @NonNull String packageName, @NonNull String deviceId,
+                @Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
+                @AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
                 @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
-            getOrCreateDiscretePackageOps(packageName).addDiscreteAccess(op, attributionTag, flags,
-                    uidState, accessTime, accessDuration, attributionFlags, attributionChainId);
+            getOrCreateDiscretePackageOps(packageName).addDiscreteAccess(op, deviceId,
+                    attributionTag, flags, uidState, accessTime, accessDuration,
+                    attributionFlags, attributionChainId);
         }
 
         private DiscretePackageOps getOrCreateDiscretePackageOps(String packageName) {
@@ -948,7 +973,7 @@
         }
     }
 
-    private final class DiscretePackageOps {
+    static final class DiscretePackageOps {
         ArrayMap<Integer, DiscreteOp> mPackageOps;
 
         DiscretePackageOps() {
@@ -959,12 +984,12 @@
             return mPackageOps.isEmpty();
         }
 
-        void addDiscreteAccess(int op, @Nullable String attributionTag,
+        void addDiscreteAccess(int op, @NonNull String deviceId, @Nullable String attributionTag,
                 @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState,
                 long accessTime, long accessDuration,
                 @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
-            getOrCreateDiscreteOp(op).addDiscreteAccess(attributionTag, flags, uidState, accessTime,
-                    accessDuration, attributionFlags, attributionChainId);
+            getOrCreateDiscreteOp(op).addDiscreteAccess(deviceId, attributionTag, flags, uidState,
+                    accessTime, accessDuration, attributionFlags, attributionChainId);
         }
 
         void merge(DiscretePackageOps other) {
@@ -1056,10 +1081,148 @@
         }
     }
 
-    private final class DiscreteOp {
-        ArrayMap<String, List<DiscreteOpEvent>> mAttributedOps;
+    static final class DiscreteOp {
+        ArrayMap<String, DiscreteDeviceOp> mDeviceAttributedOps;
 
         DiscreteOp() {
+            mDeviceAttributedOps = new ArrayMap<>();
+        }
+
+        boolean isEmpty() {
+            return mDeviceAttributedOps.isEmpty();
+        }
+
+        void merge(DiscreteOp other) {
+            int nDevices = other.mDeviceAttributedOps.size();
+            for (int i = 0; i < nDevices; i++) {
+                String deviceId = other.mDeviceAttributedOps.keyAt(i);
+                DiscreteDeviceOp otherDeviceOps = other.mDeviceAttributedOps.valueAt(i);
+                getOrCreateDiscreteDeviceOp(deviceId).merge(otherDeviceOps);
+            }
+        }
+
+        // Note: Update this method when we want to filter by device Id.
+        private void filter(long beginTimeMillis, long endTimeMillis,
+                @AppOpsManager.HistoricalOpsRequestFilter int filter,
+                @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter,
+                int currentUid, String currentPkgName, int currentOp,
+                ArrayMap<Integer, AttributionChain> attributionChains) {
+            int nDevices = mDeviceAttributedOps.size();
+            for (int i = nDevices - 1; i >= 0; i--) {
+                mDeviceAttributedOps.valueAt(i).filter(beginTimeMillis, endTimeMillis, filter,
+                        attributionTagFilter, flagsFilter, currentUid, currentPkgName, currentOp,
+                        attributionChains);
+                if (mDeviceAttributedOps.valueAt(i).isEmpty()) {
+                    mDeviceAttributedOps.removeAt(i);
+                }
+            }
+        }
+
+        private void offsetHistory(long offset) {
+            int nDevices = mDeviceAttributedOps.size();
+            for (int i = 0; i < nDevices; i++) {
+                mDeviceAttributedOps.valueAt(i).offsetHistory(offset);
+            }
+        }
+
+        void addDiscreteAccess(@NonNull String deviceId, @Nullable String attributionTag,
+                @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState,
+                long accessTime, long accessDuration,
+                @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
+            getOrCreateDiscreteDeviceOp(deviceId).addDiscreteAccess(attributionTag, flags, uidState,
+                    accessTime, accessDuration, attributionFlags, attributionChainId);
+        }
+
+        private DiscreteDeviceOp getOrCreateDiscreteDeviceOp(String deviceId) {
+            return mDeviceAttributedOps.computeIfAbsent(deviceId, k -> new DiscreteDeviceOp());
+        }
+
+        // TODO: b/308716962 Retrieve discrete histories from all devices and integrate them with
+        // HistoricalOps
+        private void applyToHistory(AppOpsManager.HistoricalOps result, int uid,
+                @NonNull String packageName, int op,
+                @NonNull ArrayMap<Integer, AttributionChain> attributionChains) {
+            if (mDeviceAttributedOps.get(PERSISTENT_DEVICE_ID_DEFAULT) != null) {
+                mDeviceAttributedOps.get(PERSISTENT_DEVICE_ID_DEFAULT).applyToHistory(result, uid,
+                        packageName, op, attributionChains);
+            }
+        }
+
+        private void dump(@NonNull PrintWriter pw, @NonNull SimpleDateFormat sdf,
+                @NonNull Date date, @NonNull String prefix, int nDiscreteOps) {
+            int nDevices = mDeviceAttributedOps.size();
+            for (int i = 0; i < nDevices; i++) {
+                pw.print(prefix);
+                pw.print("Device: ");
+                pw.print(mDeviceAttributedOps.keyAt(i));
+                pw.println();
+                mDeviceAttributedOps.valueAt(i).dump(pw, sdf, date, prefix + "  ",
+                        nDiscreteOps);
+            }
+        }
+
+        void serialize(TypedXmlSerializer out) throws Exception {
+            int nDevices = mDeviceAttributedOps.size();
+            for (int i = 0; i < nDevices; i++) {
+                String deviceId = mDeviceAttributedOps.keyAt(i);
+                mDeviceAttributedOps.valueAt(i).serialize(out, deviceId);
+            }
+        }
+
+        void deserialize(TypedXmlPullParser parser, long beginTimeMillis) throws Exception {
+            int outerDepth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                if (TAG_TAG.equals(parser.getName())) {
+                    String attributionTag = parser.getAttributeValue(null, ATTR_TAG);
+
+                    int innerDepth = parser.getDepth();
+                    while (XmlUtils.nextElementWithin(parser, innerDepth)) {
+                        if (TAG_ENTRY.equals(parser.getName())) {
+                            long noteTime = parser.getAttributeLong(null, ATTR_NOTE_TIME);
+                            long noteDuration = parser.getAttributeLong(null, ATTR_NOTE_DURATION,
+                                    -1);
+                            int uidState = parser.getAttributeInt(null, ATTR_UID_STATE);
+                            int opFlags = parser.getAttributeInt(null, ATTR_FLAGS);
+                            int attributionFlags = parser.getAttributeInt(null,
+                                    ATTR_ATTRIBUTION_FLAGS, AppOpsManager.ATTRIBUTION_FLAGS_NONE);
+                            int attributionChainId = parser.getAttributeInt(null, ATTR_CHAIN_ID,
+                                    AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
+                            String deviceId = parser.getAttributeValue(null, ATTR_DEVICE_ID);
+                            if (deviceId == null) {
+                                deviceId = PERSISTENT_DEVICE_ID_DEFAULT;
+                            }
+                            if (noteTime + noteDuration < beginTimeMillis) {
+                                continue;
+                            }
+
+                            DiscreteDeviceOp deviceOps = getOrCreateDiscreteDeviceOp(deviceId);
+                            List<DiscreteOpEvent> events =
+                                    deviceOps.getOrCreateDiscreteOpEventsList(attributionTag);
+                            DiscreteOpEvent event = new DiscreteOpEvent(noteTime, noteDuration,
+                                    uidState, opFlags, attributionFlags, attributionChainId);
+                            events.add(event);
+                        }
+                    }
+                }
+            }
+
+            int nDeviceOps = mDeviceAttributedOps.size();
+            for (int i = 0; i < nDeviceOps; i++) {
+                DiscreteDeviceOp deviceOp = mDeviceAttributedOps.valueAt(i);
+
+                int nAttrOps = deviceOp.mAttributedOps.size();
+                for (int j = 0; j < nAttrOps; j++) {
+                    List<DiscreteOpEvent> events = deviceOp.mAttributedOps.valueAt(j);
+                    events.sort(Comparator.comparingLong(a -> a.mNoteTime));
+                }
+            }
+        }
+    }
+
+    static final class DiscreteDeviceOp {
+        ArrayMap<String, List<DiscreteOpEvent>> mAttributedOps;
+
+        DiscreteDeviceOp() {
             mAttributedOps = new ArrayMap<>();
         }
 
@@ -1067,7 +1230,7 @@
             return mAttributedOps.isEmpty();
         }
 
-        void merge(DiscreteOp other) {
+        void merge(DiscreteDeviceOp other) {
             int nTags = other.mAttributedOps.size();
             for (int i = 0; i < nTags; i++) {
                 String tag = other.mAttributedOps.keyAt(i);
@@ -1097,7 +1260,7 @@
                         currentUid, currentPkgName, currentOp, mAttributedOps.keyAt(i),
                         attributionChains);
                 mAttributedOps.put(tag, list);
-                if (list.size() == 0) {
+                if (list.isEmpty()) {
                     mAttributedOps.removeAt(i);
                 }
             }
@@ -1125,8 +1288,7 @@
             List<DiscreteOpEvent> attributedOps = getOrCreateDiscreteOpEventsList(
                     attributionTag);
 
-            int nAttributedOps = attributedOps.size();
-            int i = nAttributedOps;
+            int i = attributedOps.size();
             for (; i > 0; i--) {
                 DiscreteOpEvent previousOp = attributedOps.get(i - 1);
                 if (discretizeTimeStamp(previousOp.mNoteTime) < discretizeTimeStamp(accessTime)) {
@@ -1148,12 +1310,8 @@
         }
 
         private List<DiscreteOpEvent> getOrCreateDiscreteOpEventsList(String attributionTag) {
-            List<DiscreteOpEvent> result = mAttributedOps.get(attributionTag);
-            if (result == null) {
-                result = new ArrayList<>();
-                mAttributedOps.put(attributionTag, result);
-            }
-            return result;
+            return mAttributedOps.computeIfAbsent(attributionTag,
+                    k -> new ArrayList<>());
         }
 
         private void applyToHistory(AppOpsManager.HistoricalOps result, int uid,
@@ -1167,8 +1325,7 @@
                 for (int j = 0; j < nEvents; j++) {
                     DiscreteOpEvent event = events.get(j);
                     AppOpsManager.OpEventProxyInfo proxy = null;
-                    if (event.mAttributionChainId != ATTRIBUTION_CHAIN_ID_NONE
-                            && attributionChains != null) {
+                    if (event.mAttributionChainId != ATTRIBUTION_CHAIN_ID_NONE) {
                         AttributionChain chain = attributionChains.get(event.mAttributionChainId);
                         if (chain != null && chain.isComplete()
                                 && chain.isStart(packageName, uid, tag, op, event)
@@ -1198,65 +1355,31 @@
                 int first = nDiscreteOps < 1 ? 0 : max(0, nOps - nDiscreteOps);
                 for (int j = first; j < nOps; j++) {
                     ops.get(j).dump(pw, sdf, date, prefix + "  ");
-
                 }
             }
         }
 
-        void serialize(TypedXmlSerializer out) throws Exception {
+        void serialize(TypedXmlSerializer out, String deviceId) throws Exception {
             int nAttributions = mAttributedOps.size();
             for (int i = 0; i < nAttributions; i++) {
                 out.startTag(null, TAG_TAG);
                 String tag = mAttributedOps.keyAt(i);
                 if (tag != null) {
-                    out.attribute(null, ATTR_TAG, mAttributedOps.keyAt(i));
+                    out.attribute(null, ATTR_TAG, tag);
                 }
                 List<DiscreteOpEvent> ops = mAttributedOps.valueAt(i);
                 int nOps = ops.size();
                 for (int j = 0; j < nOps; j++) {
                     out.startTag(null, TAG_ENTRY);
-                    ops.get(j).serialize(out);
+                    ops.get(j).serialize(out, deviceId);
                     out.endTag(null, TAG_ENTRY);
                 }
                 out.endTag(null, TAG_TAG);
             }
         }
-
-        void deserialize(TypedXmlPullParser parser, long beginTimeMillis) throws Exception {
-            int outerDepth = parser.getDepth();
-            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
-                if (TAG_TAG.equals(parser.getName())) {
-                    String attributionTag = parser.getAttributeValue(null, ATTR_TAG);
-                    List<DiscreteOpEvent> events = getOrCreateDiscreteOpEventsList(
-                            attributionTag);
-                    int innerDepth = parser.getDepth();
-                    while (XmlUtils.nextElementWithin(parser, innerDepth)) {
-                        if (TAG_ENTRY.equals(parser.getName())) {
-                            long noteTime = parser.getAttributeLong(null, ATTR_NOTE_TIME);
-                            long noteDuration = parser.getAttributeLong(null, ATTR_NOTE_DURATION,
-                                    -1);
-                            int uidState = parser.getAttributeInt(null, ATTR_UID_STATE);
-                            int opFlags = parser.getAttributeInt(null, ATTR_FLAGS);
-                            int attributionFlags = parser.getAttributeInt(null,
-                                    ATTR_ATTRIBUTION_FLAGS, AppOpsManager.ATTRIBUTION_FLAGS_NONE);
-                            int attributionChainId = parser.getAttributeInt(null, ATTR_CHAIN_ID,
-                                    AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
-                            if (noteTime + noteDuration < beginTimeMillis) {
-                                continue;
-                            }
-                            DiscreteOpEvent event = new DiscreteOpEvent(noteTime, noteDuration,
-                                    uidState, opFlags, attributionFlags, attributionChainId);
-                            events.add(event);
-                        }
-                    }
-                    Collections.sort(events, (a, b) -> a.mNoteTime < b.mNoteTime ? -1
-                            : (a.mNoteTime == b.mNoteTime ? 0 : 1));
-                }
-            }
-        }
     }
 
-    private final class DiscreteOpEvent {
+    static final class DiscreteOpEvent {
         final long mNoteTime;
         final long mNoteDuration;
         final @AppOpsManager.UidState int mUidState;
@@ -1306,7 +1429,7 @@
             pw.println();
         }
 
-        private void serialize(TypedXmlSerializer out) throws Exception {
+        private void serialize(TypedXmlSerializer out, String deviceId) throws Exception {
             out.attributeLong(null, ATTR_NOTE_TIME, mNoteTime);
             if (mNoteDuration != -1) {
                 out.attributeLong(null, ATTR_NOTE_DURATION, mNoteDuration);
@@ -1317,6 +1440,9 @@
             if (mAttributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE) {
                 out.attributeInt(null, ATTR_CHAIN_ID, mAttributionChainId);
             }
+            if (!Objects.equals(deviceId, PERSISTENT_DEVICE_ID_DEFAULT)) {
+                out.attribute(null, ATTR_DEVICE_ID, deviceId);
+            }
             out.attributeInt(null, ATTR_UID_STATE, mUidState);
             out.attributeInt(null, ATTR_FLAGS, mOpFlag);
         }
diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java
index dbd47d0..fffb108 100644
--- a/services/core/java/com/android/server/appop/HistoricalRegistry.java
+++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java
@@ -472,9 +472,9 @@
     }
 
     void incrementOpAccessedCount(int op, int uid, @NonNull String packageName,
-            @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags,
-            long accessTime, @AppOpsManager.AttributionFlags int attributionFlags,
-            int attributionChainId) {
+            @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState,
+            @OpFlags int flags, long accessTime,
+            @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
         synchronized (mInMemoryLock) {
             if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
                 if (!isPersistenceInitializedMLocked()) {
@@ -485,8 +485,9 @@
                         System.currentTimeMillis()).increaseAccessCount(op, uid, packageName,
                         attributionTag, uidState, flags, 1);
 
-                mDiscreteRegistry.recordDiscreteAccess(uid, packageName, op, attributionTag,
-                        flags, uidState, accessTime, -1, attributionFlags, attributionChainId);
+                mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op,
+                        attributionTag, flags, uidState, accessTime, -1, attributionFlags,
+                        attributionChainId);
             }
         }
     }
@@ -507,8 +508,8 @@
     }
 
     void increaseOpAccessDuration(int op, int uid, @NonNull String packageName,
-            @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags,
-            long eventStartTime, long increment,
+            @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState,
+            @OpFlags int flags, long eventStartTime, long increment,
             @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
         synchronized (mInMemoryLock) {
             if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
@@ -519,9 +520,9 @@
                 getUpdatedPendingHistoricalOpsMLocked(
                         System.currentTimeMillis()).increaseAccessDuration(op, uid, packageName,
                         attributionTag, uidState, flags, increment);
-                mDiscreteRegistry.recordDiscreteAccess(uid, packageName, op, attributionTag,
-                        flags, uidState, eventStartTime, increment, attributionFlags,
-                        attributionChainId);
+                mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op,
+                        attributionTag, flags, uidState, eventStartTime, increment,
+                        attributionFlags, attributionChainId);
             }
         }
     }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index ca69f31..8d8a54e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -31,6 +31,7 @@
 import static android.media.audio.Flags.automaticBtDeviceType;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.media.audio.Flags.asDeviceConnectionFailure;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -529,6 +530,17 @@
         }
     };
 
+    /**
+     * package-protected for unit testing only
+     * Returns the currently connected devices
+     * @return the collection of connected devices
+     */
+    /*package*/ @NonNull Collection<DeviceInfo> getConnectedDevices() {
+        synchronized (mDevicesLock) {
+            return mConnectedDevices.values();
+        }
+    }
+
     // List of devices actually connected to AudioPolicy (through AudioSystem), only one
     // by device type, which is used as the key, value is the DeviceInfo generated key.
     // For the moment only for A2DP sink devices.
@@ -598,8 +610,9 @@
     /**
      * Class to store info about connected devices.
      * Use makeDeviceListKey() to make a unique key for this list.
+     * Package-protected for unit tests
      */
-    private static class DeviceInfo {
+    /*package*/ static class DeviceInfo {
         final int mDeviceType;
         final @NonNull String mDeviceName;
         final @NonNull String mDeviceAddress;
@@ -762,13 +775,27 @@
     // Always executed on AudioDeviceBroker message queue
     /*package*/ void onRestoreDevices() {
         synchronized (mDevicesLock) {
+            int res;
+            List<DeviceInfo> failedReconnectionDeviceList = new ArrayList<>(/*initialCapacity*/ 0);
             //TODO iterate on mApmConnectedDevices instead once it handles all device types
             for (DeviceInfo di : mConnectedDevices.values()) {
-                mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(di.mDeviceType,
+                res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
+                        di.mDeviceType,
                         di.mDeviceAddress,
                         di.mDeviceName),
                         AudioSystem.DEVICE_STATE_AVAILABLE,
                         di.mDeviceCodecFormat);
+                if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
+                    failedReconnectionDeviceList.add(di);
+                }
+            }
+            if (asDeviceConnectionFailure()) {
+                for (DeviceInfo di : failedReconnectionDeviceList) {
+                    AudioService.sDeviceLogger.enqueueAndSlog(
+                            "Device inventory restore failed to reconnect " + di,
+                            EventLogger.Event.ALOGE, TAG);
+                    mConnectedDevices.remove(di.getKey(), di);
+                }
             }
             mAppliedStrategyRolesInt.clear();
             mAppliedPresetRolesInt.clear();
@@ -2070,8 +2097,9 @@
                     "APM failed to make available A2DP device addr="
                             + Utils.anonymizeBluetoothAddress(address)
                             + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-            // TODO: connection failed, stop here
-            // TODO: return;
+            if (asDeviceConnectionFailure()) {
+                return;
+            }
         } else {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "A2DP sink device addr=" + Utils.anonymizeBluetoothAddress(address)
@@ -2336,8 +2364,7 @@
                     "APM failed to make unavailable A2DP device addr="
                             + Utils.anonymizeBluetoothAddress(address)
                             + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-            // TODO:  failed to disconnect, stop here
-            // TODO: return;
+            // not taking further action: proceeding as if disconnection from APM worked
         } else {
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                     "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
@@ -2383,8 +2410,9 @@
                     "APM failed to make available A2DP source device addr="
                             + Utils.anonymizeBluetoothAddress(address)
                             + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-            // TODO: connection failed, stop here
-            // TODO: return
+            if (asDeviceConnectionFailure()) {
+                return;
+            }
         } else {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address)
@@ -2402,6 +2430,7 @@
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
+        // always remove regardless of the result
         mConnectedDevices.remove(
                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address));
         mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
@@ -2418,9 +2447,18 @@
 
         AudioDeviceAttributes ada = new AudioDeviceAttributes(
                 DEVICE_OUT_HEARING_AID, address, name);
-        mAudioSystem.setDeviceConnectionState(ada,
+        final int res = mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_AVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
+        if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
+            AudioService.sDeviceLogger.enqueueAndSlog(
+                    "APM failed to make available HearingAid addr=" + address
+                            + " error=" + res,
+                    EventLogger.Event.ALOGE, TAG);
+            return;
+        }
+        AudioService.sDeviceLogger.enqueueAndSlog("HearingAid made available addr=" + address,
+                EventLogger.Event.ALOGI, TAG);
         mConnectedDevices.put(
                 DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address),
                 new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address));
@@ -2447,6 +2485,7 @@
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
+        // always remove regardless of return code
         mConnectedDevices.remove(
                 DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address));
         // Remove Hearing Aid routes as well
@@ -2540,11 +2579,12 @@
             final int res = mAudioSystem.setDeviceConnectionState(ada,
                     AudioSystem.DEVICE_STATE_AVAILABLE, codec);
             if (res != AudioSystem.AUDIO_STATUS_OK) {
-                AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+                AudioService.sDeviceLogger.enqueueAndSlog(
                         "APM failed to make available LE Audio device addr=" + address
-                                + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-                // TODO: connection failed, stop here
-                // TODO: return;
+                                + " error=" + res, EventLogger.Event.ALOGE, TAG);
+                if (asDeviceConnectionFailure()) {
+                    return;
+                }
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "LE Audio " + (AudioSystem.isInputDevice(device) ? "source" : "sink")
@@ -2596,8 +2636,7 @@
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "APM failed to make unavailable LE Audio device addr=" + address
                                 + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-                // TODO:  failed to disconnect, stop here
-                // TODO: return;
+                // not taking further action: proceeding as if disconnection from APM worked
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1183768..ac43e86 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -63,6 +63,7 @@
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.media.audio.Flags.absVolumeIndexFix;
 import static com.android.media.audio.Flags.alarmMinVolumeZero;
+import static com.android.media.audio.Flags.asDeviceConnectionFailure;
 import static com.android.media.audio.Flags.audioserverPermissions;
 import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
 import static com.android.media.audio.Flags.replaceStreamBtSco;
@@ -4306,7 +4307,8 @@
         super.getVolumeGroupVolumeIndex_enforcePermission();
         synchronized (VolumeStreamState.class) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
-                throw new IllegalArgumentException("No volume group for id " + groupId);
+                Log.e(TAG, "No volume group for id " + groupId);
+                return 0;
             }
             VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
             // Return 0 when muted, not min index since for e.g. Voice Call, it has a non zero
@@ -4322,7 +4324,8 @@
         super.getVolumeGroupMaxVolumeIndex_enforcePermission();
         synchronized (VolumeStreamState.class) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
-                throw new IllegalArgumentException("No volume group for id " + groupId);
+                Log.e(TAG, "No volume group for id " + groupId);
+                return 0;
             }
             VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
             return vgs.getMaxIndex();
@@ -4336,7 +4339,8 @@
         super.getVolumeGroupMinVolumeIndex_enforcePermission();
         synchronized (VolumeStreamState.class) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
-                throw new IllegalArgumentException("No volume group for id " + groupId);
+                Log.e(TAG, "No volume group for id " + groupId);
+                return 0;
             }
             VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
             return vgs.getMinIndex();
@@ -4765,6 +4769,8 @@
     private void dumpFlags(PrintWriter pw) {
 
         pw.println("\nFun with Flags:");
+        pw.println("\tcom.android.media.audio.as_device_connection_failure:"
+                + asDeviceConnectionFailure());
         pw.println("\tandroid.media.audio.autoPublicVolumeApiHardening:"
                 + autoPublicVolumeApiHardening());
         pw.println("\tandroid.media.audio.Flags.automaticBtDeviceType:"
@@ -8259,11 +8265,21 @@
     private static final SparseArray<VolumeGroupState> sVolumeGroupStates = new SparseArray<>();
 
     private void initVolumeGroupStates() {
+        int btScoGroupId = -1;
+        VolumeGroupState voiceCallGroup = null;
         for (final AudioVolumeGroup avg : getAudioVolumeGroups()) {
             try {
-                // if no valid attributes, this volume group is not controllable
-                if (ensureValidAttributes(avg)) {
-                    sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
+                if (ensureValidVolumeGroup(avg)) {
+                    final VolumeGroupState vgs = new VolumeGroupState(avg);
+                    sVolumeGroupStates.append(avg.getId(), vgs);
+                    if (vgs.isVoiceCall()) {
+                        voiceCallGroup = vgs;
+                    }
+                } else {
+                    // invalid volume group will be reported for bt sco group with no other
+                    // legacy stream type, we try to replace it in sVolumeGroupStates with the
+                    // voice call volume group
+                    btScoGroupId = avg.getId();
                 }
             } catch (IllegalArgumentException e) {
                 // Volume Groups without attributes are not controllable through set/get volume
@@ -8271,10 +8287,15 @@
                 if (DEBUG_VOL) {
                     Log.d(TAG, "volume group " + avg.name() + " for internal policy needs");
                 }
-                continue;
             }
         }
 
+        if (replaceStreamBtSco() && btScoGroupId >= 0 && voiceCallGroup != null) {
+            // the bt sco group is deprecated, storing the voice call group instead
+            // to keep the code backwards compatible when calling the volume group APIs
+            sVolumeGroupStates.append(btScoGroupId, voiceCallGroup);
+        }
+
         // need mSettingsLock for vgs.applyAllVolumes -> vss.setIndex which grabs this lock after
         // VSS.class. Locking order needs to be preserved
         synchronized (mSettingsLock) {
@@ -8285,7 +8306,15 @@
         }
     }
 
-    private boolean ensureValidAttributes(AudioVolumeGroup avg) {
+    /**
+     * Returns false if the legacy stream types only contains the deprecated
+     * {@link AudioSystem#STREAM_BLUETOOTH_SCO}.
+     *
+     * @throws IllegalArgumentException if it has more than one non-default {@link AudioAttributes}
+     *
+     * @param avg the volume group to check
+     */
+    private boolean ensureValidVolumeGroup(AudioVolumeGroup avg) {
         boolean hasAtLeastOneValidAudioAttributes = avg.getAudioAttributes().stream()
                 .anyMatch(aa -> !aa.equals(AudioProductStrategy.getDefaultAttributes()));
         if (!hasAtLeastOneValidAudioAttributes) {
@@ -8293,10 +8322,11 @@
                     + " has no valid audio attributes");
         }
         if (replaceStreamBtSco()) {
-            for (int streamType : avg.getLegacyStreamTypes()) {
-                if (streamType == AudioSystem.STREAM_BLUETOOTH_SCO) {
-                    return false;
-                }
+            // if there are multiple legacy stream types associated we can omit stream bt sco
+            // otherwise this is not a valid volume group
+            if (avg.getLegacyStreamTypes().length == 1
+                    && avg.getLegacyStreamTypes()[0] == AudioSystem.STREAM_BLUETOOTH_SCO) {
+                return false;
             }
         }
         return true;
@@ -8637,6 +8667,10 @@
             return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_MUSIC;
         }
 
+        public boolean isVoiceCall() {
+            return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_VOICE_CALL;
+        }
+
         public void applyAllVolumes(boolean userSwitch) {
             String caller = "from vgs";
             synchronized (AudioService.VolumeStreamState.class) {
diff --git a/services/core/java/com/android/server/biometrics/biometrics.aconfig b/services/core/java/com/android/server/biometrics/biometrics.aconfig
index 92fd9cb..15c8850 100644
--- a/services/core/java/com/android/server/biometrics/biometrics.aconfig
+++ b/services/core/java/com/android/server/biometrics/biometrics.aconfig
@@ -14,3 +14,10 @@
   description: "This flag controls whether virtual HAL is used for testing instead of TestHal "
   bug: "294254230"
 }
+
+flag {
+  name: "notify_fingerprint_loe"
+  namespace: "biometrics_framework"
+  description: "This flag controls whether a notification should be sent to notify user when loss of enrollment happens"
+  bug: "351036558"
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
index 53e6bdb..27f9cc8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
@@ -151,6 +151,43 @@
     }
 
     /**
+     * Shows a fingerprint notification for loss of enrollment
+     */
+    public static void showFingerprintLoeNotification(@NonNull Context context) {
+        Slog.d(TAG, "Showing fingerprint LOE notification");
+
+        final String name =
+                context.getString(R.string.device_unlock_notification_name);
+        final String title = context.getString(R.string.fingerprint_dangling_notification_title);
+        final String content = context.getString(R.string.fingerprint_loe_notification_msg);
+
+        // Create "Set up" notification action button.
+        final Intent setupIntent =
+                new Intent(BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH);
+        final PendingIntent setupPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
+                setupIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+        final String setupText =
+                context.getString(R.string.biometric_dangling_notification_action_set_up);
+        final Notification.Action setupAction = new Notification.Action.Builder(
+                null, setupText, setupPendingIntent).build();
+
+        // Create "Not now" notification action button.
+        final Intent notNowIntent =
+                new Intent(BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_DISMISS);
+        final PendingIntent notNowPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
+                notNowIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+        final String notNowText = context.getString(
+                R.string.biometric_dangling_notification_action_not_now);
+        final Notification.Action notNowAction = new Notification.Action.Builder(
+                null, notNowText, notNowPendingIntent).build();
+
+        showNotificationHelper(context, name, title, content, setupPendingIntent, setupAction,
+                notNowAction, Notification.CATEGORY_SYSTEM, FINGERPRINT_RE_ENROLL_CHANNEL,
+                FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG, Notification.VISIBILITY_SECRET, false,
+                Notification.FLAG_NO_CLEAR);
+    }
+
+    /**
      * Shows a fingerprint bad calibration notification.
      */
     public static void showBadCalibrationNotification(@NonNull Context context) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java b/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java
index 7fb27b6..63678aa 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java
@@ -57,6 +57,7 @@
     protected boolean mInvalidationInProgress;
     protected final Context mContext;
     protected final File mFile;
+    private boolean mIsInvalidBiometricState = false;
 
     private final Runnable mWriteStateRunnable = this::doWriteStateInternal;
 
@@ -102,7 +103,7 @@
             serializer.endDocument();
             destination.finishWrite(out);
         } catch (Throwable t) {
-            Slog.wtf(TAG, "Failed to write settings, restoring backup", t);
+            Slog.e(TAG, "Failed to write settings, restoring backup", t);
             destination.failWrite(out);
             throw new IllegalStateException("Failed to write to file: " + mFile.toString(), t);
         } finally {
@@ -192,6 +193,29 @@
         }
     }
 
+    /**
+     * Return true if the biometric file is correctly read. Otherwise return false.
+     */
+    public boolean isInvalidBiometricState() {
+        return mIsInvalidBiometricState;
+    }
+
+    /**
+     * Delete the file of the biometric state.
+     */
+    public void deleteBiometricFile() {
+        synchronized (this) {
+            if (!mFile.exists()) {
+                return;
+            }
+            if (mFile.delete()) {
+                Slog.i(TAG, mFile + " is deleted successfully");
+            } else {
+                Slog.i(TAG, "Failed to delete " + mFile);
+            }
+        }
+    }
+
     private boolean isUnique(String name) {
         for (T identifier : mBiometrics) {
             if (identifier.getName().equals(name)) {
@@ -218,7 +242,8 @@
         try {
             in = new FileInputStream(mFile);
         } catch (FileNotFoundException fnfe) {
-            Slog.i(TAG, "No fingerprint state");
+            Slog.i(TAG, "No fingerprint state", fnfe);
+            mIsInvalidBiometricState = true;
             return;
         }
         try {
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricUtils.java
index ebe4679..0b4f640 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricUtils.java
@@ -33,4 +33,14 @@
     CharSequence getUniqueName(Context context, int userId);
     void setInvalidationInProgress(Context context, int userId, boolean inProgress);
     boolean isInvalidationInProgress(Context context, int userId);
+
+    /**
+     * Return true if the biometric file is correctly read. Otherwise return false.
+     */
+    boolean hasValidBiometricUserState(Context context, int userId);
+
+    /**
+     * Delete the file of the biometric state.
+     */
+    void deleteStateForUser(int userId);
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
index 69ad152..3b6aeef 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
@@ -25,6 +25,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.BiometricsProto;
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 
@@ -62,7 +63,7 @@
     }
 
     private final ArrayList<UserTemplate> mUnknownHALTemplates = new ArrayList<>();
-    private final BiometricUtils<S> mBiometricUtils;
+    protected final BiometricUtils<S> mBiometricUtils;
     private final Map<Integer, Long> mAuthenticatorIds;
     private final boolean mHasEnrollmentsBeforeStarting;
     private BaseClientMonitor mCurrentTask;
@@ -105,6 +106,11 @@
                     startCleanupUnknownHalTemplates();
                 }
             }
+
+            if (mBiometricUtils.hasValidBiometricUserState(getContext(), getTargetUserId())
+                    && Flags.notifyFingerprintLoe()) {
+                handleInvalidBiometricState();
+            }
         }
     };
 
@@ -248,4 +254,8 @@
     public ArrayList<UserTemplate> getUnknownHALTemplates() {
         return mUnknownHALTemplates;
     }
+
+    protected void handleInvalidBiometricState() {}
+
+    protected abstract int getModality();
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceUtils.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceUtils.java
index c574478..79285cb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceUtils.java
@@ -124,6 +124,22 @@
         return getStateForUser(context, userId).isInvalidationInProgress();
     }
 
+    @Override
+    public boolean hasValidBiometricUserState(Context context, int userId) {
+        return getStateForUser(context, userId).isInvalidBiometricState();
+    }
+
+    @Override
+    public void deleteStateForUser(int userId) {
+        synchronized (this) {
+            FaceUserState state = mUserStates.get(userId);
+            if (state != null) {
+                state.deleteBiometricFile();
+                mUserStates.delete(userId);
+            }
+        }
+    }
+
     private FaceUserState getStateForUser(Context ctx, int userId) {
         synchronized (this) {
             FaceUserState state = mUserStates.get(userId);
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java
index e75c6ab..964bf6c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.face.IFace;
 import android.hardware.face.Face;
 import android.os.IBinder;
@@ -77,4 +78,9 @@
         FaceUtils.getInstance(getSensorId()).addBiometricForUser(
                 getContext(), getTargetUserId(), (Face) identifier);
     }
+
+    @Override
+    protected int getModality() {
+        return BiometricsProtoEnums.MODALITY_FACE;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUtils.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUtils.java
index 0062d31..b8c06c7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUtils.java
@@ -140,6 +140,22 @@
         return getStateForUser(context, userId).isInvalidationInProgress();
     }
 
+    @Override
+    public boolean hasValidBiometricUserState(Context context, int userId) {
+        return getStateForUser(context, userId).isInvalidBiometricState();
+    }
+
+    @Override
+    public void deleteStateForUser(int userId) {
+        synchronized (this) {
+            FingerprintUserState state = mUserStates.get(userId);
+            if (state != null) {
+                state.deleteBiometricFile();
+                mUserStates.delete(userId);
+            }
+        }
+    }
+
     private FingerprintUserState getStateForUser(Context ctx, int userId) {
         synchronized (this) {
             FingerprintUserState state = mUserStates.get(userId);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java
index 5edc2ca..1fc5179 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java
@@ -22,9 +22,11 @@
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.fingerprint.Fingerprint;
 import android.os.IBinder;
+import android.util.Slog;
 
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.BiometricNotificationUtils;
 import com.android.server.biometrics.sensors.BiometricUtils;
 import com.android.server.biometrics.sensors.InternalCleanupClient;
 import com.android.server.biometrics.sensors.InternalEnumerateClient;
@@ -42,6 +44,8 @@
 public class FingerprintInternalCleanupClient
         extends InternalCleanupClient<Fingerprint, AidlSession> {
 
+    private static final String TAG = "FingerprintInternalCleanupClient";
+
     public FingerprintInternalCleanupClient(@NonNull Context context,
             @NonNull Supplier<AidlSession> lazyDaemon,
             int userId, @NonNull String owner, int sensorId,
@@ -80,4 +84,16 @@
         FingerprintUtils.getInstance(getSensorId()).addBiometricForUser(
                 getContext(), getTargetUserId(), (Fingerprint) identifier);
     }
+
+    @Override
+    public void handleInvalidBiometricState() {
+        Slog.d(TAG, "Invalid fingerprint user state: delete the state.");
+        mBiometricUtils.deleteStateForUser(getTargetUserId());
+        BiometricNotificationUtils.showFingerprintLoeNotification(getContext());
+    }
+
+    @Override
+    protected int getModality() {
+        return BiometricsProtoEnums.MODALITY_FINGERPRINT;
+    }
 }
diff --git a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
index 537fb325..615db34 100644
--- a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
+++ b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
@@ -1,9 +1,4 @@
 {
-  "presubmit": [
-    {
-      "name": "CrashRecoveryModuleTests"
-    }
-  ],
   "postsubmit": [
     {
       "name": "FrameworksMockingServicesTests",
@@ -12,6 +7,9 @@
           "include-filter": "com.android.server.RescuePartyTest"
         }
       ]
+    },
+    {
+      "name": "CrashRecoveryModuleTests"
     }
   ]
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 7b5cff7..226bdf5 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -579,6 +579,14 @@
         return mCurrentBrightnessMapper.getMode();
     }
 
+    /**
+     * @return The preset for this mapping strategy. Presets are used on devices that allow users
+     * to choose from a set of predefined options in display auto-brightness settings.
+     */
+    public int getPreset() {
+        return mCurrentBrightnessMapper.getPreset();
+    }
+
     public boolean isInIdleMode() {
         return mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE;
     }
diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
index 8405e0a..b0507fb 100644
--- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
+++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
@@ -140,10 +140,10 @@
             builder.setShortTermModelLowerLuxMultiplier(SHORT_TERM_MODEL_THRESHOLD_RATIO);
             builder.setShortTermModelUpperLuxMultiplier(SHORT_TERM_MODEL_THRESHOLD_RATIO);
             return new PhysicalMappingStrategy(builder.build(), nitsRange, brightnessRange,
-                    autoBrightnessAdjustmentMaxGamma, mode, displayWhiteBalanceController);
+                    autoBrightnessAdjustmentMaxGamma, mode, preset, displayWhiteBalanceController);
         } else if (isValidMapping(luxLevels, brightnessLevels)) {
             return new SimpleMappingStrategy(luxLevels, brightnessLevels,
-                    autoBrightnessAdjustmentMaxGamma, shortTermModelTimeout, mode);
+                    autoBrightnessAdjustmentMaxGamma, shortTermModelTimeout, mode, preset);
         } else {
             return null;
         }
@@ -394,6 +394,12 @@
     abstract int getMode();
 
     /**
+     * @return The preset for this mapping strategy. Presets are used on devices that allow users
+     * to choose from a set of predefined options in display auto-brightness settings.
+     */
+    abstract int getPreset();
+
+    /**
      * Check if the short term model should be reset given the anchor lux the last
      * brightness change was made at and the current ambient lux.
      */
@@ -598,6 +604,8 @@
         @AutomaticBrightnessController.AutomaticBrightnessMode
         private final int mMode;
 
+        private final int mPreset;
+
         private Spline mSpline;
         private float mMaxGamma;
         private float mAutoBrightnessAdjustment;
@@ -606,7 +614,8 @@
         private long mShortTermModelTimeout;
 
         private SimpleMappingStrategy(float[] lux, float[] brightness, float maxGamma,
-                long timeout, @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
+                long timeout, @AutomaticBrightnessController.AutomaticBrightnessMode int mode,
+                int preset) {
             Preconditions.checkArgument(lux.length != 0 && brightness.length != 0,
                     "Lux and brightness arrays must not be empty!");
             Preconditions.checkArgument(lux.length == brightness.length,
@@ -633,6 +642,7 @@
             computeSpline();
             mShortTermModelTimeout = timeout;
             mMode = mode;
+            mPreset = preset;
         }
 
         @Override
@@ -766,6 +776,11 @@
         }
 
         @Override
+        int getPreset() {
+            return mPreset;
+        }
+
+        @Override
         float getUserLux() {
             return mUserLux;
         }
@@ -837,6 +852,8 @@
         @AutomaticBrightnessController.AutomaticBrightnessMode
         private final int mMode;
 
+        private final int mPreset;
+
         // Previous short-term models and the times that they were computed stored for debugging
         // purposes
         private List<Spline> mPreviousBrightnessSplines = new ArrayList<>();
@@ -846,7 +863,7 @@
 
         public PhysicalMappingStrategy(BrightnessConfiguration config, float[] nits,
                 float[] brightness, float maxGamma,
-                @AutomaticBrightnessController.AutomaticBrightnessMode int mode,
+                @AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset,
                 @Nullable DisplayWhiteBalanceController displayWhiteBalanceController) {
 
             Preconditions.checkArgument(nits.length != 0 && brightness.length != 0,
@@ -860,6 +877,7 @@
                     PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, "brightness");
 
             mMode = mode;
+            mPreset = preset;
             mMaxGamma = maxGamma;
             mAutoBrightnessAdjustment = 0;
             mUserLux = INVALID_LUX;
@@ -1073,6 +1091,11 @@
         }
 
         @Override
+        int getPreset() {
+            return mPreset;
+        }
+
+        @Override
         float getUserLux() {
             return mUserLux;
         }
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index 515e704..8a3e392 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -60,7 +60,7 @@
         mModeChangeCallback = modeChangeCallback;
         mHdrClamper = hdrClamper;
         mNormalBrightnessModeController = normalBrightnessModeController;
-        mUseHdrClamper = flags.isHdrClamperEnabled();
+        mUseHdrClamper = flags.isHdrClamperEnabled() && !flags.useNewHdrBrightnessModifier();
         mUseNbmController = flags.isNbmControllerEnabled();
         if (mUseNbmController) {
             mNormalBrightnessModeController.resetNbmData(
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index ed6ed60..cc115f1 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -588,22 +588,43 @@
  *         <minorVersion>0</minorVersion>
  *     </usiVersion>
  *     <evenDimmer enabled="true">
- *       <transitionPoint>0.1</transitionPoint>
- *
- *       <nits>0.2</nits>
- *       <nits>2.0</nits>
- *       <nits>500.0</nits>
- *       <nits>1000.0</nits>
- *
- *       <backlight>0</backlight>
- *       <backlight>0.0001</backlight>
- *       <backlight>0.5</backlight>
- *       <backlight>1.0</backlight>
- *
- *       <brightness>0</brightness>
- *       <brightness>0.1</brightness>
- *       <brightness>0.5</brightness>
- *       <brightness>1.0</brightness>
+ *         <transitionPoint>0.1</transitionPoint>
+ *         <brightnessMapping>
+ *             <brightnessPoint>
+ *                 <nits>0.2</nits>
+ *                 <backlight>0</backlight>
+ *                 <brightness>0</brightness>
+ *                 </brightnessPoint>
+ *             <brightnessPoint>
+ *                 <nits>2.0</nits>
+ *                 <backlight>0.01</backlight>
+ *                 <brightness>0.002</brightness>
+ *             </brightnessPoint>
+ *             <brightnessPoint>
+ *                 <nits>500.0</nits>
+ *                 <backlight>0.5</backlight>
+ *                 <brightness>0.5</brightness>
+ *             </brightnessPoint>
+ *             <brightnessPoint>
+ *                 <nits>1000</nits>
+ *                 <backlight>1.0</backlight>
+ *                 <brightness>1.0</brightness>
+ *             </brightnessPoint>
+ *         </brightnessMapping>
+ *         <luxToMinimumNitsMap>
+ *             <point>
+ *                 <value>10</value>
+ *                 <nits>0.3</nits>
+ *             </point>
+ *             <point>
+ *                 <value>50</value>
+ *                 <nits>0.7</nits>
+ *             </point>
+ *             <point>
+ *                 <value>100</value>
+ *                 <nits>1.0</nits>
+ *             </point>
+ *         </luxToMinimumNitsMap>
  *     </evenDimmer>
  *     <screenBrightnessCapForWearBedtimeMode>0.1</screenBrightnessCapForWearBedtimeMode>
  *     <idleScreenRefreshRateTimeout>
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 2cec869..9e905ab 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -722,6 +722,7 @@
             if (userSwitching) {
                 mCurrentUserId = newUserId;
             }
+            mDisplayModeDirector.onSwitchUser();
             mLogicalDisplayMapper.forEachLocked(logicalDisplay -> {
                 if (logicalDisplay.getDisplayInfoLocked().type != Display.TYPE_INTERNAL) {
                     return;
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 8b21d98..480c370 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -702,6 +702,17 @@
     private void handleOnSwitchUser(@UserIdInt int newUserId, int userSerial, float newBrightness) {
         Slog.i(mTag, "Switching user newUserId=" + newUserId + " userSerial=" + userSerial
                 + " newBrightness=" + newBrightness);
+
+        if (mAutomaticBrightnessController != null) {
+            int autoBrightnessPreset = Settings.System.getIntForUser(mContext.getContentResolver(),
+                    Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+                    Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL,
+                    UserHandle.USER_CURRENT);
+            if (autoBrightnessPreset != mAutomaticBrightnessController.getPreset()) {
+                setUpAutoBrightness(mContext, mHandler);
+            }
+        }
+
         handleBrightnessModeChange();
         if (mBrightnessTracker != null) {
             mBrightnessTracker.onSwitchUser(newUserId);
@@ -714,6 +725,7 @@
         if (mAutomaticBrightnessController != null) {
             mAutomaticBrightnessController.resetShortTermModel();
         }
+        mBrightnessClamperController.onUserSwitch();
         sendUpdatePowerState();
     }
 
@@ -1009,7 +1021,7 @@
         if (mFlags.areAutoBrightnessModesEnabled()) {
             mContext.getContentResolver().registerContentObserver(
                     Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_FOR_ALS),
-                    /* notifyForDescendants= */ false, mSettingsObserver, UserHandle.USER_CURRENT);
+                    /* notifyForDescendants= */ false, mSettingsObserver, UserHandle.USER_ALL);
         }
         handleBrightnessModeChange();
     }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 9324fc1..12c3197 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -71,6 +71,7 @@
 
     private final List<DisplayDeviceDataListener> mDisplayDeviceDataListeners = new ArrayList<>();
     private final List<StatefulModifier> mStatefulModifiers = new ArrayList<>();
+    private final List<UserSwitchListener> mUserSwitchListeners = new ArrayList<>();
     private ModifiersAggregatedState mModifiersAggregatedState = new ModifiersAggregatedState();
 
     private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener;
@@ -127,6 +128,9 @@
             if (m instanceof StatefulModifier s) {
                 mStatefulModifiers.add(s);
             }
+            if (m instanceof UserSwitchListener l) {
+                mUserSwitchListeners.add(l);
+            }
         });
         mOnPropertiesChangedListener =
                 properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged);
@@ -209,6 +213,13 @@
     }
 
     /**
+     * Called when the user switches.
+     */
+    public void onUserSwitch() {
+        mUserSwitchListeners.forEach(listener -> listener.onSwitchUser());
+    }
+
+    /**
      * Used to dump ClampersController state.
      */
     public void dump(PrintWriter writer) {
@@ -466,6 +477,13 @@
     }
 
     /**
+     * A clamper/modifier should implement this interface if it reads user-specific settings
+     */
+    interface UserSwitchListener {
+        void onSwitchUser();
+    }
+
+    /**
      * StatefulModifiers contribute to AggregatedState, that is used to decide if brightness
      * adjustement is needed
      */
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
index 951980a..c3596c3 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
@@ -41,7 +41,8 @@
  * Class used to prevent the screen brightness dipping below a certain value, based on current
  * lux conditions and user preferred minimum.
  */
-public class BrightnessLowLuxModifier extends BrightnessModifier {
+public class BrightnessLowLuxModifier extends BrightnessModifier implements
+        BrightnessClamperController.UserSwitchListener {
 
     // To enable these logs, run:
     // 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot'
@@ -81,10 +82,9 @@
      */
     @VisibleForTesting
     public void recalculateLowerBound() {
-        int userId = UserHandle.USER_CURRENT;
         float settingNitsLowerBound = Settings.Secure.getFloatForUser(
                 mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
-                /* def= */ MIN_NITS_DEFAULT, userId);
+                /* def= */ MIN_NITS_DEFAULT, UserHandle.USER_CURRENT);
 
         boolean isActive = isSettingEnabled()
                 && mAmbientLux != BrightnessMappingStrategy.INVALID_LUX;
@@ -190,6 +190,11 @@
     }
 
     @Override
+    public void onSwitchUser() {
+        recalculateLowerBound();
+    }
+
+    @Override
     public void dump(PrintWriter pw) {
         pw.println("BrightnessLowLuxModifier:");
         pw.println("  mIsActive=" + mIsActive);
@@ -221,10 +226,10 @@
             super(handler);
             mContentResolver.registerContentObserver(
                     Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS),
-                    false, this);
+                    false, this, UserHandle.USER_ALL);
             mContentResolver.registerContentObserver(
                     Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED),
-                    false, this);
+                    false, this, UserHandle.USER_ALL);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
index 5e44cc3..ae1801c 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
@@ -31,6 +31,7 @@
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.DisplayBrightnessState;
 import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.brightness.BrightnessReason;
 import com.android.server.display.config.HdrBrightnessData;
 
 import java.io.PrintWriter;
@@ -99,7 +100,7 @@
             mMaxBrightness = mPendingMaxBrightness;
             mClamperChangeListener.onChanged();
         };
-        onDisplayChanged(displayData);
+        mHandler.post(() -> onDisplayChanged(displayData));
     }
 
     // Called in DisplayControllerHandler
@@ -120,6 +121,8 @@
 
         stateBuilder.setHdrBrightness(hdrBrightness);
         stateBuilder.setCustomAnimationRate(mTransitionRate);
+        stateBuilder.getBrightnessReason().addModifier(BrightnessReason.MODIFIER_HDR);
+
         // transition rate applied, reset
         mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
     }
@@ -168,10 +171,18 @@
         }
     }
 
+    // Called in DisplayControllerHandler
     @Override
     public void onDisplayChanged(BrightnessClamperController.DisplayDeviceData displayData) {
-        mHandler.post(() -> onDisplayChanged(displayData.mDisplayToken, displayData.mWidth,
-                displayData.mHeight, displayData.mDisplayDeviceConfig));
+        mDisplayDeviceConfig = displayData.mDisplayDeviceConfig;
+        mScreenSize = (float) displayData.mWidth * displayData.mHeight;
+        HdrBrightnessData data = mDisplayDeviceConfig.getHdrBrightnessData();
+        if (data == null) {
+            unregisterHdrListener();
+        } else {
+            registerHdrListener(displayData.mDisplayToken);
+        }
+        recalculate(data, mMaxDesiredHdrRatio);
     }
 
     // Called in DisplayControllerHandler, when any modifier state changes
@@ -215,20 +226,6 @@
     }
 
     // Called in DisplayControllerHandler
-    private void onDisplayChanged(IBinder displayToken, int width, int height,
-            DisplayDeviceConfig config) {
-        mDisplayDeviceConfig = config;
-        mScreenSize = (float) width * height;
-        HdrBrightnessData data = config.getHdrBrightnessData();
-        if (data == null) {
-            unregisterHdrListener();
-        } else {
-            registerHdrListener(displayToken);
-        }
-        recalculate(data, mMaxDesiredHdrRatio);
-    }
-
-    // Called in DisplayControllerHandler
     private void recalculate(@Nullable HdrBrightnessData data, float maxDesiredHdrRatio) {
         Mode newMode = recalculateMode(data);
         // if HDR mode changed, notify changed
@@ -258,6 +255,10 @@
         if (data == null) {
             return Mode.NO_HDR;
         }
+        // no HDR layer present
+        if (mHdrLayerSize == DEFAULT_HDR_LAYER_SIZE) {
+            return Mode.NO_HDR;
+        }
         // HDR layer < minHdr % for Nbm
         if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForNbm) {
             return Mode.NO_HDR;
diff --git a/services/core/java/com/android/server/display/brightness/clamper/LightSensorController.java b/services/core/java/com/android/server/display/brightness/clamper/LightSensorController.java
index d89dd28..b219cb1 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/LightSensorController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/LightSensorController.java
@@ -62,6 +62,9 @@
     private final SensorEventListener mLightSensorEventListener = new SensorEventListener() {
         @Override
         public void onSensorChanged(SensorEvent event) {
+            if (event.sensor != mRegisteredLightSensor) {
+                return;
+            }
             long now = mInjector.getTime();
             mAmbientFilter.addValue(TimeUnit.NANOSECONDS.toMillis(event.timestamp),
                     event.values[0]);
@@ -95,15 +98,13 @@
         if (mRegisteredLightSensor == mLightSensor) {
             return;
         }
+        if (mLightSensor != null) {
+            mSensorManager.registerListener(mLightSensorEventListener,
+                    mLightSensor, mLightSensorRate * 1000, mHandler);
+        }
         if (mRegisteredLightSensor != null) {
             stop();
         }
-        if (mLightSensor == null) {
-            return;
-        }
-
-        mSensorManager.registerListener(mLightSensorEventListener,
-                mLightSensor, mLightSensorRate * 1000, mHandler);
         mRegisteredLightSensor = mLightSensor;
 
         if (DEBUG) {
@@ -115,7 +116,7 @@
         if (mRegisteredLightSensor == null) {
             return;
         }
-        mSensorManager.unregisterListener(mLightSensorEventListener);
+        mSensorManager.unregisterListener(mLightSensorEventListener, mRegisteredLightSensor);
         mRegisteredLightSensor = null;
         mAmbientFilter.clear();
         mLightSensorListener.onAmbientLuxChange(INVALID_LUX);
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index d610f08..5e471c8 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -121,6 +121,7 @@
     private static final int MSG_HIGH_BRIGHTNESS_THRESHOLDS_CHANGED = 6;
     private static final int MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED = 7;
     private static final int MSG_REFRESH_RATE_IN_HBM_HDR_CHANGED = 8;
+    private static final int MSG_SWITCH_USER = 9;
 
     private final Object mLock = new Object();
     private final Context mContext;
@@ -564,6 +565,13 @@
     }
 
     /**
+     * Called when the user switches.
+     */
+    public void onSwitchUser() {
+        mHandler.obtainMessage(MSG_SWITCH_USER).sendToTarget();
+    }
+
+    /**
      * Print the object's state and debug information into the given stream.
      *
      * @param pw The stream to dump information to.
@@ -789,6 +797,13 @@
                     mHbmObserver.onDeviceConfigRefreshRateInHbmHdrChanged(refreshRateInHbmHdr);
                     break;
                 }
+
+                case MSG_SWITCH_USER: {
+                    synchronized (mLock) {
+                        mSettingsObserver.updateRefreshRateSettingLocked();
+                        mSettingsObserver.updateModeSwitchingTypeSettingLocked();
+                    }
+                }
             }
         }
     }
@@ -1012,10 +1027,10 @@
             final ContentResolver cr = mContext.getContentResolver();
             mInjector.registerPeakRefreshRateObserver(cr, this);
             mInjector.registerMinRefreshRateObserver(cr, this);
-            cr.registerContentObserver(mLowPowerModeSetting, false /*notifyDescendants*/, this,
-                    UserHandle.USER_SYSTEM);
-            cr.registerContentObserver(mMatchContentFrameRateSetting, false /*notifyDescendants*/,
-                    this);
+            cr.registerContentObserver(mLowPowerModeSetting, /* notifyDescendants= */ false, this,
+                    UserHandle.USER_ALL);
+            cr.registerContentObserver(mMatchContentFrameRateSetting,
+                    /* notifyDescendants= */ false, this, UserHandle.USER_ALL);
             mInjector.registerDisplayListener(mDisplayListener, mHandler);
 
             float deviceConfigDefaultPeakRefresh =
@@ -1156,14 +1171,15 @@
             float highestRefreshRate = getMaxRefreshRateLocked(displayId);
 
             float minRefreshRate = Settings.System.getFloatForUser(cr,
-                    Settings.System.MIN_REFRESH_RATE, 0f, cr.getUserId());
+                    Settings.System.MIN_REFRESH_RATE, 0f, UserHandle.USER_CURRENT);
             if (Float.isInfinite(minRefreshRate)) {
                 // Infinity means that we want the highest possible refresh rate
                 minRefreshRate = highestRefreshRate;
             }
 
             float peakRefreshRate = Settings.System.getFloatForUser(cr,
-                    Settings.System.PEAK_REFRESH_RATE, mDefaultPeakRefreshRate, cr.getUserId());
+                    Settings.System.PEAK_REFRESH_RATE, mDefaultPeakRefreshRate,
+                    UserHandle.USER_CURRENT);
             if (Float.isInfinite(peakRefreshRate)) {
                 // Infinity means that we want the highest possible refresh rate
                 peakRefreshRate = highestRefreshRate;
@@ -1234,9 +1250,9 @@
 
         private void updateModeSwitchingTypeSettingLocked() {
             final ContentResolver cr = mContext.getContentResolver();
-            int switchingType = Settings.Secure.getIntForUser(
-                    cr, Settings.Secure.MATCH_CONTENT_FRAME_RATE, mModeSwitchingType /*default*/,
-                    cr.getUserId());
+            int switchingType = Settings.Secure.getIntForUser(cr,
+                    Settings.Secure.MATCH_CONTENT_FRAME_RATE, /* default= */ mModeSwitchingType,
+                    UserHandle.USER_CURRENT);
             if (switchingType != mModeSwitchingType) {
                 mModeSwitchingType = switchingType;
                 notifyDesiredDisplayModeSpecsChangedLocked();
@@ -3033,14 +3049,14 @@
         public void registerPeakRefreshRateObserver(@NonNull ContentResolver cr,
                 @NonNull ContentObserver observer) {
             cr.registerContentObserver(PEAK_REFRESH_RATE_URI, false /*notifyDescendants*/,
-                    observer, UserHandle.USER_SYSTEM);
+                    observer, UserHandle.USER_ALL);
         }
 
         @Override
         public void registerMinRefreshRateObserver(@NonNull ContentResolver cr,
                 @NonNull ContentObserver observer) {
             cr.registerContentObserver(MIN_REFRESH_RATE_URI, false /*notifyDescendants*/,
-                    observer, UserHandle.USER_SYSTEM);
+                    observer, UserHandle.USER_ALL);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index d43e783..a3b77e8 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -543,18 +543,20 @@
     }
 
     private void startDozingInternal(IBinder token, int screenState,
-            @Display.StateReason int reason, int screenBrightness) {
+            @Display.StateReason int reason, float screenBrightnessFloat, int screenBrightnessInt) {
         Slog.d(TAG, "Dream requested to start dozing: " + token
                 + ", screenState=" + Display.stateToString(screenState)
                 + ", reason=" + Display.stateReasonToString(reason)
-                + ", screenBrightness=" + screenBrightness);
+                + ", screenBrightnessFloat=" + screenBrightnessFloat
+                + ", screenBrightnessInt=" + screenBrightnessInt);
 
         synchronized (mLock) {
             if (mCurrentDream != null && mCurrentDream.token == token && mCurrentDream.canDoze) {
                 mCurrentDream.dozeScreenState = screenState;
-                mCurrentDream.dozeScreenBrightness = screenBrightness;
+                mCurrentDream.dozeScreenBrightness = screenBrightnessInt;
+                mCurrentDream.dozeScreenBrightnessFloat = screenBrightnessFloat;
                 mPowerManagerInternal.setDozeOverrideFromDreamManager(
-                        screenState, reason, screenBrightness);
+                        screenState, reason, screenBrightnessFloat, screenBrightnessInt);
                 if (!mCurrentDream.isDozing) {
                     mCurrentDream.isDozing = true;
                     mDozeWakeLock.acquire();
@@ -575,6 +577,7 @@
                 mPowerManagerInternal.setDozeOverrideFromDreamManager(
                         Display.STATE_UNKNOWN,
                         Display.STATE_REASON_DREAM_MANAGER,
+                        PowerManager.BRIGHTNESS_INVALID_FLOAT,
                         PowerManager.BRIGHTNESS_DEFAULT);
             }
         }
@@ -1095,7 +1098,7 @@
         @Override // Binder call
         public void startDozing(
                 IBinder token, int screenState, @Display.StateReason int reason,
-                int screenBrightness) {
+                float screenBrightnessFloat, int screeBrightnessInt) {
             // Requires no permission, called by Dream from an arbitrary process.
             if (token == null) {
                 throw new IllegalArgumentException("token must not be null");
@@ -1103,7 +1106,8 @@
 
             final long ident = Binder.clearCallingIdentity();
             try {
-                startDozingInternal(token, screenState, reason, screenBrightness);
+                startDozingInternal(token, screenState, reason, screenBrightnessFloat,
+                        screeBrightnessInt);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -1112,7 +1116,7 @@
         @Override // Binder call
         public void startDozingOneway(
                 IBinder token, int screenState, @Display.StateReason int reason,
-                int screenBrightness) {
+                float screenBrightnessFloat, int screeBrightnessInt) {
             // Requires no permission, called by Dream from an arbitrary process.
             if (token == null) {
                 throw new IllegalArgumentException("token must not be null");
@@ -1120,7 +1124,8 @@
 
             final long ident = Binder.clearCallingIdentity();
             try {
-                startDozingInternal(token, screenState, reason, screenBrightness);
+                startDozingInternal(token, screenState, reason, screenBrightnessFloat,
+                        screeBrightnessInt);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -1277,6 +1282,7 @@
         public boolean isWaking = false;
         public int dozeScreenState = Display.STATE_UNKNOWN;
         public int dozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
+        public float dozeScreenBrightnessFloat = PowerManager.BRIGHTNESS_INVALID_FLOAT;
 
         DreamRecord(ComponentName name, int userId, boolean isPreview, boolean canDoze) {
             this.name = name;
@@ -1297,6 +1303,7 @@
                     + ", isWaking=" + isWaking
                     + ", dozeScreenState=" + dozeScreenState
                     + ", dozeScreenBrightness=" + dozeScreenBrightness
+                    + ", dozeScreenBrightnessFloat=" + dozeScreenBrightnessFloat
                     + '}';
         }
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
index 3c3bdd5..7746276 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
@@ -769,6 +769,7 @@
      * @return true if the UI Broadcast type is valid
      */
     private static boolean isValidUiBroadcastType(int value) {
+        value = value & 0xFF;
         return ((value == 0x00)
                 || (value == 0x01)
                 || (value == 0x10)
diff --git a/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java b/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java
index 539a00d..a33d70a 100644
--- a/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestActiveSourceAction.java
@@ -19,6 +19,7 @@
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
 
 /**
  * Feature action that sends <Request Active Source> message and waits for <Active Source> on TV
@@ -39,6 +40,10 @@
     // Number of retries <Request Active Source> is sent if no device answers this message.
     private static final int MAX_SEND_RETRY_COUNT = 1;
 
+    // Timeout to wait for the LauncherX API call to be completed.
+    @VisibleForTesting
+    protected static final int TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS = 10000;
+
     private int mSendRetryCount = 0;
 
 
@@ -55,7 +60,7 @@
         // We wait for default timeout to allow the message triggered by the LauncherX API call to
         // be sent by the TV and another default timeout in case the message has to be answered
         // (e.g. TV sent a <Set Stream Path> or <Routing Change>).
-        addTimer(mState, HdmiConfig.TIMEOUT_MS * 2);
+        addTimer(mState, TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
         return true;
     }
 
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 55de9aa..a06ad14 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1254,9 +1254,10 @@
     /**
      * Start drag and drop.
      *
-     * @param fromChannel The input channel that is currently receiving a touch gesture that should
-     *                    be turned into the drag pointer.
-     * @param dragAndDropChannel The input channel associated with the system drag window.
+     * @param fromChannelToken The token of the input channel that is currently receiving a touch
+     *                        gesture that should be turned into the drag pointer.
+     * @param dragAndDropChannelToken The token of the input channel associated with the system drag
+     *                               window.
      * @return true if drag and drop was successfully started, false otherwise.
      */
     public boolean startDragAndDrop(@NonNull IBinder fromChannelToken,
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
index a7280e6..58e3452 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
@@ -71,7 +71,20 @@
     @Retention(SOURCE)
     @Target({METHOD})
     @interface PermissionVerified {
+        /**
+         * The name of the permission that is verified, if precisely one permission is required.
+         * If more than one permission is required, specify either {@link #allOf()} instead.
+         *
+         * <p>If specified, {@link #allOf()} must both be {@code null}.</p>
+         */
         String value() default "";
+
+        /**
+         * Specifies a list of permission names that are all required.
+         *
+         * <p>If specified, {@link #value()} must both be {@code null}.</p>
+         */
+        String[] allOf() default {};
     }
 
     @BinderThread
@@ -132,13 +145,17 @@
 
         void showInputMethodPickerFromClient(IInputMethodClient client, int auxiliarySubtypeMode);
 
-        @PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+        @PermissionVerified(allOf = {
+                Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                Manifest.permission.WRITE_SECURE_SETTINGS})
         void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId);
 
         @PermissionVerified(Manifest.permission.TEST_INPUT_METHOD)
         boolean isInputMethodPickerShownForTest();
 
-        @PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+        @PermissionVerified(allOf = {
+                Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                Manifest.permission.WRITE_SECURE_SETTINGS})
         void onImeSwitchButtonClickFromSystem(int displayId);
 
         InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId);
@@ -153,8 +170,10 @@
 
         void reportPerceptibleAsync(IBinder windowToken, boolean perceptible);
 
-        @PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
-        void removeImeSurface();
+        @PermissionVerified(allOf = {
+                Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                Manifest.permission.INTERNAL_SYSTEM_WINDOW})
+        void removeImeSurface(int displayId);
 
         void removeImeSurfaceFromWindowAsync(IBinder windowToken);
 
@@ -330,13 +349,14 @@
         mCallback.showInputMethodPickerFromClient(client, auxiliarySubtypeMode);
     }
 
-    @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @EnforcePermission(allOf = {
+            Manifest.permission.WRITE_SECURE_SETTINGS,
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL})
     @Override
     public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
         super.showInputMethodPickerFromSystem_enforcePermission();
 
         mCallback.showInputMethodPickerFromSystem(auxiliarySubtypeMode, displayId);
-
     }
 
     @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
@@ -347,7 +367,9 @@
         return mCallback.isInputMethodPickerShownForTest();
     }
 
-    @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @EnforcePermission(allOf = {
+            Manifest.permission.WRITE_SECURE_SETTINGS,
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL})
     @Override
     public void onImeSwitchButtonClickFromSystem(int displayId) {
         super.onImeSwitchButtonClickFromSystem_enforcePermission();
@@ -382,12 +404,14 @@
         mCallback.reportPerceptibleAsync(windowToken, perceptible);
     }
 
-    @EnforcePermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+    @EnforcePermission(allOf = {
+            Manifest.permission.INTERNAL_SYSTEM_WINDOW,
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL})
     @Override
-    public void removeImeSurface() {
+    public void removeImeSurface(int displayId) {
         super.removeImeSurface_enforcePermission();
 
-        mCallback.removeImeSurface();
+        mCallback.removeImeSurface(displayId);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index cdea6ff..42a99de 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -121,9 +121,9 @@
     @GuardedBy("ImfLock.class")
     private boolean mRequestedImeScreenshot;
 
-    /** The window token of the current visible IME layering target overlay. */
+    /** Whether there is a visible IME layering target overlay. */
     @GuardedBy("ImfLock.class")
-    private IBinder mCurVisibleImeLayeringOverlay;
+    private boolean mHasVisibleImeLayeringOverlay;
 
     /** The window token of the current visible IME input target. */
     @GuardedBy("ImfLock.class")
@@ -218,34 +218,36 @@
         mPolicy = imePolicy;
         mWindowManagerInternal.setInputMethodTargetChangeListener(new ImeTargetChangeListener() {
             @Override
-            public void onImeTargetOverlayVisibilityChanged(IBinder overlayWindowToken,
+            public void onImeTargetOverlayVisibilityChanged(@NonNull IBinder overlayWindowToken,
                     @WindowManager.LayoutParams.WindowType int windowType, boolean visible,
                     boolean removed) {
                 // Ignoring the starting window since it's ok to cover the IME target
                 // window in temporary without affecting the IME visibility.
-                final var overlay = (visible && !removed && windowType != TYPE_APPLICATION_STARTING)
-                                ? overlayWindowToken : null;
+                final boolean hasOverlay = visible && !removed
+                        && windowType != TYPE_APPLICATION_STARTING;
                 synchronized (ImfLock.class) {
-                    mCurVisibleImeLayeringOverlay = overlay;
-
+                    mHasVisibleImeLayeringOverlay = hasOverlay;
                 }
             }
 
             @Override
             public void onImeInputTargetVisibilityChanged(IBinder imeInputTarget,
                     boolean visibleRequested, boolean removed) {
+                final boolean visibleAndNotRemoved = visibleRequested && !removed;
                 synchronized (ImfLock.class) {
-                    if (mCurVisibleImeInputTarget == imeInputTarget && (!visibleRequested
-                            || removed)
-                            && mCurVisibleImeLayeringOverlay != null) {
+                    if (visibleAndNotRemoved) {
+                        mCurVisibleImeInputTarget = imeInputTarget;
+                        return;
+                    }
+                    if (mHasVisibleImeLayeringOverlay
+                            && mCurVisibleImeInputTarget == imeInputTarget) {
                         final int reason = SoftInputShowHideReason.HIDE_WHEN_INPUT_TARGET_INVISIBLE;
                         final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE,
                                 ImeTracker.ORIGIN_SERVER, reason, false /* fromUser */);
                         mService.onApplyImeVisibilityFromComputerLocked(imeInputTarget, statsToken,
                                 new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT, reason));
                     }
-                    mCurVisibleImeInputTarget =
-                            (visibleRequested && !removed) ? imeInputTarget : null;
+                    mCurVisibleImeInputTarget = null;
                 }
             }
         });
@@ -577,7 +579,6 @@
     }
 
     @GuardedBy("ImfLock.class")
-    @VisibleForTesting
     ImeVisibilityResult onInteractiveChanged(IBinder windowToken, boolean interactive) {
         final ImeTargetWindowState state = getWindowStateOrNull(windowToken);
         if (state != null && state.isRequestedImeVisible() && mInputShown && !interactive) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index 13209d8..dba0465 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -75,13 +75,13 @@
     public abstract void setInteractive(boolean interactive);
 
     /**
-     * Hides the input methods for all the users, if visible.
+     * Hides the input method for the specified {@code originatingDisplayId}, if visible.
      *
      * @param reason               the reason for hiding the current input method
      * @param originatingDisplayId the display ID the request is originated
      */
     @ImfLockFree
-    public abstract void hideAllInputMethods(@SoftInputShowHideReason int reason,
+    public abstract void hideInputMethod(@SoftInputShowHideReason int reason,
             int originatingDisplayId);
 
     /**
@@ -315,7 +315,7 @@
 
                 @ImfLockFree
                 @Override
-                public void hideAllInputMethods(@SoftInputShowHideReason int reason,
+                public void hideInputMethod(@SoftInputShowHideReason int reason,
                         int originatingDisplayId) {
                 }
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index effecd4..084e118 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -254,9 +254,7 @@
     private @interface MultiUserUnawareField {
     }
 
-    private static final int MSG_SHOW_IM_SUBTYPE_PICKER = 1;
-
-    private static final int MSG_HIDE_ALL_INPUT_METHODS = 1035;
+    private static final int MSG_HIDE_INPUT_METHOD = 1035;
     private static final int MSG_REMOVE_IME_SURFACE = 1060;
     private static final int MSG_REMOVE_IME_SURFACE_FROM_WINDOW = 1061;
 
@@ -474,6 +472,9 @@
         IInputMethodSession mSession;
         InputChannel mChannel;
 
+        @UserIdInt
+        final int mUserId;
+
         @Override
         public String toString() {
             return "SessionState{uid=" + mClient.mUid + " pid=" + mClient.mPid
@@ -482,15 +483,17 @@
                     + " session=" + Integer.toHexString(
                     System.identityHashCode(mSession))
                     + " channel=" + mChannel
+                    + " userId=" + mUserId
                     + "}";
         }
 
         SessionState(ClientState client, IInputMethodInvoker method,
-                IInputMethodSession session, InputChannel channel) {
+                IInputMethodSession session, InputChannel channel, @UserIdInt int userId) {
             mClient = client;
             mMethod = method;
             mSession = session;
             mChannel = channel;
+            mUserId = userId;
         }
     }
 
@@ -994,8 +997,6 @@
                     Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
             ioThread.start();
 
-            SecureSettingsWrapper.setContentResolver(context.getContentResolver());
-
             return new InputMethodManagerService(context,
                     shouldEnableConcurrentMultiUserMode(context), thread.getLooper(),
                     Handler.createAsync(ioThread.getLooper()),
@@ -1054,7 +1055,6 @@
         public void onUserRemoved(UserInfo user) {
             // Called directly from UserManagerService. Do not block the calling thread.
             final int userId = user.id;
-            SecureSettingsWrapper.onUserRemoved(userId);
             AdditionalSubtypeMapRepository.remove(userId);
             InputMethodSettingsRepository.remove(userId);
             mService.mUserDataRepository.remove(userId);
@@ -1126,6 +1126,21 @@
                 }
             });
         }
+
+        @Override
+        public void onUserStopped(@NonNull TargetUser user) {
+            final int userId = user.getUserIdentifier();
+            // Called on ActivityManager thread.
+            SecureSettingsWrapper.onUserStopped(userId);
+            mService.mIoHandler.post(() -> {
+                final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId);
+                final var settings = InputMethodManagerService.queryInputMethodServicesInternal(
+                        mService.mContext, userId, additionalSubtypeMap,
+                        DirectBootAwareness.AUTO).getMethodMap();
+                InputMethodSettingsRepository.put(userId,
+                        InputMethodSettings.create(settings, userId));
+            });
+        }
     }
 
     @GuardedBy("ImfLock.class")
@@ -1160,6 +1175,7 @@
             mConcurrentMultiUserModeEnabled = concurrentMultiUserModeEnabled;
             mContext = context;
             mRes = context.getResources();
+            SecureSettingsWrapper.onStart(mContext);
 
             mHandler = Handler.createAsync(uiLooper, this);
             mIoHandler = ioHandler;
@@ -1843,13 +1859,6 @@
         }
     }
 
-    @VisibleForTesting
-    void setAttachedClientForTesting(@NonNull ClientState cs) {
-        synchronized (ImfLock.class) {
-            getUserData(mCurrentUserId).mCurClient = cs;
-        }
-    }
-
     @GuardedBy("ImfLock.class")
     private boolean isShowRequestedForCurrentWindow(@UserIdInt int userId) {
         final var userData = getUserData(userId);
@@ -2349,7 +2358,7 @@
                     if (userData.mCurClient != null) {
                         clearClientSessionLocked(userData.mCurClient);
                         userData.mCurClient.mCurSession = new SessionState(
-                                userData.mCurClient, method, session, channel);
+                                userData.mCurClient, method, session, channel, userId);
                         InputBindResult res = attachNewInputLocked(
                                 StartInputReason.SESSION_CREATED_BY_IME, true, userId);
                         attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_IME, true,
@@ -2490,9 +2499,7 @@
                     sessionState.mSession.finishSession();
                 } catch (RemoteException e) {
                     Slog.w(TAG, "Session failed to close due to remote exception", e);
-                    // TODO(b/350386877): Propagate userId from the caller or infer it from
-                    //  sessionState
-                    final int userId = mCurrentUserId;
+                    final int userId = sessionState.mUserId;
                     final var bindingController = getInputMethodBindingController(userId);
                     updateSystemUiLocked(0 /* vis */, bindingController.getBackDisposition(),
                             userId);
@@ -3038,6 +3045,9 @@
 
     @GuardedBy("ImfLock.class")
     private void sendResultReceiverFailureLocked(@Nullable ResultReceiver resultReceiver) {
+        if (resultReceiver == null) {
+            return;
+        }
         final boolean isInputShown = mVisibilityStateComputer.isInputShown();
         resultReceiver.send(isInputShown
                 ? InputMethodManager.RESULT_UNCHANGED_SHOWN
@@ -3947,10 +3957,9 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean canShowInputMethodPickerLocked(IInputMethodClient client) {
+    private boolean canShowInputMethodPickerLocked(IInputMethodClient client,
+            @UserIdInt int userId) {
         final int uid = Binder.getCallingUid();
-        // TODO(b/305849394): Get userId from callers.
-        final int userId = mCurrentUserId;
         final var userData = getUserData(userId);
         if (userData.mImeBindingState.mFocusedWindowClient != null && client != null
                 && userData.mImeBindingState.mFocusedWindowClient.mClient.asBinder()
@@ -3977,29 +3986,38 @@
         }
         final int callingUserId = UserHandle.getCallingUserId();
         synchronized (ImfLock.class) {
-            if (!canShowInputMethodPickerLocked(client)) {
+            final int userId = resolveImeUserIdLocked(callingUserId);
+            if (!canShowInputMethodPickerLocked(client, userId)) {
                 Slog.w(TAG, "Ignoring showInputMethodPickerFromClient of uid "
                         + Binder.getCallingUid() + ": " + client);
                 return;
             }
-            final int userId = resolveImeUserIdLocked(callingUserId);
             final var userData = getUserData(userId);
             // Always call subtype picker, because subtype picker is a superset of input method
             // picker.
             final int displayId = (userData.mCurClient != null)
                     ? userData.mCurClient.mSelfReportedDisplayId : DEFAULT_DISPLAY;
-            mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId)
-                    .sendToTarget();
+            mHandler.post(() -> {
+                synchronized (ImfLock.class) {
+                    showInputMethodPickerLocked(auxiliarySubtypeMode, displayId, userId);
+                }
+            });
         }
     }
 
-    @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @IInputMethodManagerImpl.PermissionVerified(allOf = {
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+            Manifest.permission.WRITE_SECURE_SETTINGS})
     @Override
     public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
         // Always call subtype picker, because subtype picker is a superset of input method
         // picker.
-        mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId)
-                .sendToTarget();
+        mHandler.post(() -> {
+            synchronized (ImfLock.class) {
+                final int userId = resolveImeUserIdFromDisplayIdLocked(displayId);
+                showInputMethodPickerLocked(auxiliarySubtypeMode, displayId, userId);
+            }
+        });
     }
 
     /**
@@ -4083,7 +4101,9 @@
         }
     }
 
-    @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @IInputMethodManagerImpl.PermissionVerified(allOf = {
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+            Manifest.permission.WRITE_SECURE_SETTINGS})
     @Override
     public void onImeSwitchButtonClickFromSystem(int displayId) {
         synchronized (ImfLock.class) {
@@ -4425,9 +4445,11 @@
         });
     }
 
-    @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+    @IInputMethodManagerImpl.PermissionVerified(allOf = {
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+            Manifest.permission.INTERNAL_SYSTEM_WINDOW})
     @Override
-    public void removeImeSurface() {
+    public void removeImeSurface(int displayId) {
         mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget();
     }
 
@@ -4990,89 +5012,80 @@
         userData.mEnabledAccessibilitySessions = accessibilitySessions;
     }
 
+    @GuardedBy("ImfLock.class")
+    private void showInputMethodPickerLocked(int auxiliarySubtypeMode, int displayId,
+            @UserIdInt int userId) {
+        final boolean showAuxSubtypes;
+        switch (auxiliarySubtypeMode) {
+            // This is undocumented so far, but IMM#showInputMethodPicker() has been
+            // implemented so that auxiliary subtypes will be excluded when the soft
+            // keyboard is invisible.
+            case InputMethodManager.SHOW_IM_PICKER_MODE_AUTO ->
+                    showAuxSubtypes = mVisibilityStateComputer.isInputShown();
+            case InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES ->
+                    showAuxSubtypes = true;
+            case InputMethodManager.SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES ->
+                    showAuxSubtypes = false;
+            default -> {
+                Slog.e(TAG, "Unknown subtype picker mode=" + auxiliarySubtypeMode);
+                return;
+            }
+        }
+        final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
+        final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
+                && mWindowManagerInternal.isKeyguardSecure(userId);
+        final String lastInputMethodId = settings.getSelectedInputMethod();
+        int lastInputMethodSubtypeId = settings.getSelectedInputMethodSubtypeId(lastInputMethodId);
+
+        final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
+                .getSortedInputMethodAndSubtypeList(
+                        showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
+                        mContext, settings);
+        if (imList.isEmpty()) {
+            Slog.w(TAG, "Show switching menu failed, imList is empty,"
+                    + " showAuxSubtypes: " + showAuxSubtypes
+                    + " isScreenLocked: " + isScreenLocked
+                    + " userId: " + userId);
+            return;
+        }
+
+        if (mNewInputMethodSwitcherMenuEnabled) {
+            if (DEBUG) {
+                Slog.v(TAG, "Show IME switcher menu,"
+                        + " showAuxSubtypes=" + showAuxSubtypes
+                        + " displayId=" + displayId
+                        + " preferredInputMethodId=" + lastInputMethodId
+                        + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId);
+            }
+
+            final var itemsAndIndex = getInputMethodPickerItems(imList,
+                    lastInputMethodId, lastInputMethodSubtypeId, userId);
+            final var menuItems = itemsAndIndex.first;
+            final int selectedIndex = itemsAndIndex.second;
+
+            if (selectedIndex == -1) {
+                Slog.w(TAG, "Switching menu shown with no item selected"
+                        + ", IME id: " + lastInputMethodId
+                        + ", subtype index: " + lastInputMethodSubtypeId);
+            }
+
+            mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId);
+        } else {
+            mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
+                    lastInputMethodId, lastInputMethodSubtypeId, imList);
+        }
+    }
+
     @SuppressWarnings("unchecked")
     @UiThread
     @Override
     public boolean handleMessage(Message msg) {
         switch (msg.what) {
-            case MSG_SHOW_IM_SUBTYPE_PICKER:
-                final boolean showAuxSubtypes;
-                final int displayId = msg.arg2;
-                switch (msg.arg1) {
-                    case InputMethodManager.SHOW_IM_PICKER_MODE_AUTO:
-                        // This is undocumented so far, but IMM#showInputMethodPicker() has been
-                        // implemented so that auxiliary subtypes will be excluded when the soft
-                        // keyboard is invisible.
-                        synchronized (ImfLock.class) {
-                            showAuxSubtypes = mVisibilityStateComputer.isInputShown();
-                        }
-                        break;
-                    case InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES:
-                        showAuxSubtypes = true;
-                        break;
-                    case InputMethodManager.SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES:
-                        showAuxSubtypes = false;
-                        break;
-                    default:
-                        Slog.e(TAG, "Unknown subtype picker mode = " + msg.arg1);
-                        return false;
-                }
+            case MSG_HIDE_INPUT_METHOD: {
+                @SoftInputShowHideReason final int reason = msg.arg1;
+                final int originatingDisplayId = msg.arg2;
                 synchronized (ImfLock.class) {
-                    final InputMethodSettings settings =
-                            InputMethodSettingsRepository.get(mCurrentUserId);
-                    final int userId = settings.getUserId();
-                    final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
-                            && mWindowManagerInternal.isKeyguardSecure(userId);
-                    final String lastInputMethodId = settings.getSelectedInputMethod();
-                    int lastInputMethodSubtypeId =
-                            settings.getSelectedInputMethodSubtypeId(lastInputMethodId);
-
-                    final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
-                            .getSortedInputMethodAndSubtypeList(
-                                    showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
-                                    mContext, settings);
-                    if (imList.isEmpty()) {
-                        Slog.w(TAG, "Show switching menu failed, imList is empty,"
-                                + " showAuxSubtypes: " + showAuxSubtypes
-                                + " isScreenLocked: " + isScreenLocked
-                                + " userId: " + userId);
-                        return false;
-                    }
-
-                    if (mNewInputMethodSwitcherMenuEnabled) {
-                        if (DEBUG) {
-                            Slog.v(TAG, "Show IME switcher menu,"
-                                    + " showAuxSubtypes=" + showAuxSubtypes
-                                    + " displayId=" + displayId
-                                    + " preferredInputMethodId=" + lastInputMethodId
-                                    + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId);
-                        }
-
-                        final var itemsAndIndex = getInputMethodPickerItems(imList,
-                                lastInputMethodId, lastInputMethodSubtypeId, userId);
-                        final var menuItems = itemsAndIndex.first;
-                        final int selectedIndex = itemsAndIndex.second;
-
-                        if (selectedIndex == -1) {
-                            Slog.w(TAG, "Switching menu shown with no item selected"
-                                    + ", IME id: " + lastInputMethodId
-                                    + ", subtype index: " + lastInputMethodSubtypeId);
-                        }
-
-                        mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId);
-                    } else {
-                        mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
-                                lastInputMethodId, lastInputMethodSubtypeId, imList);
-                    }
-                }
-                return true;
-
-            // ---------------------------------------------------------
-
-            case MSG_HIDE_ALL_INPUT_METHODS:
-                synchronized (ImfLock.class) {
-                    // TODO(b/305849394): Needs to figure out what to do where for background users.
-                    final int userId = mCurrentUserId;
+                    final int userId = resolveImeUserIdFromDisplayIdLocked(originatingDisplayId);
                     final var userData = getUserData(userId);
                     if (Flags.refactorInsetsController()) {
                         if (userData.mImeBindingState != null
@@ -5080,15 +5093,16 @@
                                 && userData.mImeBindingState.mFocusedWindowClient.mClient != null) {
                             userData.mImeBindingState.mFocusedWindowClient.mClient
                                     .setImeVisibility(false,
-                                    null /* TODO(b329229469) check statsToken */);
+                                            null /* TODO(b329229469) check statsToken */);
                         }
                     } else {
-                        @SoftInputShowHideReason final int reason = (int) msg.obj;
+
                         hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
                                 0 /* flags */, reason, userId);
                     }
                 }
                 return true;
+            }
             case MSG_REMOVE_IME_SURFACE: {
                 synchronized (ImfLock.class) {
                     // TODO(b/305849394): Needs to figure out what to do where for background users.
@@ -5803,10 +5817,11 @@
 
         @ImfLockFree
         @Override
-        public void hideAllInputMethods(@SoftInputShowHideReason int reason,
+        public void hideInputMethod(@SoftInputShowHideReason int reason,
                 int originatingDisplayId) {
-            mHandler.removeMessages(MSG_HIDE_ALL_INPUT_METHODS);
-            mHandler.obtainMessage(MSG_HIDE_ALL_INPUT_METHODS, reason).sendToTarget();
+            mHandler.removeMessages(MSG_HIDE_INPUT_METHOD);
+            mHandler.obtainMessage(MSG_HIDE_INPUT_METHOD, reason, originatingDisplayId)
+                    .sendToTarget();
         }
 
         @ImfLockFree
diff --git a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
index b3500be..476888e 100644
--- a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
+++ b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
@@ -20,7 +20,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.app.ActivityManagerInternal;
 import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.UserInfo;
 import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -318,13 +321,30 @@
     }
 
     /**
-     * Called when the system is starting.
+     * Called when {@link InputMethodManagerService} is starting.
      *
-     * @param contentResolver the {@link ContentResolver} to be used
+     * @param context the {@link Context} to be used.
      */
     @AnyThread
-    static void setContentResolver(@NonNull ContentResolver contentResolver) {
-        sContentResolver = contentResolver;
+    static void onStart(@NonNull Context context) {
+        sContentResolver = context.getContentResolver();
+
+        final int userId = LocalServices.getService(ActivityManagerInternal.class)
+                .getCurrentUserId();
+        final UserManagerInternal userManagerInternal =
+                LocalServices.getService(UserManagerInternal.class);
+        putOrGet(userId, createImpl(userManagerInternal, userId));
+
+        userManagerInternal.addUserLifecycleListener(
+                new UserManagerInternal.UserLifecycleListener() {
+                    @Override
+                    public void onUserRemoved(UserInfo user) {
+                        synchronized (sMutationLock) {
+                            sUserMap = sUserMap.cloneWithRemoveOrSelf(user.id);
+                        }
+                    }
+                }
+        );
     }
 
     /**
@@ -357,14 +377,19 @@
     }
 
     /**
-     * Called when a user is being removed.
+     * Called when a user is stopped, which changes the user storage to the locked state again.
      *
-     * @param userId the ID of the user whose storage is being removed.
+     * @param userId the ID of the user whose storage is being locked again.
      */
     @AnyThread
-    static void onUserRemoved(@UserIdInt int userId) {
+    static void onUserStopped(@UserIdInt int userId) {
+        final LockedUserImpl lockedUserImpl = new LockedUserImpl(userId, sContentResolver);
         synchronized (sMutationLock) {
-            sUserMap = sUserMap.cloneWithRemoveOrSelf(userId);
+            final ReaderWriter current = sUserMap.get(userId);
+            if (current == null || current instanceof LockedUserImpl) {
+                return;
+            }
+            sUserMap = sUserMap.cloneWithPutOrSelf(userId, lockedUserImpl);
         }
     }
 
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index fdb2e6f..c940a9c 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -252,7 +252,9 @@
         offload(() -> mInner.showInputMethodPickerFromClient(client, auxiliarySubtypeMode));
     }
 
-    @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @IInputMethodManagerImpl.PermissionVerified(allOf = {
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+            Manifest.permission.WRITE_SECURE_SETTINGS})
     @Override
     public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
         mInner.showInputMethodPickerFromSystem(auxiliarySubtypeMode, displayId);
@@ -264,7 +266,9 @@
         return mInner.isInputMethodPickerShownForTest();
     }
 
-    @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+    @IInputMethodManagerImpl.PermissionVerified(allOf = {
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+            Manifest.permission.WRITE_SECURE_SETTINGS})
     @Override
     public void onImeSwitchButtonClickFromSystem(int displayId) {
         mInner.onImeSwitchButtonClickFromSystem(displayId);
@@ -298,10 +302,12 @@
         mInner.reportPerceptibleAsync(windowToken, perceptible);
     }
 
-    @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+    @IInputMethodManagerImpl.PermissionVerified(allOf = {
+            Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+            Manifest.permission.INTERNAL_SYSTEM_WINDOW})
     @Override
-    public void removeImeSurface() {
-        mInner.removeImeSurface();
+    public void removeImeSurface(int displayId) {
+        mInner.removeImeSurface(displayId);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 1a8e44b..1fdb57c 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -950,7 +950,7 @@
                 || isPackageOrComponentAllowed(component.getPackageName(), userId))) {
             return false;
         }
-        return componentHasBindPermission(component, userId);
+        return isValidService(component, userId);
     }
 
     private boolean componentHasBindPermission(ComponentName component, int userId) {
@@ -1302,11 +1302,12 @@
                     if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) {
                         final ComponentName component = ComponentName.unflattenFromString(
                                 approvedPackageOrComponent);
-                        if (component != null && !componentHasBindPermission(component, userId)) {
+                        if (component != null && !isValidService(component, userId)) {
                             approved.removeAt(j);
                             if (DEBUG) {
                                 Slog.v(TAG, "Removing " + approvedPackageOrComponent
-                                        + " from approved list; no bind permission found "
+                                        + " from approved list; no bind permission or "
+                                        + "service interface filter found "
                                         + mConfig.bindPermission);
                             }
                         }
@@ -1325,6 +1326,11 @@
         }
     }
 
+    protected boolean isValidService(ComponentName component, int userId) {
+        return componentHasBindPermission(component, userId) && queryPackageForServices(
+                component.getPackageName(), userId).contains(component);
+    }
+
     protected boolean isValidEntry(String packageOrComponent, int userId) {
         return hasMatchingServices(packageOrComponent, userId);
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 016abff..4179edd 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1315,10 +1315,24 @@
                         nv.rank, nv.count);
 
                 StatusBarNotification sbn = r.getSbn();
-                cancelNotification(callingUid, callingPid, sbn.getPackageName(), sbn.getTag(),
-                        sbn.getId(), FLAG_AUTO_CANCEL,
-                        FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_BUBBLE,
-                        false, r.getUserId(), REASON_CLICK, nv.rank, nv.count, null);
+                // Notifications should be cancelled on click if they have been lifetime extended,
+                // regardless of presence or absence of FLAG_AUTO_CANCEL.
+                if (lifetimeExtensionRefactor()
+                        && (sbn.getNotification().flags
+                        & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) != 0) {
+                    cancelNotification(callingUid, callingPid, sbn.getPackageName(), sbn.getTag(),
+                            sbn.getId(), FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
+                            FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
+                                    | FLAG_BUBBLE,
+                            false, r.getUserId(), REASON_CLICK, nv.rank, nv.count, null);
+
+                } else {
+                    // Otherwise, only FLAG_AUTO_CANCEL notifications should be canceled on click.
+                    cancelNotification(callingUid, callingPid, sbn.getPackageName(), sbn.getTag(),
+                            sbn.getId(), FLAG_AUTO_CANCEL,
+                            FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_BUBBLE,
+                            false, r.getUserId(), REASON_CLICK, nv.rank, nv.count, null);
+                }
                 nv.recycle();
                 reportUserInteraction(r);
                 mAssistants.notifyAssistantNotificationClicked(r);
diff --git a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
index 15e758c..cf0c6c2 100644
--- a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
+++ b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
@@ -236,6 +236,7 @@
         return new Notification.Builder(mSystemUserContext, BUSN_CHANNEL_ID)
                 .setSmallIcon(icon)
                 .setTicker(title)
+                .setCategory(Notification.CATEGORY_REMINDER)
                 .setWhen(0)
                 .setOngoing(true)
                 .setColor(fgContext.getColor(R.color.system_notification_accent_color))
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index a17c48d..a0d5ea8 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -501,9 +501,9 @@
             mPm.setUpCustomResolverActivity(pkg, pkgSetting);
         }
 
-        // When upgrading a package, pkgSetting is copied from oldPkgSetting. Clear the app
-        // metadata file path for the new package.
-        if (oldPkgSetting != null) {
+        // When upgrading a package, clear the app metadata file path for the new package.
+        if (oldPkgSetting != null
+                && oldPkgSetting.getLastUpdateTime() < pkgSetting.getLastUpdateTime()) {
             pkgSetting.setAppMetadataFilePath(null);
             pkgSetting.setAppMetadataSource(APP_METADATA_SOURCE_UNKNOWN);
         }
@@ -3825,13 +3825,13 @@
                 // This also has the (beneficial) side effect where if a package disappears from an
                 // APEX, leaving only a /data copy, it will lose its apexModuleName.
                 //
-                // This must be done before scanSystemPackageLI as that will throw in the case of a
+                // This must be done before scanPackageForInitLI as that will throw in the case of a
                 // system -> data package.
                 disabledPkgSetting.setApexModuleName(activeApexInfo.apexModuleName);
             }
         }
 
-        final Pair<ScanResult, Boolean> scanResultPair = scanSystemPackageLI(
+        final Pair<ScanResult, Boolean> scanResultPair = scanPackageForInitLI(
                 parsedPackage, parseFlags, scanFlags, user);
         final ScanResult scanResult = scanResultPair.first;
         boolean shouldHideSystemApp = scanResultPair.second;
@@ -4066,7 +4066,7 @@
         }
     }
 
-    private Pair<ScanResult, Boolean> scanSystemPackageLI(ParsedPackage parsedPackage,
+    private Pair<ScanResult, Boolean> scanPackageForInitLI(ParsedPackage parsedPackage,
             @ParsingPackageUtils.ParseFlags int parseFlags,
             @PackageManagerService.ScanFlags int scanFlags,
             @Nullable UserHandle user) throws PackageManagerException {
@@ -4179,7 +4179,7 @@
                         ParsingPackageUtils.getSigningDetails(input, parsedPackage,
                                 false /*skipVerify*/);
                 if (result.isError()) {
-                    throw new PrepareFailure("Failed collect during scanSystemPackageLI",
+                    throw new PrepareFailure("Failed collect during scanPackageForInitLI",
                             result.getException());
                 }
                 disabledPkgSetting.setSigningDetails(result.getResult());
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index a1dffc6..9757582 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -1452,6 +1452,13 @@
             }
 
             if (!ArrayUtils.isEmpty(after.splitNames)) {
+                if (beforeSplitNames.length != beforeSplitRevisionCodes.length) {
+                    throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
+                            "Current split names and the split revision codes are not 1:1 mapping."
+                                    + "This indicates that the package info data has been"
+                                    + " corrupted.");
+                }
+
                 for (int i = 0; i < after.splitNames.length; i++) {
                     final String splitName = after.splitNames[i];
                     final int j = ArrayUtils.indexOf(beforeSplitNames, splitName);
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 9177e2b..b7dfd8d 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -4489,10 +4489,24 @@
         String splitName = parser.getAttributeValue(null, ATTR_NAME);
         int splitRevision = parser.getAttributeInt(null, ATTR_VERSION, -1);
         if (splitName != null && splitRevision >= 0) {
+            final int beforeSplitNamesLength = outPs.getSplitNames().length;
+            // If the split name already exists in the outPs#getSplitNames, don't add it
+            // into the array and update its revision code below
             outPs.setSplitNames(ArrayUtils.appendElement(String.class,
                     outPs.getSplitNames(), splitName));
-            outPs.setSplitRevisionCodes(ArrayUtils.appendInt(
-                    outPs.getSplitRevisionCodes(), splitRevision));
+
+            // If the same split name has already been added before, update the latest
+            // revision code
+            final int afterSplitNamesLength = outPs.getSplitNames().length;
+            if (beforeSplitNamesLength == afterSplitNamesLength) {
+                final int index = ArrayUtils.indexOf(outPs.getSplitNames(), splitName);
+                final int[] splitRevisionCodes = outPs.getSplitRevisionCodes();
+                splitRevisionCodes[index] = splitRevision;
+                outPs.setSplitRevisionCodes(splitRevisionCodes);
+            } else {
+                outPs.setSplitRevisionCodes(ArrayUtils.appendInt(
+                        outPs.getSplitRevisionCodes(), splitRevision, /* allowDuplicates= */ true));
+            }
         }
 
         XmlUtils.skipCurrentTag(parser);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 7534bfe..d0706d2 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1158,8 +1158,7 @@
                     break;
                 case SHORT_PRESS_POWER_CLOSE_IME_OR_GO_HOME: {
                     if (mDismissImeOnBackKeyPressed) {
-                        // TODO(b/308479256): Check if hiding "all" IMEs is OK or not.
-                        InputMethodManagerInternal.get().hideAllInputMethods(
+                        InputMethodManagerInternal.get().hideInputMethod(
                                 SoftInputShowHideReason.HIDE_POWER_BUTTON_GO_HOME, displayId);
                     } else {
                         shortPressPowerGoHome();
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index aa5f5a24..b28da55b 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -375,9 +375,9 @@
             final boolean unimportantForLogging = newOwnerUid == Process.SYSTEM_UID
                     && (newFlags & PowerManager.UNIMPORTANT_FOR_LOGGING) != 0;
             try {
-                mBatteryStats.noteChangeWakelockFromSource(workSource, ownerPid, tag, historyTag,
-                        monitorType, newWorkSource, newOwnerPid, newTag, newHistoryTag,
-                        newMonitorType, unimportantForLogging);
+                notifyWakelockChanging(workSource, ownerPid, tag,
+                            historyTag, monitorType, newWorkSource, newOwnerPid, newTag,
+                            newHistoryTag, newMonitorType, unimportantForLogging);
             } catch (RemoteException ex) {
                 // Ignore
             }
@@ -1127,6 +1127,29 @@
         mWakeLockLog.onWakeLockReleased(tag, ownerUid, currentTime);
     }
 
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    private void notifyWakelockChanging(WorkSource workSource, int ownerPid, String tag,
+            String historyTag, int monitorType, WorkSource newWorkSource, int newOwnerPid,
+            String newTag, String newHistoryTag, int newMonitorType, boolean unimportantForLogging)
+            throws RemoteException {
+        if (!mFlags.improveWakelockLatency()) {
+            mBatteryStats.noteChangeWakelockFromSource(workSource, ownerPid, tag,
+                    historyTag, monitorType, newWorkSource, newOwnerPid, newTag,
+                    newHistoryTag, newMonitorType, unimportantForLogging);
+        } else {
+            mHandler.post(() -> {
+                try {
+                    mBatteryStats.noteChangeWakelockFromSource(workSource, ownerPid, tag,
+                            historyTag, monitorType, newWorkSource, newOwnerPid, newTag,
+                            newHistoryTag, newMonitorType, unimportantForLogging);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Failed to notify the wakelock changing from source via "
+                            + "Notifier." + e.getLocalizedMessage());
+                }
+            });
+        }
+    }
+
     private final class NotifierHandler extends Handler {
 
         public NotifierHandler(Looper looper) {
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 10faf14..ecb0c30b 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -34,6 +34,7 @@
 import static com.android.internal.util.LatencyTracker.ACTION_TURN_ON_SCREEN;
 import static com.android.server.deviceidle.Flags.disableWakelocksInLightIdle;
 import static com.android.server.display.DisplayDeviceConfig.INVALID_BRIGHTNESS_IN_CONFIG;
+import static com.android.server.display.brightness.BrightnessUtils.isValidBrightnessValue;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -650,11 +651,16 @@
 
     private int mDozeScreenStateOverrideReasonFromDreamManager = Display.STATE_REASON_UNKNOWN;
 
-    // The screen brightness to use while dozing.
+    // The screen brightness between 1 and 255 to use while dozing.
     private int mDozeScreenBrightnessOverrideFromDreamManager = PowerManager.BRIGHTNESS_DEFAULT;
 
+    /**
+     * The screen brightness between {@link PowerManager#BRIGHTNESS_MIN} and
+     * {@link PowerManager.BRIGHTNESS_MAX} to use while dozing.
+     */
     private float mDozeScreenBrightnessOverrideFromDreamManagerFloat =
             PowerManager.BRIGHTNESS_INVALID_FLOAT;
+
     // Keep display state when dozing.
     private boolean mDrawWakeLockOverrideFromSidekick;
 
@@ -4455,15 +4461,21 @@
     }
 
     private void setDozeOverrideFromDreamManagerInternal(
-            int screenState, @Display.StateReason int reason, int screenBrightness) {
+            int screenState, @Display.StateReason int reason, float screenBrightnessFloat,
+            int screenBrightnessInt) {
         synchronized (mLock) {
             if (mDozeScreenStateOverrideFromDreamManager != screenState
-                    || mDozeScreenBrightnessOverrideFromDreamManager != screenBrightness) {
+                    || mDozeScreenBrightnessOverrideFromDreamManager != screenBrightnessInt
+                    || !BrightnessSynchronizer.floatEquals(
+                    mDozeScreenBrightnessOverrideFromDreamManagerFloat,
+                    screenBrightnessFloat)) {
                 mDozeScreenStateOverrideFromDreamManager = screenState;
                 mDozeScreenStateOverrideReasonFromDreamManager = reason;
-                mDozeScreenBrightnessOverrideFromDreamManager = screenBrightness;
+                mDozeScreenBrightnessOverrideFromDreamManager = screenBrightnessInt;
                 mDozeScreenBrightnessOverrideFromDreamManagerFloat =
-                        BrightnessSynchronizer.brightnessIntToFloat(mDozeScreenBrightnessOverrideFromDreamManager);
+                        isValidBrightnessValue(screenBrightnessFloat)
+                                ? screenBrightnessFloat
+                                : BrightnessSynchronizer.brightnessIntToFloat(screenBrightnessInt);
                 mDirty |= DIRTY_SETTINGS;
                 updatePowerStateLocked();
             }
@@ -7095,7 +7107,7 @@
 
         @Override
         public void setDozeOverrideFromDreamManager(
-                int screenState, int reason, int screenBrightness) {
+                int screenState, int reason, float screenBrightnessFloat, int screenBrightnessInt) {
             switch (screenState) {
                 case Display.STATE_UNKNOWN:
                 case Display.STATE_OFF:
@@ -7108,11 +7120,17 @@
                     screenState = Display.STATE_UNKNOWN;
                     break;
             }
-            if (screenBrightness < PowerManager.BRIGHTNESS_DEFAULT
-                    || screenBrightness > PowerManager.BRIGHTNESS_ON) {
-                screenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
+            if (screenBrightnessInt < PowerManager.BRIGHTNESS_DEFAULT
+                    || screenBrightnessInt > PowerManager.BRIGHTNESS_ON) {
+                screenBrightnessInt = PowerManager.BRIGHTNESS_DEFAULT;
             }
-            setDozeOverrideFromDreamManagerInternal(screenState, reason, screenBrightness);
+            if (screenBrightnessFloat != PowerManager.BRIGHTNESS_OFF_FLOAT
+                    && (screenBrightnessFloat < PowerManager.BRIGHTNESS_MIN
+                    || screenBrightnessFloat > PowerManager.BRIGHTNESS_MAX)) {
+                screenBrightnessFloat = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+            }
+            setDozeOverrideFromDreamManagerInternal(screenState, reason, screenBrightnessFloat,
+                    screenBrightnessInt);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/power/hint/TEST_MAPPING b/services/core/java/com/android/server/power/hint/TEST_MAPPING
index 34c25c6..5450700 100644
--- a/services/core/java/com/android/server/power/hint/TEST_MAPPING
+++ b/services/core/java/com/android/server/power/hint/TEST_MAPPING
@@ -1,27 +1,18 @@
 {
   "presubmit": [
     {
-      "name": "FrameworksServicesTests",
+      "name": "PerformanceHintTests",
       "options": [
-        {
-          "include-filter": "com.android.server.power.hint"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
+        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+        {"exclude-annotation": "org.junit.Ignore"}
       ]
-    }
-  ],
-  "postsubmit": [
+    },
     {
       "name": "CtsStatsdAtomHostTestCases",
       "options": [
+        {"include-filter": "android.cts.statsdatom.performancehintmanager"},
         {"exclude-annotation": "androidx.test.filters.FlakyTest"},
-        {"exclude-annotation": "org.junit.Ignore"},
-        {"include-filter": "android.cts.statsdatom.performancehintmanager"}
-      ],
-      "file_patterns": [
-        "(/|^)HintManagerService.java"
+        {"exclude-annotation": "org.junit.Ignore"}
       ]
     }
   ]
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index c4b37c69..143b3ff 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -5431,8 +5431,6 @@
         }
     }
 
-    int mSensorNesting;
-
     @GuardedBy("this")
     public void noteStartSensorLocked(int uid, int sensor) {
         noteStartSensorLocked(uid, sensor, mClock.elapsedRealtime(), mClock.uptimeMillis());
@@ -5441,11 +5439,8 @@
     @GuardedBy("this")
     public void noteStartSensorLocked(int uid, int sensor, long elapsedRealtimeMs, long uptimeMs) {
         uid = mapUid(uid);
-        if (mSensorNesting == 0) {
-            mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
-                    HistoryItem.STATE_SENSOR_ON_FLAG);
-        }
-        mSensorNesting++;
+        mHistory.recordStateStartEvent(elapsedRealtimeMs, uptimeMs,
+                HistoryItem.STATE_SENSOR_ON_FLAG, uid, "sensor:0x" + Integer.toHexString(sensor));
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
                 .noteStartSensor(sensor, elapsedRealtimeMs);
     }
@@ -5458,11 +5453,8 @@
     @GuardedBy("this")
     public void noteStopSensorLocked(int uid, int sensor, long elapsedRealtimeMs, long uptimeMs) {
         uid = mapUid(uid);
-        mSensorNesting--;
-        if (mSensorNesting == 0) {
-            mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
-                    HistoryItem.STATE_SENSOR_ON_FLAG);
-        }
+        mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs,
+                HistoryItem.STATE_SENSOR_ON_FLAG, uid, "sensor:0x" + Integer.toHexString(sensor));
         getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs)
                 .noteStopSensor(sensor, elapsedRealtimeMs);
     }
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index a5e4cf5..b308f38 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -93,8 +93,10 @@
                 if (!mPowerStatsExporterEnabled.get(BatteryConsumer.POWER_COMPONENT_BLUETOOTH)) {
                     mPowerCalculators.add(new BluetoothPowerCalculator(mPowerProfile));
                 }
-                mPowerCalculators.add(new SensorPowerCalculator(
-                        mContext.getSystemService(SensorManager.class)));
+                if (!mPowerStatsExporterEnabled.get(BatteryConsumer.POWER_COMPONENT_SENSORS)) {
+                    mPowerCalculators.add(new SensorPowerCalculator(
+                            mContext.getSystemService(SensorManager.class)));
+                }
                 if (!mPowerStatsExporterEnabled.get(BatteryConsumer.POWER_COMPONENT_GNSS)) {
                     mPowerCalculators.add(new GnssPowerCalculator(mPowerProfile));
                 }
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
index 39954b8..5f41090 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
@@ -59,58 +59,61 @@
      */
     public void exportAggregatedPowerStats(BatteryUsageStats.Builder batteryUsageStatsBuilder,
             long monotonicStartTime, long monotonicEndTime) {
-        boolean hasStoredSpans = false;
-        long maxEndTime = monotonicStartTime;
-        List<PowerStatsSpan.Metadata> spans = mPowerStatsStore.getTableOfContents();
-        for (int i = spans.size() - 1; i >= 0; i--) {
-            PowerStatsSpan.Metadata metadata = spans.get(i);
-            if (!metadata.getSections().contains(AggregatedPowerStatsSection.TYPE)) {
-                continue;
-            }
-
-            List<PowerStatsSpan.TimeFrame> timeFrames = metadata.getTimeFrames();
-            long spanMinTime = Long.MAX_VALUE;
-            long spanMaxTime = Long.MIN_VALUE;
-            for (int j = 0; j < timeFrames.size(); j++) {
-                PowerStatsSpan.TimeFrame timeFrame = timeFrames.get(j);
-                long startMonotonicTime = timeFrame.startMonotonicTime;
-                long endMonotonicTime = startMonotonicTime + timeFrame.duration;
-                if (startMonotonicTime < spanMinTime) {
-                    spanMinTime = startMonotonicTime;
+        synchronized (this) {
+            boolean hasStoredSpans = false;
+            long maxEndTime = monotonicStartTime;
+            List<PowerStatsSpan.Metadata> spans = mPowerStatsStore.getTableOfContents();
+            for (int i = spans.size() - 1; i >= 0; i--) {
+                PowerStatsSpan.Metadata metadata = spans.get(i);
+                if (!metadata.getSections().contains(AggregatedPowerStatsSection.TYPE)) {
+                    continue;
                 }
-                if (endMonotonicTime > spanMaxTime) {
-                    spanMaxTime = endMonotonicTime;
+
+                List<PowerStatsSpan.TimeFrame> timeFrames = metadata.getTimeFrames();
+                long spanMinTime = Long.MAX_VALUE;
+                long spanMaxTime = Long.MIN_VALUE;
+                for (int j = 0; j < timeFrames.size(); j++) {
+                    PowerStatsSpan.TimeFrame timeFrame = timeFrames.get(j);
+                    long startMonotonicTime = timeFrame.startMonotonicTime;
+                    long endMonotonicTime = startMonotonicTime + timeFrame.duration;
+                    if (startMonotonicTime < spanMinTime) {
+                        spanMinTime = startMonotonicTime;
+                    }
+                    if (endMonotonicTime > spanMaxTime) {
+                        spanMaxTime = endMonotonicTime;
+                    }
+                }
+
+                if (!(spanMinTime >= monotonicStartTime && spanMaxTime < monotonicEndTime)) {
+                    continue;
+                }
+
+                if (spanMaxTime > maxEndTime) {
+                    maxEndTime = spanMaxTime;
+                }
+
+                PowerStatsSpan span = mPowerStatsStore.loadPowerStatsSpan(metadata.getId(),
+                        AggregatedPowerStatsSection.TYPE);
+                if (span == null) {
+                    Slog.e(TAG, "Could not read PowerStatsStore section " + metadata);
+                    continue;
+                }
+                List<PowerStatsSpan.Section> sections = span.getSections();
+                for (int k = 0; k < sections.size(); k++) {
+                    hasStoredSpans = true;
+                    PowerStatsSpan.Section section = sections.get(k);
+                    populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder,
+                            ((AggregatedPowerStatsSection) section).getAggregatedPowerStats());
                 }
             }
 
-            if (!(spanMinTime >= monotonicStartTime && spanMaxTime < monotonicEndTime)) {
-                continue;
+            if (!hasStoredSpans
+                    || maxEndTime < monotonicEndTime - mBatterySessionTimeSpanSlackMillis) {
+                mPowerStatsAggregator.aggregatePowerStats(maxEndTime, monotonicEndTime,
+                        stats -> populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder, stats));
             }
-
-            if (spanMaxTime > maxEndTime) {
-                maxEndTime = spanMaxTime;
-            }
-
-            PowerStatsSpan span = mPowerStatsStore.loadPowerStatsSpan(metadata.getId(),
-                    AggregatedPowerStatsSection.TYPE);
-            if (span == null) {
-                Slog.e(TAG, "Could not read PowerStatsStore section " + metadata);
-                continue;
-            }
-            List<PowerStatsSpan.Section> sections = span.getSections();
-            for (int k = 0; k < sections.size(); k++) {
-                hasStoredSpans = true;
-                PowerStatsSpan.Section section = sections.get(k);
-                populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder,
-                        ((AggregatedPowerStatsSection) section).getAggregatedPowerStats());
-            }
+            mPowerStatsAggregator.reset();
         }
-
-        if (!hasStoredSpans || maxEndTime < monotonicEndTime - mBatterySessionTimeSpanSlackMillis) {
-            mPowerStatsAggregator.aggregatePowerStats(maxEndTime, monotonicEndTime,
-                    stats -> populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder, stats));
-        }
-        mPowerStatsAggregator.reset();
     }
 
     private void populateBatteryUsageStatsBuilder(
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java
index 7d7b3c2..c81c7ff 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java
@@ -257,8 +257,8 @@
             for (int i = deviceStateEstimations.size() - 1; i >= 0; i--) {
                 deviceStateEstimations.get(i).intermediates = null;
             }
-            for (int i = deviceStateEstimations.size() - 1; i >= 0; i--) {
-                deviceStateEstimations.get(i).intermediates = null;
+            for (int i = combinedDeviceStateEstimations.size() - 1; i >= 0; i--) {
+                combinedDeviceStateEstimations.get(i).intermediates = null;
             }
             for (int i = uidStateEstimates.size() - 1; i >= 0; i--) {
                 UidStateEstimate uidStateEstimate = uidStateEstimates.get(i);
diff --git a/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java
index e203e4a..908c751 100644
--- a/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/ScreenPowerStatsProcessor.java
@@ -119,6 +119,7 @@
         if (!uids.isEmpty()) {
             computeUidPowerEstimates(stats, uids);
         }
+        mPlan.resetIntermediates();
     }
 
     private void computeDevicePowerEstimates(PowerComponentAggregatedPowerStats stats) {
diff --git a/services/core/java/com/android/server/power/stats/SensorPowerStatsLayout.java b/services/core/java/com/android/server/power/stats/SensorPowerStatsLayout.java
new file mode 100644
index 0000000..e66cd39
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/SensorPowerStatsLayout.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.stats;
+
+import android.os.PersistableBundle;
+import android.util.Slog;
+import android.util.SparseIntArray;
+
+public class SensorPowerStatsLayout extends PowerStatsLayout {
+    private static final String TAG = "SensorPowerStatsLayout";
+    private static final String EXTRA_DEVICE_SENSOR_HANDLES = "dsh";
+    private static final String EXTRA_UID_SENSOR_POSITIONS = "usp";
+
+    private final SparseIntArray mSensorPositions = new SparseIntArray();
+
+    void addUidSensorSection(int handle, String label) {
+        mSensorPositions.put(handle, addUidSection(1, label, FLAG_OPTIONAL));
+    }
+
+    /**
+     * Returns the position in the uid stats array of the duration element corresponding
+     * to the specified sensor identified by its handle.
+     */
+    public int getUidSensorDurationPosition(int handle) {
+        return mSensorPositions.get(handle, UNSUPPORTED);
+    }
+
+    /**
+     * Adds the specified duration to the accumulated timer for the specified sensor.
+     */
+    public void addUidSensorDuration(long[] stats, int handle, long durationMs) {
+        int position = mSensorPositions.get(handle, UNSUPPORTED);
+        if (position == UNSUPPORTED) {
+            Slog.e(TAG, "Unknown sensor: " + handle);
+            return;
+        }
+        stats[position] += durationMs;
+    }
+
+    @Override
+    public void toExtras(PersistableBundle extras) {
+        super.toExtras(extras);
+
+        int[] handlers = new int[mSensorPositions.size()];
+        int[] uidDurationPositions = new int[mSensorPositions.size()];
+
+        for (int i = 0; i < mSensorPositions.size(); i++) {
+            handlers[i] = mSensorPositions.keyAt(i);
+            uidDurationPositions[i] = mSensorPositions.valueAt(i);
+        }
+
+        extras.putIntArray(EXTRA_DEVICE_SENSOR_HANDLES, handlers);
+        extras.putIntArray(EXTRA_UID_SENSOR_POSITIONS, uidDurationPositions);
+    }
+
+    @Override
+    public void fromExtras(PersistableBundle extras) {
+        super.fromExtras(extras);
+
+        int[] handlers = extras.getIntArray(EXTRA_DEVICE_SENSOR_HANDLES);
+        int[] uidDurationPositions = extras.getIntArray(EXTRA_UID_SENSOR_POSITIONS);
+
+        for (int i = 0; i < handlers.length; i++) {
+            mSensorPositions.put(handlers[i], uidDurationPositions[i]);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/power/stats/SensorPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/SensorPowerStatsProcessor.java
new file mode 100644
index 0000000..5bd3288
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/SensorPowerStatsProcessor.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.stats;
+
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.os.BatteryConsumer;
+import android.os.BatteryStats;
+import android.os.PersistableBundle;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.os.PowerStats;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Supplier;
+
+public class SensorPowerStatsProcessor extends PowerStatsProcessor {
+    private static final String TAG = "SensorPowerStatsProcessor";
+    private static final String ANDROID_SENSOR_TYPE_PREFIX = "android.sensor.";
+
+    private static final double MILLIS_IN_HOUR = 1000.0 * 60 * 60;
+    private static final String SENSOR_EVENT_TAG_PREFIX = "sensor:0x";
+    private final Supplier<SensorManager> mSensorManagerSupplier;
+
+    private static final long INITIAL_TIMESTAMP = -1;
+    private SensorManager mSensorManager;
+    private SensorPowerStatsLayout mStatsLayout;
+    private PowerStats mPowerStats;
+    private boolean mIsInitialized;
+    private PowerStats.Descriptor mDescriptor;
+    private long mLastUpdateTimestamp;
+    private PowerEstimationPlan mPlan;
+
+    private static class SensorState {
+        public int sensorHandle;
+        public boolean stateOn;
+        public int uid;
+        public long startTime = INITIAL_TIMESTAMP;
+    }
+
+    private static class Intermediates {
+        public double power;
+    }
+
+    private final SparseArray<SensorState> mSensorStates = new SparseArray<>();
+    private long[] mTmpDeviceStatsArray;
+    private long[] mTmpUidStatsArray;
+
+    public SensorPowerStatsProcessor(Supplier<SensorManager> sensorManagerSupplier) {
+        mSensorManagerSupplier = sensorManagerSupplier;
+    }
+
+    private boolean ensureInitialized() {
+        if (mIsInitialized) {
+            return true;
+        }
+
+        mSensorManager = mSensorManagerSupplier.get();
+        if (mSensorManager == null) {
+            return false;
+        }
+
+        mStatsLayout = new SensorPowerStatsLayout();
+        List<Sensor> sensorList = new ArrayList<>(mSensorManager.getSensorList(Sensor.TYPE_ALL));
+        sensorList.sort(Comparator.comparingInt(Sensor::getId));
+        for (int i = 0; i < sensorList.size(); i++) {
+            Sensor sensor = sensorList.get(i);
+            String label = makeLabel(sensor, sensorList);
+            mStatsLayout.addUidSensorSection(sensor.getHandle(), label);
+        }
+        mStatsLayout.addUidSectionPowerEstimate();
+        mStatsLayout.addDeviceSectionPowerEstimate();
+
+        PersistableBundle extras = new PersistableBundle();
+        mStatsLayout.toExtras(extras);
+        mDescriptor = new PowerStats.Descriptor(
+                BatteryConsumer.POWER_COMPONENT_SENSORS, mStatsLayout.getDeviceStatsArrayLength(),
+                null, 0, mStatsLayout.getUidStatsArrayLength(),
+                extras);
+
+        mPowerStats = new PowerStats(mDescriptor);
+        mTmpUidStatsArray = new long[mDescriptor.uidStatsArrayLength];
+        mTmpDeviceStatsArray = new long[mDescriptor.statsArrayLength];
+
+        mIsInitialized = true;
+        return true;
+    }
+
+    private String makeLabel(Sensor sensor, List<Sensor> sensorList) {
+        int type = sensor.getType();
+        String label = sensor.getStringType();
+
+        boolean isSingleton = true;
+        for (int i = sensorList.size() - 1; i >= 0; i--) {
+            Sensor s = sensorList.get(i);
+            if (s == sensor) {
+                continue;
+            }
+            if (s.getType() == type) {
+                isSingleton = false;
+                break;
+            }
+        }
+        if (!isSingleton) {
+            StringBuilder sb = new StringBuilder(label).append('.');
+            if (sensor.getId() > 0) { // 0 and -1 are reserved
+                sb.append(sensor.getId());
+            } else {
+                sb.append(sensor.getName());
+            }
+            label = sb.toString();
+        }
+        if (label.startsWith(ANDROID_SENSOR_TYPE_PREFIX)) {
+            label = label.substring(ANDROID_SENSOR_TYPE_PREFIX.length());
+        }
+        return label.replace(' ', '_');
+    }
+
+    @Override
+    void start(PowerComponentAggregatedPowerStats stats, long timestampMs) {
+        if (!ensureInitialized()) {
+            return;
+        }
+
+        // Establish a baseline at the beginning of an accumulation pass
+        mLastUpdateTimestamp = timestampMs;
+        flushPowerStats(stats, timestampMs);
+    }
+
+    @Override
+    void noteStateChange(PowerComponentAggregatedPowerStats stats, BatteryStats.HistoryItem item) {
+        if (!mIsInitialized) {
+            return;
+        }
+
+        if (item.eventTag == null || item.eventTag.string == null
+                || !item.eventTag.string.startsWith(SENSOR_EVENT_TAG_PREFIX)) {
+            return;
+        }
+
+        int sensorHandle;
+        try {
+            sensorHandle = Integer.parseInt(item.eventTag.string, SENSOR_EVENT_TAG_PREFIX.length(),
+                    item.eventTag.string.length(), 16);
+        } catch (NumberFormatException e) {
+            Slog.wtf(TAG, "Bad format of event tag: " + item.eventTag.string);
+            return;
+        }
+
+        SensorState sensor = mSensorStates.get(sensorHandle);
+        if (sensor == null) {
+            sensor = new SensorState();
+            sensor.sensorHandle = sensorHandle;
+            mSensorStates.put(sensorHandle, sensor);
+        }
+
+        int uid = item.eventTag.uid;
+        boolean sensorOn = (item.states & BatteryStats.HistoryItem.STATE_SENSOR_ON_FLAG) != 0;
+        if (sensorOn) {
+            if (!sensor.stateOn) {
+                sensor.stateOn = true;
+                sensor.uid = uid;
+                sensor.startTime = item.time;
+            } else if (sensor.uid != uid) {
+                recordUsageDuration(sensor, item.time);
+                sensor.uid = uid;
+            }
+        } else {
+            if (sensor.stateOn) {
+                recordUsageDuration(sensor, item.time);
+                sensor.stateOn = false;
+            }
+        }
+    }
+
+    @Override
+    void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) {
+        if (!mIsInitialized) {
+            return;
+        }
+
+        for (int i = mSensorStates.size() - 1; i >= 0; i--) {
+            SensorState sensor = mSensorStates.valueAt(i);
+            if (sensor.stateOn) {
+                recordUsageDuration(sensor, timestampMs);
+            }
+        }
+        flushPowerStats(stats, timestampMs);
+
+        if (mPlan == null) {
+            mPlan = new PowerEstimationPlan(stats.getConfig());
+        }
+
+        List<Integer> uids = new ArrayList<>();
+        stats.collectUids(uids);
+
+        computeUidPowerEstimates(stats, uids);
+        computeDevicePowerEstimates(stats);
+
+        mPlan.resetIntermediates();
+    }
+
+    protected void recordUsageDuration(SensorState sensorState, long time) {
+        long durationMs = Math.max(0, time - sensorState.startTime);
+        if (durationMs > 0) {
+            long[] uidStats = mPowerStats.uidStats.get(sensorState.uid);
+            if (uidStats == null) {
+                uidStats = new long[mDescriptor.uidStatsArrayLength];
+                mPowerStats.uidStats.put(sensorState.uid, uidStats);
+            }
+            mStatsLayout.addUidSensorDuration(uidStats, sensorState.sensorHandle, durationMs);
+        }
+        sensorState.startTime = time;
+    }
+
+    private void flushPowerStats(PowerComponentAggregatedPowerStats stats, long timestamp) {
+        mPowerStats.durationMs = timestamp - mLastUpdateTimestamp;
+        stats.addPowerStats(mPowerStats, timestamp);
+
+        Arrays.fill(mPowerStats.stats, 0);
+        mPowerStats.uidStats.clear();
+        mLastUpdateTimestamp = timestamp;
+    }
+
+    private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats,
+            List<Integer> uids) {
+        List<Sensor> sensorList = mSensorManager.getSensorList(Sensor.TYPE_ALL);
+        int[] uidSensorDurationPositions = new int[sensorList.size()];
+        double[] sensorPower = new double[sensorList.size()];
+        for (int i = sensorList.size() - 1; i >= 0; i--) {
+            Sensor sensor = sensorList.get(i);
+            uidSensorDurationPositions[i] =
+                    mStatsLayout.getUidSensorDurationPosition(sensor.getHandle());
+            sensorPower[i] = sensor.getPower() / MILLIS_IN_HOUR;
+        }
+
+        for (int i = mPlan.uidStateEstimates.size() - 1; i >= 0; i--) {
+            UidStateEstimate uidStateEstimate = mPlan.uidStateEstimates.get(i);
+            List<UidStateProportionalEstimate> proportionalEstimates =
+                    uidStateEstimate.proportionalEstimates;
+            for (int j = proportionalEstimates.size() - 1; j >= 0; j--) {
+                UidStateProportionalEstimate proportionalEstimate = proportionalEstimates.get(j);
+                for (int k = uids.size() - 1; k >= 0; k--) {
+                    int uid = uids.get(k);
+                    if (!stats.getUidStats(mTmpUidStatsArray, uid,
+                            proportionalEstimate.stateValues)) {
+                        continue;
+                    }
+                    double power = 0;
+                    for (int m = 0; m < uidSensorDurationPositions.length; m++) {
+                        int position = uidSensorDurationPositions[m];
+                        if (position == PowerStatsLayout.UNSUPPORTED
+                                || mTmpUidStatsArray[position] == 0) {
+                            continue;
+                        }
+                        power += sensorPower[m] * mTmpUidStatsArray[position];
+                    }
+                    if (power == 0) {
+                        continue;
+                    }
+
+                    mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power);
+                    stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray);
+
+                    Intermediates intermediates = (Intermediates) uidStateEstimate
+                            .combinedDeviceStateEstimate.intermediates;
+                    if (intermediates == null) {
+                        intermediates = new Intermediates();
+                        uidStateEstimate.combinedDeviceStateEstimate.intermediates = intermediates;
+                    }
+                    intermediates.power += power;
+                }
+            }
+        }
+    }
+
+    private void computeDevicePowerEstimates(PowerComponentAggregatedPowerStats stats) {
+        for (int i = mPlan.combinedDeviceStateEstimations.size() - 1; i >= 0; i--) {
+            CombinedDeviceStateEstimate estimation =
+                    mPlan.combinedDeviceStateEstimations.get(i);
+            if (estimation.intermediates == null) {
+                continue;
+            }
+
+            if (!stats.getDeviceStats(mTmpDeviceStatsArray, estimation.stateValues)) {
+                continue;
+            }
+
+            mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray,
+                    ((Intermediates) estimation.intermediates).power);
+            stats.setDeviceStats(estimation.stateValues, mTmpDeviceStatsArray);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 85c8900..e9423ce 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -1890,8 +1890,7 @@
         enforceStatusBarService();
         final long token = Binder.clearCallingIdentity();
         try {
-            // TODO(b/308479256): Check if hiding "all" IMEs is OK or not.
-            InputMethodManagerInternal.get().hideAllInputMethods(
+            InputMethodManagerInternal.get().hideInputMethod(
                     SoftInputShowHideReason.HIDE_BUBBLES, displayId);
         } finally {
             Binder.restoreCallingIdentity(token);
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index f9bad59..46bd7af 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -108,23 +108,6 @@
     }
 
     /**
-     * Resolves the default vibration amplitude of {@link #getEffectToPlay()} and each fallback.
-     *
-     * @param defaultAmplitude An integer in [1,255] representing the device default amplitude to
-     *                        replace the {@link VibrationEffect#DEFAULT_AMPLITUDE}.
-     */
-    public void resolveEffects(int defaultAmplitude) {
-        CombinedVibration newEffect =
-                mEffectToPlay.transform(VibrationEffect::resolve, defaultAmplitude);
-        if (!Objects.equals(mEffectToPlay, newEffect)) {
-            mEffectToPlay = newEffect;
-        }
-        for (int i = 0; i < mFallbacks.size(); i++) {
-            mFallbacks.setValueAt(i, mFallbacks.valueAt(i).resolve(defaultAmplitude));
-        }
-    }
-
-    /**
      * Scales the {@link #getEffectToPlay()} and each fallback effect based on the vibration usage.
      */
     public void scaleEffects(VibrationScaler scaler) {
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
index 98a2ba0d..3f9da82 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
@@ -46,6 +46,8 @@
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
     private static final VibrationAttributes COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST);
+    private static final VibrationAttributes IME_FEEDBACK_VIBRATION_ATTRIBUTES =
+            VibrationAttributes.createForUsage(VibrationAttributes.USAGE_IME_FEEDBACK);
 
     private final VibratorInfo mVibratorInfo;
     private final boolean mHapticTextHandleEnabled;
@@ -219,8 +221,6 @@
         }
 
         int vibFlags = 0;
-        boolean fromIme =
-                (privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) != 0;
         boolean bypassVibrationIntensitySetting =
                 (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0;
         if (bypassVibrationIntensitySetting) {
@@ -229,9 +229,6 @@
         if (shouldBypassInterruptionPolicy(effectId)) {
             vibFlags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY;
         }
-        if (shouldBypassIntensityScale(effectId, fromIme)) {
-            vibFlags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
-        }
 
         return vibFlags == 0 ? attrs : new VibrationAttributes.Builder(attrs)
                 .setFlags(vibFlags).build();
@@ -362,22 +359,6 @@
                 /* fallbackForPredefinedEffect= */ predefinedEffectFallback);
     }
 
-    private boolean shouldBypassIntensityScale(int effectId, boolean isIme) {
-        if (!Flags.keyboardCategoryEnabled() || mKeyboardVibrationFixedAmplitude < 0 || !isIme) {
-            // Shouldn't bypass if not support keyboard category, no fixed amplitude or not an IME.
-            return false;
-        }
-        switch (effectId) {
-            case HapticFeedbackConstants.KEYBOARD_TAP:
-                return mVibratorInfo.isPrimitiveSupported(
-                        VibrationEffect.Composition.PRIMITIVE_CLICK);
-            case HapticFeedbackConstants.KEYBOARD_RELEASE:
-                return mVibratorInfo.isPrimitiveSupported(
-                        VibrationEffect.Composition.PRIMITIVE_TICK);
-        }
-        return false;
-    }
-
     private VibrationAttributes createKeyboardVibrationAttributes(
             @HapticFeedbackConstants.PrivateFlags int privFlags) {
         // Use touch attribute when the keyboard category is disable.
@@ -388,7 +369,8 @@
         if ((privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) == 0) {
             return TOUCH_VIBRATION_ATTRIBUTES;
         }
-        return new VibrationAttributes.Builder(TOUCH_VIBRATION_ATTRIBUTES)
+        return new VibrationAttributes.Builder(IME_FEEDBACK_VIBRATION_ATTRIBUTES)
+                // TODO(b/332661766): Remove CATEGORY_KEYBOARD logic
                 .setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
                 .build();
     }
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 0206155..fb92d60 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -21,6 +21,7 @@
 import static android.os.VibrationAttributes.USAGE_ALARM;
 import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
 import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
 import static android.os.VibrationAttributes.USAGE_MEDIA;
 import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
 import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
@@ -560,6 +561,7 @@
             mKeyboardVibrationOn = loadSystemSetting(
                     Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0;
 
+            int keyboardIntensity = getDefaultIntensity(USAGE_IME_FEEDBACK);
             int alarmIntensity = toIntensity(
                     loadSystemSetting(Settings.System.ALARM_VIBRATION_INTENSITY, -1, userHandle),
                     getDefaultIntensity(USAGE_ALARM));
@@ -610,6 +612,12 @@
                 mCurrentVibrationIntensities.put(USAGE_TOUCH, hapticFeedbackIntensity);
             }
 
+            if (mVibrationConfig.isKeyboardVibrationSettingsSupported()) {
+                mCurrentVibrationIntensities.put(USAGE_IME_FEEDBACK, keyboardIntensity);
+            } else {
+                mCurrentVibrationIntensities.put(USAGE_IME_FEEDBACK, hapticFeedbackIntensity);
+            }
+
             // A11y is not disabled by any haptic feedback setting.
             mCurrentVibrationIntensities.put(USAGE_ACCESSIBILITY, positiveHapticFeedbackIntensity);
         }
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 8c9a92d..7152844 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -21,7 +21,6 @@
 import android.os.Build;
 import android.os.CombinedVibration;
 import android.os.IBinder;
-import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
@@ -177,16 +176,11 @@
             expectIsVibrationThread(true);
         }
 
-        if (!mVibration.callerInfo.attrs.isFlagSet(
-                VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
-            if (Flags.adaptiveHapticsEnabled()) {
-                waitForVibrationParamsIfRequired();
-            }
-            // Scale resolves the default amplitudes from the effect before scaling them.
-            mVibration.scaleEffects(mVibrationScaler);
-        } else {
-            mVibration.resolveEffects(mVibrationScaler.getDefaultVibrationAmplitude());
+        if (Flags.adaptiveHapticsEnabled()) {
+            waitForVibrationParamsIfRequired();
         }
+        // Scale resolves the default amplitudes from the effect before scaling them.
+        mVibration.scaleEffects(mVibrationScaler);
 
         mVibration.adaptToDevice(mDeviceAdapter);
         CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffectToPlay());
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 48c4a68..7610d7d 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -103,8 +103,7 @@
             new VibrationAttributes.Builder().build();
     private static final int ATTRIBUTES_ALL_BYPASS_FLAGS =
             VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY
-                    | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF
-                    | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+                    | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
 
     /** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */
     private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000;
@@ -925,8 +924,7 @@
     private VibrationStepConductor createVibrationStepConductor(HalVibration vib) {
         CompletableFuture<Void> requestVibrationParamsFuture = null;
 
-        if (Flags.adaptiveHapticsEnabled() && !vib.callerInfo.attrs.isFlagSet(
-                VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)
+        if (Flags.adaptiveHapticsEnabled()
                 && mVibratorControlService.shouldRequestVibrationParams(
                 vib.callerInfo.attrs.getUsage())) {
             requestVibrationParamsFuture =
@@ -940,13 +938,8 @@
     }
 
     private Vibration.EndInfo startVibrationOnInputDevicesLocked(HalVibration vib) {
-        if (!vib.callerInfo.attrs.isFlagSet(
-                VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
-            // Scale resolves the default amplitudes from the effect before scaling them.
-            vib.scaleEffects(mVibrationScaler);
-        } else {
-            vib.resolveEffects(mVibrationScaler.getDefaultVibrationAmplitude());
-        }
+        // Scale resolves the default amplitudes from the effect before scaling them.
+        vib.scaleEffects(mVibrationScaler);
         mInputDeviceDelegate.vibrateIfAvailable(vib.callerInfo, vib.getEffectToPlay());
 
         return new Vibration.EndInfo(Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index f70a3ba..d5bea4a 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -676,7 +676,12 @@
 
                     final Rect estimateCrop = new Rect(cropHint);
                     if (!multiCrop()) estimateCrop.scale(1f / options.inSampleSize);
-                    else estimateCrop.scale(1f / sampleSize);
+                    else {
+                        estimateCrop.left = (int) Math.floor(estimateCrop.left / sampleSize);
+                        estimateCrop.top = (int) Math.floor(estimateCrop.top / sampleSize);
+                        estimateCrop.right = (int) Math.ceil(estimateCrop.right / sampleSize);
+                        estimateCrop.bottom = (int) Math.ceil(estimateCrop.bottom / sampleSize);
+                    }
                     float hRatio = (float) wpData.mHeight / estimateCrop.height();
                     final int destHeight = (int) (estimateCrop.height() * hRatio);
                     final int destWidth = (int) (estimateCrop.width() * hRatio);
@@ -720,7 +725,10 @@
                         }
                         if (multiCrop()) {
                             Slog.v(TAG, "  cropHint=" + cropHint);
+                            Slog.v(TAG, "  estimateCrop=" + estimateCrop);
                             Slog.v(TAG, "  sampleSize=" + sampleSize);
+                            Slog.v(TAG, "  user defined crops: " + wallpaper.mCropHints);
+                            Slog.v(TAG, "  all crops: " + defaultCrops);
                         }
                         Slog.v(TAG, "  targetSize=" + safeWidth + "x" + safeHeight);
                         Slog.v(TAG, "  maxTextureSize=" + GLHelper.getMaxTextureSize());
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 023dd79..0c10551 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -68,7 +68,6 @@
 import android.app.ActivityTaskManager;
 import android.app.FullscreenRequestHandler;
 import android.app.IActivityClientController;
-import android.app.ICompatCameraControlCallback;
 import android.app.IRequestFinishCallback;
 import android.app.PictureInPictureParams;
 import android.app.PictureInPictureUiState;
@@ -1008,22 +1007,6 @@
         Binder.restoreCallingIdentity(origId);
     }
 
-    @Override
-    public void requestCompatCameraControl(IBinder token, boolean showControl,
-            boolean transformationApplied, ICompatCameraControlCallback callback) {
-        final long origId = Binder.clearCallingIdentity();
-        try {
-            synchronized (mGlobalLock) {
-                final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
-                if (r != null) {
-                    r.updateCameraCompatState(showControl, transformationApplied, callback);
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(origId);
-        }
-    }
-
     /**
      * Initialize the {@link #mSetPipAspectRatioQuotaTracker} if applicable, which should happen
      * out of {@link #mGlobalLock} to avoid deadlock (AM lock is used in QuotaTrack ctor).
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index b3208bf..4085ec9 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -4,10 +4,6 @@
 import static android.app.ActivityManager.START_SUCCESS;
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
 import static android.app.ActivityManager.processStateAmToProto;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
 import static android.app.WaitResult.INVALID_DELAY;
 import static android.app.WaitResult.LAUNCH_STATE_COLD;
 import static android.app.WaitResult.LAUNCH_STATE_HOT;
@@ -69,11 +65,6 @@
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
 import static com.android.internal.util.FrameworkStatsLog.APP_START_OCCURRED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL;
 import static com.android.internal.util.FrameworkStatsLog.APP_START_OCCURRED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED;
-import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__APPEARED_APPLY_TREATMENT;
-import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__APPEARED_REVERT_TREATMENT;
-import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__CLICKED_APPLY_TREATMENT;
-import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__CLICKED_DISMISS;
-import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__CLICKED_REVERT_TREATMENT;
 import static com.android.server.am.MemoryStatUtil.MemoryStat;
 import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem;
 import static com.android.server.am.ProcessList.INVALID_ADJ;
@@ -89,7 +80,6 @@
 import android.app.ActivityOptions;
 import android.app.ActivityOptions.SourceInfo;
 import android.app.ApplicationStartInfo;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.WaitResult;
 import android.app.WindowConfiguration.WindowingMode;
 import android.content.ComponentName;
@@ -1662,71 +1652,6 @@
         }
     }
 
-    /**
-     * Logs the Camera Compat Control appeared event that corresponds to the given {@code state}
-     * with the given {@code packageUid}.
-     */
-    void logCameraCompatControlAppearedEventReported(@CameraCompatControlState int state,
-            int packageUid) {
-        switch (state) {
-            case CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED:
-                logCameraCompatControlEventReported(
-                        CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__APPEARED_APPLY_TREATMENT,
-                        packageUid);
-                break;
-            case CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED:
-                logCameraCompatControlEventReported(
-                        CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__APPEARED_REVERT_TREATMENT,
-                        packageUid);
-                break;
-            case CAMERA_COMPAT_CONTROL_HIDDEN:
-                // Nothing to log.
-                break;
-            default:
-                Slog.w(TAG, "Unexpected state in logCameraCompatControlAppearedEventReported: "
-                        + state);
-                break;
-        }
-    }
-
-    /**
-     * Logs the Camera Compat Control clicked event that corresponds to the given {@code state}
-     * with the given {@code packageUid}.
-     */
-    void logCameraCompatControlClickedEventReported(@CameraCompatControlState int state,
-            int packageUid) {
-        switch (state) {
-            case CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED:
-                logCameraCompatControlEventReported(
-                        CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__CLICKED_APPLY_TREATMENT,
-                        packageUid);
-                break;
-            case CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED:
-                logCameraCompatControlEventReported(
-                        CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__CLICKED_REVERT_TREATMENT,
-                        packageUid);
-                break;
-            case CAMERA_COMPAT_CONTROL_DISMISSED:
-                logCameraCompatControlEventReported(
-                        CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__CLICKED_DISMISS,
-                        packageUid);
-                break;
-            default:
-                Slog.w(TAG, "Unexpected state in logCameraCompatControlAppearedEventReported: "
-                        + state);
-                break;
-        }
-    }
-
-    private void logCameraCompatControlEventReported(int event, int packageUid) {
-        FrameworkStatsLog.write(FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED, packageUid,
-                event);
-        if (DEBUG_METRICS) {
-            Slog.i(TAG, String.format("CAMERA_COMPAT_CONTROL_EVENT_REPORTED(%s, %s)", packageUid,
-                    event));
-        }
-    }
-
     private ArtManagerInternal getArtManagerInternal() {
         if (mArtManagerInternal == null) {
             // Note that this may be null.
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 7d70ea1..20ee525 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -33,12 +33,7 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
-import static android.app.CameraCompatTaskInfo.cameraCompatControlStateToString;
 import static android.app.WaitResult.INVALID_DELAY;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
@@ -271,8 +266,6 @@
 import android.app.Activity;
 import android.app.ActivityManager.TaskDescription;
 import android.app.ActivityOptions;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
-import android.app.ICompatCameraControlCallback;
 import android.app.IScreenCaptureObserver;
 import android.app.PendingIntent;
 import android.app.PictureInPictureParams;
@@ -824,19 +817,6 @@
     // and therefore #isLetterboxedForFixedOrientationAndAspectRatio returns false.
     private boolean mIsEligibleForFixedOrientationLetterbox;
 
-    // State of the Camera app compat control which is used to correct stretched viewfinder
-    // in apps that don't handle all possible configurations and changes between them correctly.
-    @CameraCompatControlState
-    private int mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN;
-
-    // The callback that allows to ask the calling View to apply the treatment for stretched
-    // issues affecting camera viewfinders when the user clicks on the camera compat control.
-    @Nullable
-    private ICompatCameraControlCallback mCompatCameraControlCallback;
-
-    private final boolean mCameraCompatControlEnabled;
-    private boolean mCameraCompatControlClickedByUser;
-
     // activity is not displayed?
     // TODO: rename to mNoDisplay
     @VisibleForTesting
@@ -1342,10 +1322,6 @@
         }
 
         mLetterboxUiController.dump(pw, prefix);
-
-        pw.println(prefix + "mCameraCompatControlState="
-                + cameraCompatControlStateToString(mCameraCompatControlState));
-        pw.println(prefix + "mCameraCompatControlEnabled=" + mCameraCompatControlEnabled);
     }
 
     static boolean dumpActivity(FileDescriptor fd, PrintWriter pw, int index, ActivityRecord r,
@@ -1884,100 +1860,6 @@
         mLetterboxUiController.getLetterboxInnerBounds(outBounds);
     }
 
-    void updateCameraCompatState(boolean showControl, boolean transformationApplied,
-            ICompatCameraControlCallback callback) {
-        if (!isCameraCompatControlEnabled()) {
-            // Feature is disabled by config_isCameraCompatControlForStretchedIssuesEnabled.
-            return;
-        }
-        if (mCameraCompatControlClickedByUser && (showControl
-                || mCameraCompatControlState == CAMERA_COMPAT_CONTROL_DISMISSED)) {
-            // The user already applied treatment on this activity or dismissed control.
-            // Respecting their choice.
-            return;
-        }
-        mCompatCameraControlCallback = callback;
-        int newCameraCompatControlState = !showControl
-                ? CAMERA_COMPAT_CONTROL_HIDDEN
-                : transformationApplied
-                        ? CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED
-                        : CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        boolean changed = setCameraCompatControlState(newCameraCompatControlState);
-        if (!changed) {
-            return;
-        }
-        mTaskSupervisor.getActivityMetricsLogger().logCameraCompatControlAppearedEventReported(
-                newCameraCompatControlState, info.applicationInfo.uid);
-        if (newCameraCompatControlState == CAMERA_COMPAT_CONTROL_HIDDEN) {
-            mCameraCompatControlClickedByUser = false;
-            mCompatCameraControlCallback = null;
-        }
-        // Trigger TaskInfoChanged to update the camera compat UI.
-        getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
-        // TaskOrganizerController#onTaskInfoChanged adds pending task events to the queue waiting
-        // for the surface placement to be ready. So need to trigger surface placement to dispatch
-        // events to avoid stale state for the camera compat control.
-        getDisplayContent().setLayoutNeeded();
-        mWmService.mWindowPlacerLocked.performSurfacePlacement();
-    }
-
-    void updateCameraCompatStateFromUser(@CameraCompatControlState int state) {
-        if (!isCameraCompatControlEnabled()) {
-            // Feature is disabled by config_isCameraCompatControlForStretchedIssuesEnabled.
-            return;
-        }
-        if (state == CAMERA_COMPAT_CONTROL_HIDDEN) {
-            Slog.w(TAG, "Unexpected hidden state in updateCameraCompatState");
-            return;
-        }
-        boolean changed = setCameraCompatControlState(state);
-        mCameraCompatControlClickedByUser = true;
-        if (!changed) {
-            return;
-        }
-        mTaskSupervisor.getActivityMetricsLogger().logCameraCompatControlClickedEventReported(
-                state, info.applicationInfo.uid);
-        if (state == CAMERA_COMPAT_CONTROL_DISMISSED) {
-            mCompatCameraControlCallback = null;
-            return;
-        }
-        if (mCompatCameraControlCallback == null) {
-            Slog.w(TAG, "Callback for a camera compat control is null");
-            return;
-        }
-        try {
-            if (state == CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED) {
-                mCompatCameraControlCallback.applyCameraCompatTreatment();
-            } else {
-                mCompatCameraControlCallback.revertCameraCompatTreatment();
-            }
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Unable to apply or revert camera compat treatment", e);
-        }
-    }
-
-    private boolean setCameraCompatControlState(@CameraCompatControlState int state) {
-        if (!isCameraCompatControlEnabled()) {
-            // Feature is disabled by config_isCameraCompatControlForStretchedIssuesEnabled.
-            return false;
-        }
-        if (mCameraCompatControlState != state) {
-            mCameraCompatControlState = state;
-            return true;
-        }
-        return false;
-    }
-
-    @CameraCompatControlState
-    int getCameraCompatControlState() {
-        return mCameraCompatControlState;
-    }
-
-    @VisibleForTesting
-    boolean isCameraCompatControlEnabled() {
-        return mCameraCompatControlEnabled;
-    }
-
     /**
      * @return {@code true} if bar shown within a given rectangle is allowed to be fully transparent
      *     when the current activity is displayed.
@@ -2105,8 +1987,6 @@
         // initialised.
         mLetterboxUiController = new LetterboxUiController(mWmService, this);
         mAppCompatController = new AppCompatController(mWmService, this);
-        mCameraCompatControlEnabled = mWmService.mContext.getResources()
-                .getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled);
         mResolveConfigHint = new TaskFragment.ConfigOverrideHint();
         if (mWmService.mFlags.mInsetsDecoupledConfiguration) {
             // When the stable configuration is the default behavior, override for the legacy apps
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index afdbc0a..509a060 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2901,6 +2901,8 @@
         ActivityRecord fillAndReturnTop(Task task, TaskInfo info) {
             info.numActivities = 0;
             info.baseActivity = null;
+            info.capturedLink = null;
+            info.capturedLinkTimestamp = 0;
             mInfo = info;
             task.forAllActivities(this);
             final ActivityRecord top = mTopRunning;
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 54024e9..02c8a49 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -20,9 +20,11 @@
 import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
 import static android.app.ActivityOptions.BackgroundActivityStartMode;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
@@ -226,6 +228,21 @@
         };
     }
 
+    static String balStartModeToString(@BackgroundActivityStartMode int startMode) {
+        return switch (startMode) {
+            case MODE_BACKGROUND_ACTIVITY_START_ALLOWED -> "MODE_BACKGROUND_ACTIVITY_START_ALLOWED";
+            case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED ->
+                    "MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED";
+            case MODE_BACKGROUND_ACTIVITY_START_COMPAT -> "MODE_BACKGROUND_ACTIVITY_START_COMPAT";
+            case MODE_BACKGROUND_ACTIVITY_START_DENIED -> "MODE_BACKGROUND_ACTIVITY_START_DENIED";
+            case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS ->
+                    "MODE_BACKGROUND_ACTIVITY_START_ALWAYS";
+            case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE ->
+                    "MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE";
+            default -> "MODE_BACKGROUND_ACTIVITY_START_ALLOWED(" + startMode + ")";
+        };
+    }
+
     @GuardedBy("mService.mGlobalLock")
     private final HashMap<Integer, FinishedActivityEntry> mTaskIdToFinishedActivity =
             new HashMap<>();
@@ -464,10 +481,6 @@
             this.mResultForRealCaller = resultForRealCaller;
         }
 
-        public boolean isPendingIntentBalAllowedByPermission() {
-            return PendingIntentRecord.isPendingIntentBalAllowedByPermission(mCheckedOptions);
-        }
-
         public boolean callerExplicitOptInOrAutoOptIn() {
             if (mAutoOptInCaller) {
                 return !callerExplicitOptOut();
@@ -528,6 +541,8 @@
             sb.append("; balAllowedByPiCreatorWithHardening: ")
                     .append(mBalAllowedByPiCreatorWithHardening);
             sb.append("; resultIfPiCreatorAllowsBal: ").append(mResultForCaller);
+            sb.append("; callerStartMode: ").append(balStartModeToString(
+                    mCheckedOptions.getPendingIntentBackgroundActivityStartMode()));
             sb.append("; hasRealCaller: ").append(hasRealCaller());
             sb.append("; isCallForResult: ").append(mIsCallForResult);
             sb.append("; isPendingIntent: ").append(isPendingIntent());
@@ -553,6 +568,8 @@
                 }
                 sb.append("; balAllowedByPiSender: ").append(mBalAllowedByPiSender);
                 sb.append("; resultIfPiSenderAllowsBal: ").append(mResultForRealCaller);
+                sb.append("; realCallerStartMode: ").append(balStartModeToString(
+                        mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()));
             }
             // features
             sb.append("; balImproveRealCallerVisibilityCheck: ")
@@ -949,7 +966,8 @@
             }
         }
 
-        if (state.isPendingIntentBalAllowedByPermission()
+        if (state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
+                == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
                 && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) {
             return new BalVerdict(BAL_ALLOW_PERMISSION,
                     /*background*/ false,
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 3b99954..1994174 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -52,7 +52,7 @@
  * display change transition. In this case, we will queue all display updates until the current
  * transition's collection finishes and then apply them afterwards.
  */
-public class DeferredDisplayUpdater implements DisplayUpdater {
+class DeferredDisplayUpdater {
 
     /**
      * List of fields that could be deferred before applying to DisplayContent.
@@ -110,7 +110,7 @@
         continueScreenUnblocking();
     };
 
-    public DeferredDisplayUpdater(@NonNull DisplayContent displayContent) {
+    DeferredDisplayUpdater(@NonNull DisplayContent displayContent) {
         mDisplayContent = displayContent;
         mNonOverrideDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
     }
@@ -122,8 +122,7 @@
      *
      * @param finishCallback is called when all pending display updates are finished
      */
-    @Override
-    public void updateDisplayInfo(@NonNull Runnable finishCallback) {
+    void updateDisplayInfo(@NonNull Runnable finishCallback) {
         // Get the latest display parameters from the DisplayManager
         final DisplayInfo displayInfo = getCurrentDisplayInfo();
 
@@ -310,9 +309,11 @@
         return !Objects.equals(first.uniqueId, second.uniqueId);
     }
 
-    @Override
-    public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation,
-            DisplayAreaInfo newDisplayAreaInfo) {
+    /**
+     * Called after physical display has changed and after DisplayContent applied new display
+     * properties.
+     */
+    void onDisplayContentDisplayPropertiesPostChanged() {
         // Unblock immediately in case there is no transition. This is unlikely to happen.
         if (mScreenUnblocker != null && !mDisplayContent.mTransitionController.inTransition()) {
             mScreenUnblocker.sendToTarget();
@@ -320,13 +321,16 @@
         }
     }
 
-    @Override
-    public void onDisplaySwitching(boolean switching) {
+    /**
+     * Called with {@code true} when physical display is going to switch. And {@code false} when
+     * the display is turned on or the device goes to sleep.
+     */
+    void onDisplaySwitching(boolean switching) {
         mShouldWaitForTransitionWhenScreenOn = switching;
     }
 
-    @Override
-    public boolean waitForTransition(@NonNull Message screenUnblocker) {
+    /** Returns {@code true} if the transition will control when to turn on the screen. */
+    boolean waitForTransition(@NonNull Message screenUnblocker) {
         if (!Flags.waitForTransitionOnDisplaySwitch()) return false;
         if (!mShouldWaitForTransitionWhenScreenOn) {
             return false;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 403c307..0d165f6 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -159,7 +159,6 @@
 import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
 import static com.android.server.wm.utils.RegionUtils.forEachRectReverse;
 import static com.android.server.wm.utils.RegionUtils.rectListToRegion;
-import static com.android.window.flags.Flags.deferDisplayUpdates;
 import static com.android.window.flags.Flags.explicitRefreshRateHints;
 
 import android.annotation.IntDef;
@@ -478,7 +477,7 @@
     AppCompatCameraPolicy mAppCompatCameraPolicy;
 
     DisplayFrames mDisplayFrames;
-    final DisplayUpdater mDisplayUpdater;
+    final DeferredDisplayUpdater mDisplayUpdater;
 
     private boolean mInTouchMode;
 
@@ -623,7 +622,6 @@
     @VisibleForTesting
     final DeviceStateController mDeviceStateController;
     final Consumer<DeviceStateController.DeviceState> mDeviceStateConsumer;
-    final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
     final RemoteDisplayChangeController mRemoteDisplayChangeController;
 
     /** Windows added since {@link #mCurrentFocus} was set to null. Used for ANR blaming. */
@@ -1140,11 +1138,7 @@
         mWallpaperController.resetLargestDisplay(display);
         display.getDisplayInfo(mDisplayInfo);
         display.getMetrics(mDisplayMetrics);
-        if (deferDisplayUpdates()) {
-            mDisplayUpdater = new DeferredDisplayUpdater(this);
-        } else {
-            mDisplayUpdater = new ImmediateDisplayUpdater(this);
-        }
+        mDisplayUpdater = new DeferredDisplayUpdater(this);
         mSystemGestureExclusionLimit = mWmService.mConstants.mSystemGestureExclusionLimitDp
                 * mDisplayMetrics.densityDpi / DENSITY_DEFAULT;
         isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
@@ -1168,8 +1162,6 @@
         mAppTransitionController = new AppTransitionController(mWmService, this);
         mTransitionController.registerLegacyListener(mFixedRotationTransitionListener);
         mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this);
-        mDisplaySwitchTransitionLauncher = new PhysicalDisplaySwitchTransitionLauncher(this,
-                mTransitionController);
         mRemoteDisplayChangeController = new RemoteDisplayChangeController(this);
 
         final InputChannel inputChannel = mWmService.mInputManager.monitorInput(
@@ -1190,7 +1182,6 @@
 
         mDeviceStateConsumer =
                 (@NonNull DeviceStateController.DeviceState newFoldState) -> {
-                    mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState);
                     mDisplayRotation.foldStateChanged(newFoldState);
                 };
         mDeviceStateController.registerDeviceStateCallback(mDeviceStateConsumer,
@@ -3094,8 +3085,6 @@
                 // metrics are updated as rotation settings might depend on them
                 mWmService.mDisplayWindowSettings.applySettingsToDisplayLocked(this,
                         /* includeRotationSettings */ false);
-                mDisplayUpdater.onDisplayContentDisplayPropertiesPreChanged(mDisplayId,
-                        mInitialDisplayWidth, mInitialDisplayHeight, newWidth, newHeight);
                 mDisplayRotation.physicalDisplayChanged();
                 mDisplayPolicy.physicalDisplayChanged();
             }
@@ -3130,8 +3119,7 @@
 
             if (physicalDisplayChanged) {
                 mDisplayPolicy.physicalDisplayUpdated();
-                mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged(currentRotation,
-                        getRotation(), getDisplayAreaInfo());
+                mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged();
             }
         }
     }
@@ -6278,7 +6266,7 @@
         // by looping the children, so that we don't miss any root tasks after the children size
         // changed or reordered.
         final ArrayList<Task> rootTasks = new ArrayList<>();
-        forAllRootTasks(rootTask -> {
+        getDefaultTaskDisplayArea().forAllRootTasks(rootTask -> {
             for (int activityType : activityTypes) {
                 // Collect the root tasks that are currently being organized.
                 if (rootTask.mCreatedByOrganizer) {
diff --git a/services/core/java/com/android/server/wm/DisplayUpdater.java b/services/core/java/com/android/server/wm/DisplayUpdater.java
deleted file mode 100644
index 918b180..0000000
--- a/services/core/java/com/android/server/wm/DisplayUpdater.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import android.annotation.NonNull;
-import android.os.Message;
-import android.view.Surface;
-import android.window.DisplayAreaInfo;
-
-/**
- * Interface for a helper class that manages updates of DisplayInfo coming from DisplayManager
- */
-interface DisplayUpdater {
-    /**
-     * Reads the latest display parameters from the display manager and returns them in a callback.
-     * If there are pending display updates, it will wait for them to finish first and only then it
-     * will call the callback with the latest display parameters.
-     *
-     * @param callback is called when all pending display updates are finished
-     */
-    void updateDisplayInfo(@NonNull Runnable callback);
-
-    /**
-     * Called when physical display has changed and before DisplayContent has applied new display
-     * properties
-     */
-    default void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
-            int initialDisplayHeight, int newWidth, int newHeight) {
-    }
-
-    /**
-     * Called after physical display has changed and after DisplayContent applied new display
-     * properties
-     */
-    default void onDisplayContentDisplayPropertiesPostChanged(
-            @Surface.Rotation int previousRotation, @Surface.Rotation int newRotation,
-            @NonNull DisplayAreaInfo newDisplayAreaInfo) {
-    }
-
-    /**
-     * Called with {@code true} when physical display is going to switch. And {@code false} when
-     * the display is turned on or the device goes to sleep.
-     */
-    default void onDisplaySwitching(boolean switching) {
-    }
-
-    /** Returns {@code true} if the transition will control when to turn on the screen. */
-    default boolean waitForTransition(@NonNull Message screenUnBlocker) {
-        return false;
-    }
-}
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index e0d69b0..4ec318b 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -16,9 +16,12 @@
 
 package com.android.server.wm;
 
+import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.WindowConfiguration;
+import android.companion.virtualdevice.flags.Flags;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -26,6 +29,7 @@
 import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.Slog;
+import android.view.Display;
 import android.window.DisplayWindowPolicyController;
 
 import java.io.PrintWriter;
@@ -80,6 +84,9 @@
                 if (hasDisplayCategory(activities.get(i))) {
                     return false;
                 }
+                if (!launchAllowedByDisplayPolicy(activities.get(i))) {
+                    return false;
+                }
             }
             return true;
         }
@@ -95,7 +102,13 @@
         if (mDisplayWindowPolicyController == null) {
             // Missing controller means that this display has no categories for activity launch
             // restriction.
-            return !hasDisplayCategory(activityInfo);
+            if (hasDisplayCategory(activityInfo)) {
+                return false;
+            }
+            if (!launchAllowedByDisplayPolicy(activityInfo)) {
+                return false;
+            }
+            return true;
         }
         return mDisplayWindowPolicyController.canActivityBeLaunched(activityInfo, intent,
             windowingMode, launchingFromDisplayId, isNewTask);
@@ -112,6 +125,24 @@
         return false;
     }
 
+    private boolean launchAllowedByDisplayPolicy(ActivityInfo aInfo) {
+        if (!Flags.enforceRemoteDeviceOptOutOnAllVirtualDisplays()) {
+            return true;
+        }
+        int displayType = mDisplayContent.getDisplay().getType();
+        if (displayType != Display.TYPE_VIRTUAL && displayType != Display.TYPE_WIFI) {
+            return true;
+        }
+        if ((aInfo.flags & FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES) == 0) {
+            Slog.d(TAG,
+                    String.format("Checking activity launch on display %d, activity requires"
+                                    + " android:canDisplayOnRemoteDevices=true",
+                            mDisplayContent.mDisplayId));
+            return false;
+        }
+        return true;
+    }
+
     /**
      * @see DisplayWindowPolicyController#keepActivityOnWindowFlagsChanged(ActivityInfo, int, int)
      */
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
index 8bd8098..27e6e09 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
@@ -55,8 +55,10 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * Implementation of {@link SettingsProvider} that reads the base settings provided in a display
@@ -152,19 +154,35 @@
 
     /**
      * Removes display override settings that are no longer associated with active displays.
-     * This is necessary because displays can be dynamically added or removed during
-     * the system's lifecycle (e.g., user switch, system server restart).
+     * <p>
+     * This cleanup process is essential due to the dynamic nature of displays, which can
+     * be added or removed during various system events such as user switching or
+     * system server restarts.
      *
-     * @param root The root window container used to obtain the currently active displays.
+     * @param wms  the WindowManagerService instance for retrieving all possible {@link DisplayInfo}
+     *             for the given logical display.
+     * @param root the root window container used to obtain the currently active displays.
      */
-    void removeStaleDisplaySettings(@NonNull RootWindowContainer root) {
+    void removeStaleDisplaySettingsLocked(@NonNull WindowManagerService wms,
+            @NonNull RootWindowContainer root) {
         if (!Flags.perUserDisplayWindowSettings()) {
             return;
         }
         final Set<String> displayIdentifiers = new ArraySet<>();
+        final Consumer<DisplayInfo> addDisplayIdentifier =
+                displayInfo -> displayIdentifiers.add(mOverrideSettings.getIdentifier(displayInfo));
         root.forAllDisplays(dc -> {
-            final String identifier = mOverrideSettings.getIdentifier(dc.getDisplayInfo());
-            displayIdentifiers.add(identifier);
+            // Begin with the current display's information. Note that the display layout of the
+            // current device state might not include this display (e.g., external or virtual
+            // displays), resulting in empty possible display info.
+            addDisplayIdentifier.accept(dc.getDisplayInfo());
+
+            // Then, add all possible display information for this display if available.
+            final List<DisplayInfo> displayInfos = wms.getPossibleDisplayInfoLocked(dc.mDisplayId);
+            final int size = displayInfos.size();
+            for (int i = 0; i < size; i++) {
+                addDisplayIdentifier.accept(displayInfos.get(i));
+            }
         });
         mOverrideSettings.removeStaleDisplaySettings(displayIdentifiers);
     }
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 6abef8b..c849a37 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -16,7 +16,6 @@
 
 package com.android.server.wm;
 
-import static android.content.ClipDescription.EXTRA_HIDE_DRAG_SOURCE_TASK_ID;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.View.DRAG_FLAG_GLOBAL;
 import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION;
@@ -228,7 +227,7 @@
                         final Display display = displayContent.getDisplay();
                         touchFocusTransferredFuture = mCallback.get().registerInputChannel(
                                 mDragState, display, mService.mInputManager,
-                                callingWin.mInputChannel);
+                                callingWin.mInputChannelToken);
                     } else {
                         // Skip surface logic for a drag triggered by an AccessibilityAction
                         mDragState.broadcastDragStartedLocked(touchX, touchY);
diff --git a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java b/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
deleted file mode 100644
index 4af9013..0000000
--- a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import android.annotation.NonNull;
-import android.view.DisplayInfo;
-import android.window.DisplayAreaInfo;
-
-/**
- * DisplayUpdater that immediately applies new DisplayInfo properties
- */
-public class ImmediateDisplayUpdater implements DisplayUpdater {
-
-    private final DisplayContent mDisplayContent;
-    private final DisplayInfo mDisplayInfo = new DisplayInfo();
-
-    public ImmediateDisplayUpdater(@NonNull DisplayContent displayContent) {
-        mDisplayContent = displayContent;
-        mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
-    }
-
-    @Override
-    public void updateDisplayInfo(Runnable callback) {
-        mDisplayContent.mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo(
-                mDisplayContent.mDisplayId, mDisplayInfo);
-        mDisplayContent.onDisplayInfoUpdated(mDisplayInfo);
-        callback.run();
-    }
-
-    @Override
-    public void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
-            int initialDisplayHeight, int newWidth, int newHeight) {
-        mDisplayContent.mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded(
-                displayId, initialDisplayWidth, initialDisplayHeight, newWidth, newHeight);
-    }
-
-    @Override
-    public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation,
-            DisplayAreaInfo newDisplayAreaInfo) {
-        mDisplayContent.mDisplaySwitchTransitionLauncher.onDisplayUpdated(previousRotation,
-                newRotation,
-                newDisplayAreaInfo);
-    }
-}
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index b496a65..b8869f1 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -439,8 +439,7 @@
                         final InputMethodManagerInternal inputMethodManagerInternal =
                                 LocalServices.getService(InputMethodManagerInternal.class);
                         if (inputMethodManagerInternal != null) {
-                            // TODO(b/308479256): Check if hiding "all" IMEs is OK or not.
-                            inputMethodManagerInternal.hideAllInputMethods(
+                            inputMethodManagerInternal.hideInputMethod(
                                     SoftInputShowHideReason.HIDE_RECENTS_ANIMATION,
                                     mDisplayContent.getDisplayId());
                         }
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 3d07744..7a0fd3e 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -416,8 +416,9 @@
         }
 
         final TransitionController tc = mRootWindowContainer.mTransitionController;
+        final KeyguardDisplayState state = getDisplayState(displayId);
 
-        final boolean occluded = getDisplayState(displayId).mOccluded;
+        final boolean occluded = state.mOccluded;
         final boolean performTransition = isKeyguardLocked(displayId);
         final boolean executeTransition = performTransition && !tc.isCollecting();
 
@@ -461,7 +462,7 @@
     /**
      * Called when keyguard going away state changed.
      */
-    private void handleKeyguardGoingAwayChanged(DisplayContent dc) {
+    private void handleDismissInsecureKeyguard(DisplayContent dc) {
         mService.deferWindowLayout();
         try {
             dc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY, 0 /* transitFlags */);
@@ -705,16 +706,16 @@
             }
 
             boolean hasChange = false;
-            if (lastOccluded != mOccluded) {
-                writeEventLog("updateVisibility");
+            if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
+                writeEventLog("dismissIfInsecure");
+                controller.handleDismissInsecureKeyguard(display);
+                hasChange = true;
+            } else if (lastOccluded != mOccluded) {
                 controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
                 hasChange = true;
-            } else if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
-                writeEventLog("dismissIfInsecure");
-                controller.handleKeyguardGoingAwayChanged(display);
-                hasChange = true;
             }
-            // Collect the participates for shell transition, so that transition won't happen too
+
+            // Collect the participants for shell transition, so that transition won't happen too
             // early since the transition was set ready.
             if (hasChange && top != null && (mOccluded || mKeyguardGoingAway)) {
                 display.mTransitionController.collect(top);
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 444097a..291eab1 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -325,11 +325,6 @@
                 : mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
     }
 
-    float getFixedOrientationLetterboxAspectRatio(@NonNull Configuration parentConfiguration) {
-        return mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .getFixedOrientationLetterboxAspectRatio(parentConfiguration);
-    }
-
     boolean isLetterboxEducationEnabled() {
         return mAppCompatConfiguration.getIsEducationEnabled();
     }
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
deleted file mode 100644
index 3cf301c..0000000
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH;
-
-import static com.android.internal.R.bool.config_unfoldTransitionEnabled;
-import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
-import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.OPEN;
-
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.Rect;
-import android.window.DisplayAreaInfo;
-import android.window.TransitionRequestInfo;
-import android.window.WindowContainerTransaction;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogGroup;
-import com.android.internal.protolog.ProtoLog;
-import com.android.server.wm.DeviceStateController.DeviceState;
-
-public class PhysicalDisplaySwitchTransitionLauncher {
-
-    private final DisplayContent mDisplayContent;
-    private final ActivityTaskManagerService mAtmService;
-    private final Context mContext;
-    private final TransitionController mTransitionController;
-
-    /**
-     * If on a foldable device represents whether we need to show unfold animation when receiving
-     * a physical display switch event
-     */
-    private boolean mShouldRequestTransitionOnDisplaySwitch = false;
-    /**
-     * Current device state from {@link android.hardware.devicestate.DeviceStateManager}
-     */
-    private DeviceState mDeviceState = DeviceState.UNKNOWN;
-    private Transition mTransition;
-
-    public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent,
-            TransitionController transitionController) {
-        this(displayContent, displayContent.mWmService.mAtmService,
-                displayContent.mWmService.mContext, transitionController);
-    }
-
-    @VisibleForTesting
-    public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent,
-            ActivityTaskManagerService service, Context context,
-            TransitionController transitionController) {
-        mDisplayContent = displayContent;
-        mAtmService = service;
-        mContext = context;
-        mTransitionController = transitionController;
-    }
-
-    /**
-     * Called by the display manager just before it applied the device state, it is guaranteed
-     * that in case of physical display change the
-     * {@link PhysicalDisplaySwitchTransitionLauncher#requestDisplaySwitchTransitionIfNeeded}
-     * method will be invoked *after* this one.
-     */
-    void foldStateChanged(DeviceState newDeviceState) {
-        boolean isUnfolding = mDeviceState == FOLDED
-                && (newDeviceState == HALF_FOLDED || newDeviceState == OPEN);
-
-        if (isUnfolding) {
-            // Request transition only if we are unfolding the device
-            mShouldRequestTransitionOnDisplaySwitch = true;
-        } else if (newDeviceState != HALF_FOLDED && newDeviceState != OPEN) {
-            // Cancel the transition request in case if we are folding or switching to back
-            // to the rear display before the displays got switched
-            mShouldRequestTransitionOnDisplaySwitch = false;
-        }
-
-        mDeviceState = newDeviceState;
-    }
-
-    /**
-     * Requests to start a transition for the physical display switch
-     */
-    public void requestDisplaySwitchTransitionIfNeeded(int displayId, int oldDisplayWidth,
-            int oldDisplayHeight, int newDisplayWidth, int newDisplayHeight) {
-        if (!mShouldRequestTransitionOnDisplaySwitch) return;
-        if (!mTransitionController.isShellTransitionsEnabled()) return;
-        if (!mDisplayContent.getLastHasContent()) return;
-
-        boolean shouldRequestUnfoldTransition = mContext.getResources()
-                .getBoolean(config_unfoldTransitionEnabled) && ValueAnimator.areAnimatorsEnabled();
-
-        if (!shouldRequestUnfoldTransition) {
-            return;
-        }
-
-        mTransition = null;
-
-        if (mTransitionController.isCollecting()) {
-            // Add display container to the currently collecting transition
-            mTransition = mTransitionController.getCollectingTransition();
-            mTransition.collect(mDisplayContent);
-
-            // Make sure that transition is not ready until we finish the remote display change
-            mTransition.setReady(mDisplayContent, false);
-            mTransition.addFlag(TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH);
-
-            ProtoLog.d(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
-                    "Adding display switch to existing collecting transition");
-        } else {
-            final TransitionRequestInfo.DisplayChange displayChange =
-                    new TransitionRequestInfo.DisplayChange(displayId);
-
-            final Rect startAbsBounds = new Rect(0, 0, oldDisplayWidth, oldDisplayHeight);
-            displayChange.setStartAbsBounds(startAbsBounds);
-            final Rect endAbsBounds = new Rect(0, 0, newDisplayWidth, newDisplayHeight);
-            displayChange.setEndAbsBounds(endAbsBounds);
-            displayChange.setPhysicalDisplayChanged(true);
-
-            mTransition = mTransitionController.requestStartDisplayTransition(TRANSIT_CHANGE,
-                    0 /* flags */, mDisplayContent, null /* remoteTransition */, displayChange);
-            mTransition.collect(mDisplayContent);
-        }
-
-        if (mTransition != null) {
-            mAtmService.startPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY);
-        }
-
-        mShouldRequestTransitionOnDisplaySwitch = false;
-    }
-
-    /**
-     * Called when physical display is getting updated, this could happen e.g. on foldable
-     * devices when the physical underlying display is replaced.
-     *
-     * @param fromRotation rotation before the display change
-     * @param toRotation rotation after the display change
-     * @param newDisplayAreaInfo display area info after the display change
-     */
-    public void onDisplayUpdated(int fromRotation, int toRotation,
-            @NonNull DisplayAreaInfo newDisplayAreaInfo) {
-        if (mTransition == null) return;
-
-        final boolean started = mDisplayContent.mRemoteDisplayChangeController
-                .performRemoteDisplayChange(fromRotation, toRotation, newDisplayAreaInfo,
-                        this::continueDisplayUpdate);
-
-        if (!started) {
-            markTransitionAsReady();
-        }
-    }
-
-    private void continueDisplayUpdate(@Nullable WindowContainerTransaction transaction) {
-        if (mTransition == null) return;
-
-        if (transaction != null) {
-            mAtmService.mWindowOrganizerController.applyTransaction(transaction);
-        }
-
-        markTransitionAsReady();
-    }
-
-    private void markTransitionAsReady() {
-        if (mTransition == null) return;
-
-        mTransition.setAllReady();
-        mTransition = null;
-    }
-
-}
diff --git a/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java b/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java
index e3a2065..6e87977 100644
--- a/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java
+++ b/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java
@@ -57,7 +57,7 @@
      * Returns, for the given displayId, a list of unique display infos. List contains each
      * supported device state.
      * <p>List contents are guaranteed to be unique, but returned as a list rather than a set to
-     * minimize copies needed to make an iteraable data structure.
+     * minimize copies needed to make an iterable data structure.
      */
     public List<DisplayInfo> getPossibleDisplayInfos(int displayId) {
         // Update display infos before returning, since any cached values would have been removed
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 1c9aaf9..5cef3a1 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3398,10 +3398,6 @@
                 && 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()
-                : CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
 
         final Task parentTask = getParent() != null ? getParent().asTask() : null;
         info.parentTaskId = parentTask != null && parentTask.mCreatedByOrganizer
@@ -3513,6 +3509,9 @@
         if (task.effectiveUid != baseActivityUid) {
             info.baseActivity = new ComponentName("", "");
         }
+
+        info.capturedLink = null;
+        info.capturedLinkTimestamp = 0;
     }
 
     @Nullable PictureInPictureParams getPictureInPictureParams() {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index c83b280..ed0dc3b 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2819,7 +2819,21 @@
                 mClearedTaskForReuse,
                 mClearedTaskFragmentForPip,
                 mClearedForReorderActivityToFront,
-                calculateMinDimension());
+                calculateMinDimension(),
+                isTopNonFinishingChild());
+    }
+
+    private boolean isTopNonFinishingChild() {
+        final WindowContainer<?> parent = getParent();
+        if (parent == null) {
+            // Either the TaskFragment is not attached or is going to destroy. Return false.
+            return false;
+        }
+        final ActivityRecord topNonFishingActivity = parent.getActivity(ar -> !ar.finishing);
+        // If the parent's top non-finishing activity is this TaskFragment's, it means
+        // this TaskFragment is the top non-finishing container of its parent.
+        return topNonFishingActivity != null && topNonFishingActivity
+                .equals(getTopNonFinishingActivity());
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index 6e36d427..2c5beda 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -17,7 +17,6 @@
 package com.android.server.wm;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.CameraCompatTaskInfo.cameraCompatControlStateToString;
 import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NONE;
 import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NORMAL;
 import static android.window.StartingWindowRemovalInfo.DEFER_MODE_ROTATION;
@@ -1128,35 +1127,6 @@
         }
     }
 
-    @Override
-    public void updateCameraCompatControlState(WindowContainerToken token, int state) {
-        enforceTaskPermission("updateCameraCompatControlState()");
-        final long origId = Binder.clearCallingIdentity();
-        try {
-            synchronized (mGlobalLock) {
-                final WindowContainer wc = WindowContainer.fromBinder(token.asBinder());
-                if (wc == null) {
-                    Slog.w(TAG, "Could not resolve window from token");
-                    return;
-                }
-                final Task task = wc.asTask();
-                if (task == null) {
-                    Slog.w(TAG, "Could not resolve task from token");
-                    return;
-                }
-                ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
-                        "Update camera compat control state to %s for taskId=%d",
-                        cameraCompatControlStateToString(state), task.mTaskId);
-                final ActivityRecord activity = task.getTopNonFinishingActivity();
-                if (activity != null) {
-                    activity.updateCameraCompatStateFromUser(state);
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(origId);
-        }
-    }
-
     public boolean handleInterceptBackPressedOnTaskRoot(Task task) {
         if (!shouldInterceptBackPressedOnRootTask(task)) {
             return false;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 358adc3..af3ed28 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2185,8 +2185,7 @@
         for (int i = mParticipants.size() - 1; i >= 0; --i) {
             final WallpaperWindowToken wallpaper = mParticipants.valueAt(i).asWallpaperToken();
             if (wallpaper != null) {
-                if (!wallpaper.isVisible() && (wallpaper.isVisibleRequested()
-                        || (Flags.ensureWallpaperInTransitions() && showWallpaper))) {
+                if (!wallpaper.isVisible() && wallpaper.isVisibleRequested()) {
                     wallpaper.commitVisibility(showWallpaper);
                 } else if (Flags.ensureWallpaperInTransitions() && wallpaper.isVisible()
                         && !showWallpaper && !wallpaper.getDisplayContent().isKeyguardLocked()
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index c6aaf4e..48a5050 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -41,7 +41,6 @@
 import android.view.IInputFilter;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IWindow;
-import android.view.InputChannel;
 import android.view.MagnificationSpec;
 import android.view.RemoteAnimationTarget;
 import android.view.Surface;
@@ -377,10 +376,10 @@
     public interface IDragDropCallback {
         default CompletableFuture<Boolean> registerInputChannel(
                 DragState state, Display display, InputManagerService service,
-                InputChannel source) {
+                IBinder sourceInputChannelToken) {
             return state.register(display)
                 .thenApply(unused ->
-                    service.startDragAndDrop(source.getToken(), state.getInputToken()));
+                    service.startDragAndDrop(sourceInputChannelToken, state.getInputToken()));
         }
 
         /**
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index a218068..13453a6 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -118,12 +118,12 @@
 import static com.android.server.policy.PhoneWindowManager.TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
-import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL;
-import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL;
+import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
 import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS;
 import static com.android.server.wm.SensitiveContentPackages.PackageInfo;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
@@ -357,7 +357,6 @@
 import com.android.server.power.ShutdownThread;
 import com.android.server.utils.PriorityDump;
 import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
-import com.android.window.flags.Flags;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -3750,7 +3749,7 @@
             }
             mCurrentUserId = newUserId;
             mDisplayWindowSettingsProvider.setOverrideSettingsForUser(newUserId);
-            mDisplayWindowSettingsProvider.removeStaleDisplaySettings(mRoot);
+            mDisplayWindowSettingsProvider.removeStaleDisplaySettingsLocked(this, mRoot);
             mPolicy.setCurrentUserLw(newUserId);
             mKeyguardDisableHandler.setCurrentUser(newUserId);
 
@@ -5491,7 +5490,7 @@
             mRoot.forAllDisplays(DisplayContent::reconfigureDisplayLocked);
             // Per-user display settings may leave outdated settings after user switches, especially
             // during reboots starting with the default user without setCurrentUser called.
-            mDisplayWindowSettingsProvider.removeStaleDisplaySettings(mRoot);
+            mDisplayWindowSettingsProvider.removeStaleDisplaySettingsLocked(this, mRoot);
         }
     }
 
@@ -9390,11 +9389,6 @@
             return focusedActivity;
         }
 
-        if (!Flags.embeddedActivityBackNavFlag()) {
-            // Return if flag is not enabled.
-            return focusedActivity;
-        }
-
         if (!focusedActivity.isEmbedded()) {
             // Return if the focused activity is not embedded.
             return focusedActivity;
@@ -9778,7 +9772,7 @@
                 Slog.e(TAG, "Host window not found");
                 return;
             }
-            if (hostWindow.mInputChannel == null) {
+            if (hostWindow.mInputChannelToken == null) {
                 Slog.e(TAG, "Host window does not have an input channel");
                 return;
             }
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index bc32b91..0093e9d 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -390,7 +390,10 @@
 
     private static boolean hasActivityLaunch(@NonNull WindowContainerTransaction wct) {
         for (int i = 0; i < wct.getHierarchyOps().size(); ++i) {
-            if (wct.getHierarchyOps().get(i).getType() == HIERARCHY_OP_TYPE_LAUNCH_TASK) {
+            final WindowContainerTransaction.HierarchyOp op = wct.getHierarchyOps().get(i);
+            if (op.getType() == HIERARCHY_OP_TYPE_LAUNCH_TASK
+                    || (op.getType() == HIERARCHY_OP_TYPE_PENDING_INTENT
+                            && op.getPendingIntent().isActivity())) {
                 return true;
             }
         }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 1cc5a8b..153d41b 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -638,7 +638,7 @@
     /**
      * Only populated if flag REMOVE_INPUT_CHANNEL_FROM_WINDOWSTATE is disabled.
      */
-    InputChannel mInputChannel;
+    private InputChannel mInputChannel;
 
     /**
      * The token will be assigned to {@link InputWindowHandle#token} if this window can receive
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 9e46f2f..8ae4f9a 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -41,6 +41,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.Nullable;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -50,6 +51,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.inputmethod.InputBindResult;
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 import com.android.internal.inputmethod.StartInputFlags;
@@ -70,15 +72,12 @@
 public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTestBase {
     private DefaultImeVisibilityApplier mVisibilityApplier;
 
-    private int mUserId = 0;
-
     @Before
     public void setUp() throws RemoteException {
         super.setUp();
         synchronized (ImfLock.class) {
             mVisibilityApplier = mInputMethodManagerService.getVisibilityApplierLocked();
-            mUserId = mInputMethodManagerService.getCurrentImeUserIdLocked();
-            mInputMethodManagerService.setAttachedClientForTesting(requireNonNull(
+            setAttachedClientLocked(requireNonNull(
                     mInputMethodManagerService.getClientStateLocked(mMockInputMethodClient)));
         }
     }
@@ -171,7 +170,9 @@
         // Init a IME target client on the secondary display to show IME.
         mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection,
                 10 /* selfReportedDisplayId */);
-        mInputMethodManagerService.setAttachedClientForTesting(null);
+        synchronized (ImfLock.class) {
+            setAttachedClientLocked(null);
+        }
         startInputOrWindowGainedFocus(mWindowToken, SOFT_INPUT_STATE_ALWAYS_VISIBLE);
 
         final var statsToken = ImeTracker.Token.empty();
@@ -209,7 +210,9 @@
 
     @Test
     public void testApplyImeVisibility_hideImeWhenUnbinding() {
-        mInputMethodManagerService.setAttachedClientForTesting(null);
+        synchronized (ImfLock.class) {
+            setAttachedClientLocked(null);
+        }
         startInputOrWindowGainedFocus(mWindowToken, SOFT_INPUT_STATE_ALWAYS_VISIBLE);
         ExtendedMockito.spyOn(mVisibilityApplier);
 
@@ -236,6 +239,11 @@
         }
     }
 
+    @GuardedBy("ImfLock.class")
+    private void setAttachedClientLocked(@Nullable ClientState cs) {
+        mInputMethodManagerService.getUserData(mUserId).mCurClient = cs;
+    }
+
     private InputBindResult startInputOrWindowGainedFocus(IBinder windowToken, int softInputMode) {
         return mInputMethodManagerService.startInputOrWindowGainedFocus(
                 StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */,
@@ -248,7 +256,7 @@
                 mMockRemoteInputConnection /* inputConnection */,
                 mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
                 mTargetSdkVersion /* unverifiedTargetSdkVersion */,
-                mCallingUserId /* userId */,
+                mUserId /* userId */,
                 mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
     }
 }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
index 8981b7a..dd3b33e 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -311,10 +311,8 @@
             final ArgumentCaptor<IBinder> targetCaptor = ArgumentCaptor.forClass(IBinder.class);
             final ArgumentCaptor<ImeVisibilityResult> resultCaptor = ArgumentCaptor.forClass(
                     ImeVisibilityResult.class);
-            synchronized (ImfLock.class) {
-                verify(mInputMethodManagerService).onApplyImeVisibilityFromComputerLocked(
-                        targetCaptor.capture(), notNull() /* statsToken */, resultCaptor.capture());
-            }
+            verify(mInputMethodManagerService).onApplyImeVisibilityFromComputerLocked(
+                    targetCaptor.capture(), notNull() /* statsToken */, resultCaptor.capture());
             final IBinder imeInputTarget = targetCaptor.getValue();
             final ImeVisibilityResult result = resultCaptor.getValue();
 
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
index 4d28b3c..1e3b7e9 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
@@ -140,8 +140,7 @@
         final InputMethodInfo info;
         synchronized (ImfLock.class) {
             mBindingController.setSelectedMethodId(TEST_IME_ID);
-            info = InputMethodSettingsRepository.get(mCallingUserId).getMethodMap()
-                    .get(TEST_IME_ID);
+            info = InputMethodSettingsRepository.get(mUserId).getMethodMap().get(TEST_IME_ID);
         }
         assertThat(info).isNotNull();
         assertThat(info.getId()).isEqualTo(TEST_IME_ID);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index d427a6d..461697c 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -50,7 +50,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.UserHandle;
 import android.util.ArraySet;
 import android.view.InputChannel;
 import android.view.inputmethod.EditorInfo;
@@ -128,7 +127,7 @@
     protected Context mContext;
     protected MockitoSession mMockingSession;
     protected int mTargetSdkVersion;
-    protected int mCallingUserId;
+    protected int mUserId;
     protected EditorInfo mEditorInfo;
     protected IInputMethodInvoker mMockInputMethodInvoker;
     protected InputMethodManagerService mInputMethodManagerService;
@@ -165,12 +164,12 @@
                         .spyStatic(AdditionalSubtypeUtils.class)
                         .startMocking();
 
-        mContext = spy(InstrumentationRegistry.getInstrumentation().getContext());
+        mContext = spy(InstrumentationRegistry.getInstrumentation().getTargetContext());
 
         mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
         mIsLargeScreen = mContext.getResources().getConfiguration()
                 .isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
-        mCallingUserId = UserHandle.getCallingUserId();
+        mUserId = mContext.getUserId();
         mEditorInfo = new EditorInfo();
         mEditorInfo.packageName = TEST_EDITOR_PKG_NAME;
 
@@ -202,7 +201,7 @@
         // Injecting and mocked InputMethodBindingController and InputMethod.
         mMockInputMethodInvoker = IInputMethodInvoker.create(mMockInputMethod);
         mInputManagerGlobalSession = InputManagerGlobal.createTestSession(mMockIInputManager);
-        when(mMockInputMethodBindingController.getUserId()).thenReturn(mCallingUserId);
+        when(mMockInputMethodBindingController.getUserId()).thenReturn(mUserId);
         synchronized (ImfLock.class) {
             when(mMockInputMethodBindingController.getCurMethod())
                     .thenReturn(mMockInputMethodInvoker);
@@ -222,7 +221,7 @@
                 .thenReturn(new int[] {0});
         when(mMockUserManagerInternal.getUserIds()).thenReturn(new int[] {0});
         when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true);
-        when(mMockActivityManagerInternal.getCurrentUserId()).thenReturn(mCallingUserId);
+        when(mMockActivityManagerInternal.getCurrentUserId()).thenReturn(mUserId);
         when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt()))
                 .thenReturn(Binder.getCallingUid());
         when(mMockPackageManagerInternal.isSameApp(anyString(), anyLong(), anyInt(), anyInt()))
@@ -272,14 +271,13 @@
 
         // Certain tests rely on TEST_IME_ID that is installed with AndroidTest.xml.
         // TODO(b/352615651): Consider just synthesizing test InputMethodInfo then injecting it.
-        AdditionalSubtypeMapRepository.initializeIfNecessary(mCallingUserId);
+        AdditionalSubtypeMapRepository.initializeIfNecessary(mUserId);
         final var settings = InputMethodManagerService.queryInputMethodServicesInternal(mContext,
-                mCallingUserId, AdditionalSubtypeMapRepository.get(mCallingUserId),
-                DirectBootAwareness.AUTO);
-        InputMethodSettingsRepository.put(mCallingUserId, settings);
+                mUserId, AdditionalSubtypeMapRepository.get(mUserId), DirectBootAwareness.AUTO);
+        InputMethodSettingsRepository.put(mUserId, settings);
 
         // Emulate that the user initialization is done.
-        mInputMethodManagerService.getUserData(mCallingUserId).mBackgroundLoadLatch.countDown();
+        mInputMethodManagerService.getUserData(mUserId).mBackgroundLoadLatch.countDown();
 
         // After this boot phase, services can broadcast Intents.
         lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
@@ -291,7 +289,7 @@
 
     @After
     public void tearDown() {
-        InputMethodSettingsRepository.remove(mCallingUserId);
+        InputMethodSettingsRepository.remove(mUserId);
 
         if (mInputMethodManagerService != null) {
             mInputMethodManagerService.mInputMethodDeviceConfigs.destroy();
@@ -347,8 +345,8 @@
         synchronized (ImfLock.class) {
             ClientState cs = mInputMethodManagerService.getClientStateLocked(client);
             cs.mCurSession = new InputMethodManagerService.SessionState(cs,
-                    mMockInputMethodInvoker, mMockInputMethodSession, mock(
-                    InputChannel.class));
+                    mMockInputMethodInvoker, mMockInputMethodSession, mock(InputChannel.class),
+                    mUserId);
         }
     }
 }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
index ffc4df8..c5b5668 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -263,7 +263,7 @@
                 mMockRemoteInputConnection /* inputConnection */,
                 mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
                 mTargetSdkVersion /* unverifiedTargetSdkVersion */,
-                mCallingUserId /* userId */,
+                mUserId /* userId */,
                 mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
     }
 
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceUtilsTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceUtilsTest.java
new file mode 100644
index 0000000..4a2396d
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceUtilsTest.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.pm.PackageInfoLite;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.pm.parsing.pkg.PackageImpl;
+import com.android.server.pm.pkg.AndroidPackage;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.UUID;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PackageManagerServiceUtilsTest {
+
+    private static final String PACKAGE_NAME = "com.android.app";
+    private static final File CODE_PATH =
+            InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
+
+    @Test
+    public void testCheckDowngrade_packageSetting_versionCodeSmaller_throwException()
+            throws Exception {
+        final PackageSetting before = createPackageSetting();
+        before.setLongVersionCode(2);
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+
+        assertThrows(PackageManagerException.class,
+                () -> PackageManagerServiceUtils.checkDowngrade(before, after));
+    }
+
+    @Test
+    public void testCheckDowngrade_packageSetting_baseRevisionCodeSmaller_throwException()
+            throws Exception {
+        final PackageSetting before = createPackageSetting();
+        before.setLongVersionCode(1);
+        before.setBaseRevisionCode(2);
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.baseRevisionCode = 1;
+
+        assertThrows(PackageManagerException.class,
+                () -> PackageManagerServiceUtils.checkDowngrade(before, after));
+    }
+
+    @Test
+    public void testCheckDowngrade_packageSetting_splitArraySizeIsDifferent_throwException()
+            throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final String[] splitNames = new String[] { splitOne, splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionOne };
+        final int[] afterSplitRevisionCodes = new int[] { revisionOne, revisionTwo };
+
+        final PackageSetting before = createPackageSetting();
+        before.setLongVersionCode(1);
+        before.setSplitNames(splitNames);
+        before.setSplitRevisionCodes(beforeSplitRevisionCodes);
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = splitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        assertThrows(PackageManagerException.class,
+                () -> PackageManagerServiceUtils.checkDowngrade(before, after));
+    }
+
+    @Test
+    public void testCheckDowngrade_packageSetting_splitRevisionCodeSmaller_throwException()
+            throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final int revisionThree = 360;
+        final String[] splitNames = new String[] { splitOne, splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionTwo, revisionThree};
+        final int[] afterSplitRevisionCodes = new int[] { revisionOne, revisionTwo };
+
+        final PackageSetting before = createPackageSetting();
+        before.setLongVersionCode(1);
+        before.setSplitNames(splitNames);
+        before.setSplitRevisionCodes(beforeSplitRevisionCodes);
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = splitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        assertThrows(PackageManagerException.class,
+                () -> PackageManagerServiceUtils.checkDowngrade(before, after));
+    }
+
+    @Test
+    public void testCheckDowngrade_packageSetting_sameSplitNameRevisionsBigger()
+            throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final int revisionThree = 360;
+        final String[] splitNames = new String[] { splitOne, splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionOne, revisionTwo};
+        final int[] afterSplitRevisionCodes = new int[] { revisionOne, revisionThree };
+
+        final PackageSetting before = createPackageSetting();
+        before.setLongVersionCode(1);
+        before.setSplitNames(splitNames);
+        before.setSplitRevisionCodes(beforeSplitRevisionCodes);
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = splitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        PackageManagerServiceUtils.checkDowngrade(before, after);
+    }
+
+    @Test
+    public void testCheckDowngrade_packageSetting_hasDifferentSplitNames() throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final int revisionThree = 360;
+        final String[] beforeSplitNames = new String[] { splitOne, splitTwo };
+        final String[] afterSplitNames = new String[] { splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionOne, revisionTwo};
+        final int[] afterSplitRevisionCodes = new int[] { revisionThree };
+
+        final PackageSetting before = createPackageSetting();
+        before.setLongVersionCode(1);
+        before.setSplitNames(beforeSplitNames);
+        before.setSplitRevisionCodes(beforeSplitRevisionCodes);
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = afterSplitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        PackageManagerServiceUtils.checkDowngrade(before, after);
+    }
+
+    @Test
+    public void testCheckDowngrade_packageSetting_newSplitName() throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final String[] beforeSplitNames = new String[] { splitOne };
+        final String[] afterSplitNames = new String[] { splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionTwo };
+        final int[] afterSplitRevisionCodes = new int[] { revisionOne };
+
+        final PackageSetting before = createPackageSetting();
+        before.setLongVersionCode(1);
+        before.setSplitNames(beforeSplitNames);
+        before.setSplitRevisionCodes(beforeSplitRevisionCodes);
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = afterSplitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        PackageManagerServiceUtils.checkDowngrade(before, after);
+    }
+
+    @Test
+    public void testCheckDowngrade_androidPackage_versionCodeSmaller_throwException()
+            throws Exception {
+        final AndroidPackage before = PackageImpl.forTesting(PACKAGE_NAME).hideAsParsed()
+                .setVersionCode(2).hideAsFinal();
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+
+        assertThrows(PackageManagerException.class,
+                () -> PackageManagerServiceUtils.checkDowngrade(before, after));
+    }
+
+    @Test
+    public void testCheckDowngrade_androidPackage_baseRevisionCodeSmaller_throwException()
+            throws Exception {
+        final AndroidPackage before = PackageImpl.forTesting(PACKAGE_NAME).setBaseRevisionCode(2)
+                .hideAsParsed().setVersionCode(1).hideAsFinal();
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.baseRevisionCode = 1;
+
+        assertThrows(PackageManagerException.class,
+                () -> PackageManagerServiceUtils.checkDowngrade(before, after));
+    }
+
+    @Test
+    public void testCheckDowngrade_androidPackage_splitArraySizeIsDifferent_throwException()
+            throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final String[] splitNames = new String[] { splitOne, splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionOne };
+        final int[] afterSplitRevisionCodes = new int[] { revisionOne, revisionTwo };
+
+        final AndroidPackage before = PackageImpl.forTesting(PACKAGE_NAME)
+                .asSplit(splitNames, /* splitCodePaths= */ null,
+                        beforeSplitRevisionCodes, /* splitDependencies= */ null)
+                .hideAsParsed().setVersionCode(1).hideAsFinal();
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = splitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        assertThrows(PackageManagerException.class,
+                () -> PackageManagerServiceUtils.checkDowngrade(before, after));
+    }
+
+    @Test
+    public void testCheckDowngrade_androidPackage_splitRevisionCodeSmaller_throwException()
+            throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final int revisionThree = 360;
+        final String[] splitNames = new String[] { splitOne, splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionTwo, revisionThree};
+        final int[] afterSplitRevisionCodes = new int[] { revisionOne, revisionTwo };
+
+        final AndroidPackage before = PackageImpl.forTesting(PACKAGE_NAME)
+                .asSplit(splitNames, /* splitCodePaths= */ null,
+                        beforeSplitRevisionCodes, /* splitDependencies= */ null)
+                .hideAsParsed().setVersionCode(1).hideAsFinal();
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = splitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        assertThrows(PackageManagerException.class,
+                () -> PackageManagerServiceUtils.checkDowngrade(before, after));
+    }
+
+    @Test
+    public void testCheckDowngrade_androidPackage_sameSplitNameRevisionsBigger()
+            throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final int revisionThree = 360;
+        final String[] splitNames = new String[] { splitOne, splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionOne, revisionTwo};
+        final int[] afterSplitRevisionCodes = new int[] { revisionOne, revisionThree };
+
+        final AndroidPackage before = PackageImpl.forTesting(PACKAGE_NAME)
+                .asSplit(splitNames, /* splitCodePaths= */ null,
+                        beforeSplitRevisionCodes, /* splitDependencies= */ null)
+                .hideAsParsed().setVersionCode(1).hideAsFinal();
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = splitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        PackageManagerServiceUtils.checkDowngrade(before, after);
+    }
+
+    @Test
+    public void testCheckDowngrade_androidPackage_hasDifferentSplitNames() throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final int revisionThree = 360;
+        final String[] beforeSplitNames = new String[] { splitOne, splitTwo };
+        final String[] afterSplitNames = new String[] { splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionOne, revisionTwo};
+        final int[] afterSplitRevisionCodes = new int[] { revisionThree };
+
+        final AndroidPackage before = PackageImpl.forTesting(PACKAGE_NAME)
+                .asSplit(beforeSplitNames, /* splitCodePaths= */ null,
+                        beforeSplitRevisionCodes, /* splitDependencies= */ null)
+                .hideAsParsed().setVersionCode(1).hideAsFinal();
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = afterSplitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        PackageManagerServiceUtils.checkDowngrade(before, after);
+    }
+
+    @Test
+    public void testCheckDowngrade_androidPackage_newSplitName() throws Exception {
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        final String[] beforeSplitNames = new String[] { splitOne };
+        final String[] afterSplitNames = new String[] { splitTwo };
+        final int[] beforeSplitRevisionCodes = new int[] { revisionTwo };
+        final int[] afterSplitRevisionCodes = new int[] { revisionOne };
+
+        final AndroidPackage before = PackageImpl.forTesting(PACKAGE_NAME)
+                .asSplit(beforeSplitNames, /* splitCodePaths= */ null,
+                        beforeSplitRevisionCodes, /* splitDependencies= */ null)
+                .hideAsParsed().setVersionCode(1).hideAsFinal();
+        final PackageInfoLite after = new PackageInfoLite();
+        after.versionCode = 1;
+        after.splitNames = afterSplitNames;
+        after.splitRevisionCodes = afterSplitRevisionCodes;
+
+        PackageManagerServiceUtils.checkDowngrade(before, after);
+    }
+
+    private PackageSetting createPackageSetting() {
+        return new PackageSetting(PACKAGE_NAME, PACKAGE_NAME, CODE_PATH, /* pkgFlags= */ 0,
+                /* privateFlags= */ 0 , UUID.randomUUID());
+    }
+}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index dec4634..d7af443 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -1092,7 +1092,7 @@
     }
 
     @Test
-    public void testNoPkg_writeReadSplitVersions() {
+    public void testNoPkgDifferentRevisions_writeReadSplitVersions() {
         Settings settings = makeSettings();
         PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
         packageSetting.setAppId(Process.FIRST_APPLICATION_UID);
@@ -1117,6 +1117,54 @@
     }
 
     @Test
+    public void testNoPkgSameRevisions_writeReadSplitVersions() {
+        Settings settings = makeSettings();
+        PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
+        packageSetting.setAppId(Process.FIRST_APPLICATION_UID);
+
+        final String splitOne = "one";
+        final String splitTwo = "two";
+        final int revisionOne = 311;
+        packageSetting.setSplitNames(new String[] { splitOne, splitTwo});
+        packageSetting.setSplitRevisionCodes(new int[] { revisionOne, revisionOne});
+        settings.mPackages.put(PACKAGE_NAME_1, packageSetting);
+
+        settings.writeLPr(computer, /* sync= */ true);
+        settings.mPackages.clear();
+
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1);
+        assertThat(resultSetting.getSplitNames()[0], is(splitOne));
+        assertThat(resultSetting.getSplitNames()[1], is(splitTwo));
+        assertThat(resultSetting.getSplitRevisionCodes()[0], is(revisionOne));
+        assertThat(resultSetting.getSplitRevisionCodes()[1], is(revisionOne));
+    }
+
+    @Test
+    public void testNoPkgSameSplitNames_writeReadSplitVersions() {
+        Settings settings = makeSettings();
+        PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
+        packageSetting.setAppId(Process.FIRST_APPLICATION_UID);
+
+        final String splitOne = "one";
+        final int revisionOne = 311;
+        final int revisionTwo = 330;
+        packageSetting.setSplitNames(new String[] { splitOne, splitOne});
+        packageSetting.setSplitRevisionCodes(new int[] { revisionOne, revisionTwo});
+        settings.mPackages.put(PACKAGE_NAME_1, packageSetting);
+
+        settings.writeLPr(computer, /* sync= */ true);
+        settings.mPackages.clear();
+
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1);
+        assertThat(resultSetting.getSplitNames().length, is(1));
+        assertThat(resultSetting.getSplitRevisionCodes().length, is(1));
+        assertThat(resultSetting.getSplitNames()[0], is(splitOne));
+        assertThat(resultSetting.getSplitRevisionCodes()[0], is(revisionTwo));
+    }
+
+    @Test
     public void testWriteReadArchiveState() {
         Settings settings = makeSettings();
         PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
index fb73aff..f3cd0c9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessMappingStrategyTest.java
@@ -693,6 +693,21 @@
     }
 
     @Test
+    public void testGetPreset() {
+        int preset = Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM;
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_FOR_ALS, preset);
+        setUpResources();
+        DisplayDeviceConfig ddc = new DdcBuilder()
+                .setAutoBrightnessLevels(AUTO_BRIGHTNESS_MODE_DEFAULT, preset, DISPLAY_LEVELS)
+                .setAutoBrightnessLevelsLux(AUTO_BRIGHTNESS_MODE_DEFAULT, preset, LUX_LEVELS)
+                .build();
+        BrightnessMappingStrategy strategy = BrightnessMappingStrategy.create(mContext, ddc,
+                AUTO_BRIGHTNESS_MODE_DEFAULT, /* displayWhiteBalanceController= */ null);
+        assertEquals(preset, strategy.getPreset());
+    }
+
+    @Test
     public void testAutoBrightnessModeAndPreset() {
         int mode = AUTO_BRIGHTNESS_MODE_DOZE;
         int preset = Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
index d672435..6929690 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
@@ -225,5 +225,37 @@
         assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX)
         assertThat(modifier.brightnessLowerBound).isEqualTo(LOW_LUX_BRIGHTNESS)
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+    fun testUserSwitch() {
+        // nits: 0.5 -> backlight 0.01 -> brightness -> 0.05
+        whenever(mockDisplayDeviceConfig.getBacklightFromNits(/* nits= */ 0.5f))
+            .thenReturn(0.01f)
+        whenever(mockDisplayDeviceConfig.getBrightnessFromBacklight(/* backlight = */ 0.01f))
+            .thenReturn(0.05f)
+
+        Settings.Secure.putIntForUser(context.contentResolver,
+            Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, USER_ID) // off
+        Settings.Secure.putFloatForUser(context.contentResolver,
+            Settings.Secure.EVEN_DIMMER_MIN_NITS, 1.0f, USER_ID)
+
+        modifier.recalculateLowerBound()
+
+        assertThat(modifier.isActive).isFalse()
+        assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT)
+        assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - i.e. off
+
+        Settings.Secure.putIntForUser(context.contentResolver,
+            Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, USER_ID) // on
+        Settings.Secure.putFloatForUser(context.contentResolver,
+            Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.5f, USER_ID)
+        modifier.onSwitchUser()
+
+        assertThat(modifier.isActive).isTrue()
+        assertThat(modifier.brightnessReason).isEqualTo(
+            BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND)
+        assertThat(modifier.brightnessLowerBound).isEqualTo(0.05f)
+    }
 }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt
index f59e127..79b99b5 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt
@@ -29,6 +29,7 @@
 import com.android.server.display.config.createSensorData
 import com.android.server.display.utils.AmbientFilter
 import org.junit.Before
+import org.junit.Test
 import org.mockito.kotlin.any
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.eq
@@ -62,31 +63,35 @@
             mockLightSensorListener, mockHandler, testInjector)
     }
 
-    fun `does not register light sensor if is not configured`() {
+    @Test
+    fun doesNotRegisterLightSensorIfNotConfigured() {
         controller.restart()
 
         verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener)
     }
 
-    fun `does not register light sensor if missing`() {
+    @Test
+    fun doesNotRegisterLightSensorIfMissing() {
         controller.configure(dummySensorData, DISPLAY_ID)
         controller.restart()
 
         verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener)
     }
 
-    fun `register light sensor if configured and present`() {
+    @Test
+    fun registerLightSensorIfConfiguredAndPresent() {
         testInjector.lightSensor = TestUtils
                 .createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT)
         controller.configure(dummySensorData, DISPLAY_ID)
         controller.restart()
 
         verify(mockSensorManager).registerListener(any(),
-            testInjector.lightSensor, LIGHT_SENSOR_RATE * 1000, mockHandler)
+            eq(testInjector.lightSensor), eq(LIGHT_SENSOR_RATE * 1000), eq(mockHandler))
         verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener)
     }
 
-    fun `register light sensor once if not changed`() {
+    @Test
+    fun registerLightSensorOnceIfNotChanged() {
         testInjector.lightSensor = TestUtils
                 .createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT)
         controller.configure(dummySensorData, DISPLAY_ID)
@@ -95,11 +100,12 @@
         controller.restart()
 
         verify(mockSensorManager).registerListener(any(),
-            testInjector.lightSensor, LIGHT_SENSOR_RATE * 1000, mockHandler)
+            eq(testInjector.lightSensor), eq(LIGHT_SENSOR_RATE * 1000), eq(mockHandler))
         verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener)
     }
 
-    fun `register new light sensor and unregister old if changed`() {
+    @Test
+    fun registerNewAndUnregisterOldLightSensorIfChanged() {
         val lightSensor1 = TestUtils
                 .createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT)
         testInjector.lightSensor = lightSensor1
@@ -112,19 +118,21 @@
         controller.configure(dummySensorData, DISPLAY_ID)
         controller.restart()
 
-        inOrder {
+        inOrder(mockSensorManager, mockAmbientFilter, mockLightSensorListener) {
             verify(mockSensorManager).registerListener(any(),
-                lightSensor1, LIGHT_SENSOR_RATE * 1000, mockHandler)
-            verify(mockSensorManager).unregisterListener(any<SensorEventListener>())
+                eq(lightSensor1), eq(LIGHT_SENSOR_RATE * 1000), eq(mockHandler))
+            verify(mockSensorManager).registerListener(any(),
+                eq(lightSensor2), eq(LIGHT_SENSOR_RATE * 1000), eq(mockHandler))
+            verify(mockSensorManager).unregisterListener(any<SensorEventListener>(),
+                eq(lightSensor1))
             verify(mockAmbientFilter).clear()
             verify(mockLightSensorListener).onAmbientLuxChange(LightSensorController.INVALID_LUX)
-            verify(mockSensorManager).registerListener(any(),
-                lightSensor2, LIGHT_SENSOR_RATE * 1000, mockHandler)
         }
         verifyNoMoreInteractions(mockSensorManager, mockAmbientFilter, mockLightSensorListener)
     }
 
-    fun `notifies listener on ambient lux change`() {
+    @Test
+    fun notifiesListenerOnAmbientLuxChange() {
         val expectedLux = 40f
         val eventLux = 50
         val eventTime = 60L
@@ -141,7 +149,7 @@
         listener.onSensorChanged(TestUtils.createSensorEvent(testInjector.lightSensor,
             eventLux, eventTime * 1_000_000))
 
-        inOrder {
+        inOrder(mockAmbientFilter, mockLightSensorListener) {
             verify(mockAmbientFilter).addValue(eventTime, eventLux.toFloat())
             verify(mockLightSensorListener).onAmbientLuxChange(expectedLux)
         }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index 242d559..62400eb 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -1088,6 +1088,21 @@
     }
 
     @Test
+    public void testModeSwitching_UserSwitch() {
+        DisplayModeDirector director = createDirectorFromFpsRange(0, 90);
+        assertThat(director.getModeSwitchingType()).isEqualTo(
+                DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS);
+
+        int newModeSwitchingType = DisplayManager.SWITCHING_TYPE_NONE;
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                Settings.Secure.MATCH_CONTENT_FRAME_RATE, newModeSwitchingType);
+        director.onSwitchUser();
+        waitForIdleSync();
+
+        assertThat(director.getModeSwitchingType()).isEqualTo(newModeSwitchingType);
+    }
+
+    @Test
     public void testDefaultDisplayModeIsSelectedIfAvailable() {
         final float[] refreshRates = new float[]{24f, 25f, 30f, 60f, 90f};
         final int defaultModeId = 3;
@@ -1883,6 +1898,62 @@
     }
 
     @Test
+    public void testPeakRefreshRate_UserSwitch() {
+        when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled())
+                .thenReturn(true);
+        DisplayModeDirector director =
+                new DisplayModeDirector(mContext, mHandler, mInjector,
+                        mDisplayManagerFlags, mDisplayDeviceConfigProvider);
+        director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON);
+
+        Display.Mode[] modes1 = new Display.Mode[] {
+                new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 60),
+                new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 130),
+        };
+        Display.Mode[] modes2 = new Display.Mode[] {
+                new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 60),
+                new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 140),
+        };
+        SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>();
+        supportedModesByDisplay.put(DISPLAY_ID, modes1);
+        supportedModesByDisplay.put(DISPLAY_ID_2, modes2);
+
+        Sensor lightSensor = createLightSensor();
+        SensorManager sensorManager = createMockSensorManager(lightSensor);
+        director.start(sensorManager);
+        director.injectSupportedModesByDisplay(supportedModesByDisplay);
+
+        // Disable Smooth Display
+        setPeakRefreshRate(RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+
+        Vote vote1 = director.getVote(DISPLAY_ID,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        Vote vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+        assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+
+        // Switch user to one that has Smooth Display Enabled
+        Settings.System.putFloat(mContext.getContentResolver(), Settings.System.PEAK_REFRESH_RATE,
+                Float.POSITIVE_INFINITY);
+        director.onSwitchUser();
+        waitForIdleSync();
+
+        vote1 = director.getVote(DISPLAY_ID,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0, /* frameRateHigh= */ 130);
+        assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0, /* frameRateHigh= */ 140);
+    }
+
+    @Test
     @Parameters({
         "true, true, 60",
         "false, true, 50",
@@ -2036,6 +2107,62 @@
     }
 
     @Test
+    public void testMinRefreshRate_UserSwitch() {
+        when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled())
+                .thenReturn(true);
+        DisplayModeDirector director =
+                new DisplayModeDirector(mContext, mHandler, mInjector,
+                        mDisplayManagerFlags, mDisplayDeviceConfigProvider);
+        director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON);
+
+        Display.Mode[] modes1 = new Display.Mode[] {
+                new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 60),
+                new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 130),
+        };
+        Display.Mode[] modes2 = new Display.Mode[] {
+                new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 60),
+                new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 140),
+        };
+        SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>();
+        supportedModesByDisplay.put(DISPLAY_ID, modes1);
+        supportedModesByDisplay.put(DISPLAY_ID_2, modes2);
+
+        Sensor lightSensor = createLightSensor();
+        SensorManager sensorManager = createMockSensorManager(lightSensor);
+        director.start(sensorManager);
+        director.injectSupportedModesByDisplay(supportedModesByDisplay);
+
+        // Disable Force Peak Refresh Rate
+        setMinRefreshRate(0);
+
+        Vote vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
+        Vote vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ Float.POSITIVE_INFINITY);
+        assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 0,
+                /* frameRateHigh= */ Float.POSITIVE_INFINITY);
+
+        // Switch user to one that has Force Peak Refresh Rate enabled
+        Settings.System.putFloat(mContext.getContentResolver(), Settings.System.MIN_REFRESH_RATE,
+                Float.POSITIVE_INFINITY);
+        director.onSwitchUser();
+        waitForIdleSync();
+
+        vote1 = director.getVote(DISPLAY_ID, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
+        vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE);
+        assertVoteForRenderFrameRateRange(vote1, /* frameRateLow= */ 130,
+                /* frameRateHigh= */ Float.POSITIVE_INFINITY);
+        assertVoteForRenderFrameRateRange(vote2, /* frameRateLow= */ 140,
+                /* frameRateHigh= */ Float.POSITIVE_INFINITY);
+    }
+
+    @Test
     public void testPeakAndMinRefreshRate_FlagEnabled_DisplayWithOneMode() {
         when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled())
                 .thenReturn(true);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
index 8d0b279..fc28f9e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
@@ -266,8 +266,8 @@
         rule.mocks().getHandler().flush();
 
         ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any(),
-                any(), any());
+        verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(),
+                (Bundle) any(), any(), any());
         Intent value = intentCaptor.getValue();
         assertThat(value.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE);
         assertThat(value.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)).isEqualTo(
@@ -336,8 +336,8 @@
         rule.mocks().getHandler().flush();
 
         ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any(),
-                any(), any());
+        verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(),
+                (Bundle) any(), any(), any());
         Intent value = intentCaptor.getValue();
         assertThat(value.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE);
         assertThat(value.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)).isEqualTo(
diff --git a/services/tests/performancehinttests/Android.bp b/services/tests/performancehinttests/Android.bp
new file mode 100644
index 0000000..1692921c
--- /dev/null
+++ b/services/tests/performancehinttests/Android.bp
@@ -0,0 +1,34 @@
+package {
+    default_team: "trendy_team_games",
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "PerformanceHintTests",
+    srcs: [
+        "src/**/*.java",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "flag-junit",
+        "junit",
+        "mockito-target-minus-junit4",
+        "platform-test-annotations",
+        "services.core",
+        "truth",
+    ],
+    libs: [
+        "android.test.base",
+    ],
+    test_suites: [
+        "automotive-tests",
+        "device-tests",
+    ],
+    platform_apis: true,
+    certificate: "platform",
+    dxflags: ["--multi-dex"],
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/services/tests/performancehinttests/AndroidManifest.xml b/services/tests/performancehinttests/AndroidManifest.xml
new file mode 100644
index 0000000..d955234
--- /dev/null
+++ b/services/tests/performancehinttests/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.server.power.hinttests">
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.server.power.hinttests"
+         android:label="ADPF Performance Hint Service Test"/>
+</manifest>
diff --git a/services/tests/performancehinttests/AndroidTest.xml b/services/tests/performancehinttests/AndroidTest.xml
new file mode 100644
index 0000000..578b7d6
--- /dev/null
+++ b/services/tests/performancehinttests/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Performance Hint Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="PerformanceHintTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="PerformanceHintTests" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.server.power.hinttests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+        <option name="exclude-annotation" value="androidx.test.filters.FlakyTest" />
+    </test>
+</configuration>
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/OWNERS b/services/tests/performancehinttests/OWNERS
similarity index 100%
rename from services/tests/servicestests/src/com/android/server/power/hint/OWNERS
rename to services/tests/performancehinttests/OWNERS
diff --git a/services/tests/performancehinttests/TEST_MAPPING b/services/tests/performancehinttests/TEST_MAPPING
new file mode 100644
index 0000000..fa7b897
--- /dev/null
+++ b/services/tests/performancehinttests/TEST_MAPPING
@@ -0,0 +1,19 @@
+{
+  "presubmit": [
+    {
+      "name": "PerformanceHintTests",
+      "options": [
+        {"exclude-annotation": "org.junit.Ignore"}
+      ]
+    }
+  ],
+  "ravenwood-postsubmit": [
+    {
+      "name": "PerformanceHintTestsRavenwood",
+      "host": true,
+      "options": [
+        {"exclude-annotation": "android.platform.test.annotations.DisabledOnRavenwood"}
+      ]
+    }
+  ]
+}
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
rename to services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 1decd36..7d04470 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -596,7 +596,7 @@
                 ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
         service.mUidObserver.onUidStateChanged(UID,
                 ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
-        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
                 CLEAN_UP_UID_DELAY_MILLIS));
         verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
         reset(mNativeWrapperMock);
@@ -653,7 +653,7 @@
                 ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
         service.mUidObserver.onUidStateChanged(UID,
                 ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
-        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
                 CLEAN_UP_UID_DELAY_MILLIS));
         verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
         verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
@@ -666,7 +666,7 @@
         service.mUidObserver.onUidStateChanged(UID,
                 ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
         // wait for the async uid state change to trigger resume and setThreads
-        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000));
         verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2), eq(expectedTids2));
         reset(mNativeWrapperMock);
 
@@ -675,7 +675,7 @@
         LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
         service.mUidObserver.onUidStateChanged(UID,
                 ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
-        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
                 CLEAN_UP_UID_DELAY_MILLIS));
         verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
         verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
@@ -684,7 +684,7 @@
         verifyAllHintsEnabled(session2, false);
         service.mUidObserver.onUidStateChanged(UID,
                 ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
-        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000));
         verifyAllHintsEnabled(session1, false);
         verifyAllHintsEnabled(session2, true);
         reset(mNativeWrapperMock);
@@ -705,7 +705,7 @@
         LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
         service.mUidObserver.onUidStateChanged(UID,
                 ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
-        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
                 CLEAN_UP_UID_DELAY_MILLIS));
         verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
         verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
@@ -721,7 +721,7 @@
 
         service.mUidObserver.onUidStateChanged(UID,
                 ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
-        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000) + TimeUnit.MILLISECONDS.toNanos(
                 CLEAN_UP_UID_DELAY_MILLIS));
         verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1),
                 eq(SESSION_TIDS_A));
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index ce2bb95..d45e312 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -44,6 +44,7 @@
 import android.os.RemoteException;
 import android.os.VibrationAttributes;
 import android.os.Vibrator;
+import android.os.WorkSource;
 import android.os.test.TestLooper;
 import android.provider.Settings;
 import android.testing.TestableContext;
@@ -60,6 +61,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.concurrent.Executor;
@@ -231,7 +233,7 @@
     }
 
     @Test
-    public void testOnWakeLockListener_RemoteException_NoRethrow() {
+    public void testOnWakeLockListener_RemoteException_NoRethrow() throws RemoteException {
         when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
         createNotifier();
         clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
@@ -249,33 +251,58 @@
         verifyZeroInteractions(mWakeLockLog);
         mTestLooper.dispatchAll();
         verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, 1);
-
+        clearInvocations(mBatteryStats);
         mNotifier.onWakeLockAcquired(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag",
                 "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
                 exceptingCallback);
-        mNotifier.onWakeLockChanging(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag",
-                "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
-                exceptingCallback,
-                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
-                "my.package.name", uid, pid, /* newWorkSource= */ null, /* newHistoryTag= */ null,
-                exceptingCallback);
-        verifyNoMoreInteractions(mWakeLockLog);
+
+        verifyNoMoreInteractions(mWakeLockLog, mBatteryStats);
         mTestLooper.dispatchAll();
         verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", uid,
                 PowerManager.PARTIAL_WAKE_LOCK, 1);
+        verify(mBatteryStats).noteStartWakelock(uid, pid, "wakelockTag", /* historyTag= */ null,
+                BatteryStats.WAKE_TYPE_PARTIAL, false);
+
+        verifyNoMoreInteractions(mWakeLockLog, mBatteryStats);
+        WorkSource worksourceOld = Mockito.mock(WorkSource.class);
+        WorkSource worksourceNew = Mockito.mock(WorkSource.class);
+
+        mNotifier.onWakeLockChanging(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, worksourceOld, /* historyTag= */ null,
+                exceptingCallback,
+                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, worksourceNew, /* newHistoryTag= */ null,
+                exceptingCallback);
+        mTestLooper.dispatchAll();
+        verify(mBatteryStats).noteChangeWakelockFromSource(worksourceOld, pid, "wakelockTag",
+                null, BatteryStats.WAKE_TYPE_PARTIAL, worksourceNew, pid, "wakelockTag",
+                null, BatteryStats.WAKE_TYPE_FULL, false);
         // If we didn't throw, we're good!
 
         // Test with improveWakelockLatency flag false, hence the wakelock log will run on the same
         // thread
-        clearInvocations(mWakeLockLog);
+        clearInvocations(mWakeLockLog, mBatteryStats);
         when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(false);
 
+        // Acquire the wakelock
         mNotifier.onWakeLockAcquired(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag",
                 "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
                 exceptingCallback);
         verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", uid,
                 PowerManager.PARTIAL_WAKE_LOCK, -1);
 
+        // Update the wakelock
+        mNotifier.onWakeLockChanging(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, worksourceOld, /* historyTag= */ null,
+                exceptingCallback,
+                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+                "my.package.name", uid, pid, worksourceNew, /* newHistoryTag= */ null,
+                exceptingCallback);
+        verify(mBatteryStats).noteChangeWakelockFromSource(worksourceOld, pid, "wakelockTag",
+                null, BatteryStats.WAKE_TYPE_PARTIAL, worksourceNew, pid, "wakelockTag",
+                null, BatteryStats.WAKE_TYPE_FULL, false);
+
+        // Release the wakelock
         mNotifier.onWakeLockReleased(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag",
                 "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
                 exceptingCallback);
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index b737e0f..40c521a 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -1304,6 +1304,7 @@
                 .setDozeOverrideFromDreamManager(
                         Display.STATE_ON,
                         Display.STATE_REASON_DEFAULT_POLICY,
+                        PowerManager.BRIGHTNESS_INVALID_FLOAT,
                         PowerManager.BRIGHTNESS_DEFAULT);
         assertTrue(isAcquired[0]);
     }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerStatsProcessorTest.java
new file mode 100644
index 0000000..7000487
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerStatsProcessorTest.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.stats;
+
+import static android.os.BatteryConsumer.PROCESS_STATE_BACKGROUND;
+import static android.os.BatteryConsumer.PROCESS_STATE_CACHED;
+import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND;
+import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE;
+
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.POWER_STATE_OTHER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_ON;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_OTHER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_POWER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_PROCESS_STATE;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_SCREEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.hardware.input.InputSensorInfo;
+import android.os.BatteryConsumer;
+import android.os.BatteryStats;
+import android.os.Process;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import com.android.internal.os.MonotonicClock;
+import com.android.internal.os.PowerStats;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+public class SensorPowerStatsProcessorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
+    @Rule(order = 1)
+    public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
+            .initMeasuredEnergyStatsLocked();
+
+    private static final double PRECISION = 0.00001;
+    private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42;
+    private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 101;
+    private static final int SENSOR_HANDLE_1 = 77;
+    private static final int SENSOR_HANDLE_2 = 88;
+    private static final int SENSOR_HANDLE_3 = 99;
+
+    @Mock
+    private SensorManager mSensorManager;
+
+    private MonotonicClock mMonotonicClock;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mMonotonicClock = new MonotonicClock(0, mStatsRule.getMockClock());
+        Sensor sensor1 = createSensor(SENSOR_HANDLE_1, Sensor.TYPE_STEP_COUNTER,
+                Sensor.STRING_TYPE_STEP_COUNTER, "dancing", 100);
+        Sensor sensor2 = createSensor(SENSOR_HANDLE_2, Sensor.TYPE_MOTION_DETECT,
+                "com.example", "tango", 200);
+        Sensor sensor3 = createSensor(SENSOR_HANDLE_3, Sensor.TYPE_MOTION_DETECT,
+                "com.example", "waltz", 300);
+        when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(
+                List.of(sensor1, sensor2, sensor3));
+    }
+
+    @Test
+    public void testPowerEstimation() {
+        SensorPowerStatsProcessor processor = new SensorPowerStatsProcessor(() -> mSensorManager);
+
+        PowerComponentAggregatedPowerStats stats = createAggregatedPowerStats(processor);
+
+        processor.noteStateChange(stats, buildHistoryItem(0, true, APP_UID1, SENSOR_HANDLE_1));
+
+        // Turn the screen off after 2.5 seconds
+        stats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500);
+        stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500);
+        stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, 5000);
+
+        processor.noteStateChange(stats, buildHistoryItem(6000, false, APP_UID1, SENSOR_HANDLE_1));
+        processor.noteStateChange(stats, buildHistoryItem(7000, true, APP_UID2, SENSOR_HANDLE_1));
+        processor.noteStateChange(stats, buildHistoryItem(8000, true, APP_UID2, SENSOR_HANDLE_2));
+        processor.noteStateChange(stats, buildHistoryItem(9000, false, APP_UID2, SENSOR_HANDLE_1));
+
+        processor.finish(stats, 10000);
+
+        PowerStats.Descriptor descriptor = stats.getPowerStatsDescriptor();
+        SensorPowerStatsLayout statsLayout = new SensorPowerStatsLayout();
+        statsLayout.fromExtras(descriptor.extras);
+
+        String dump = stats.toString();
+        assertThat(dump).contains(" step_counter: ");
+        assertThat(dump).contains(" com.example.tango: ");
+
+        long[] uidStats = new long[descriptor.uidStatsArrayLength];
+
+        // For UID1:
+        // SENSOR1 was on for 6000 ms.
+        //   Estimated power: 6000 * 100 = 0.167 mAh
+        //     split between three different states
+        //          fg screen-on: 6000 * 2500/10000
+        //          bg screen-off: 6000 * 2500/10000
+        //          fgs screen-off: 6000 * 5000/10000
+        double expectedPower1 = 0.166666;
+        stats.getUidStats(uidStats, APP_UID1,
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower1 * 2500 / 10000);
+        stats.getUidStats(uidStats, APP_UID1,
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower1 * 2500 / 10000);
+        stats.getUidStats(uidStats, APP_UID1,
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower1 * 5000 / 10000);
+
+        // For UID2:
+        // SENSOR1 was on for 2000 ms.
+        //   Estimated power: 2000 * 100 = 0.0556 mAh
+        //     split between three different states
+        //          cached screen-on: 2000 * 2500/10000
+        //          cached screen-off: 2000 * 7500/10000
+        // SENSOR2 was on for 2000 ms.
+        //   Estimated power: 2000 * 200 = 0.11111 mAh
+        //     split between three different states
+        //          cached screen-on: 2000 * 2500/10000
+        //          cached screen-off: 2000 * 7500/10000
+        double expectedPower2 = 0.05555 + 0.11111;
+        stats.getUidStats(uidStats, APP_UID2,
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower2 * 2500 / 10000);
+        stats.getUidStats(uidStats, APP_UID2,
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower2 * 7500 / 10000);
+
+        long[] deviceStats = new long[descriptor.statsArrayLength];
+
+        stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON));
+        assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+                .isWithin(PRECISION).of((expectedPower1 + expectedPower2) * 2500 / 10000);
+
+        stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER));
+        assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+                .isWithin(PRECISION).of((expectedPower1 + expectedPower2) * 7500 / 10000);
+    }
+
+    private BatteryStats.HistoryItem buildHistoryItem(int timestamp, boolean stateOn,
+            int uid, int sensor) {
+        mStatsRule.setTime(timestamp, timestamp);
+        BatteryStats.HistoryItem historyItem = new BatteryStats.HistoryItem();
+        historyItem.time = mMonotonicClock.monotonicTime();
+        historyItem.states = stateOn ? BatteryStats.HistoryItem.STATE_SENSOR_ON_FLAG : 0;
+        if (stateOn) {
+            historyItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE
+                    | BatteryStats.HistoryItem.EVENT_FLAG_START;
+        } else {
+            historyItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE
+                    | BatteryStats.HistoryItem.EVENT_FLAG_FINISH;
+        }
+        historyItem.eventTag = historyItem.localEventTag;
+        historyItem.eventTag.uid = uid;
+        historyItem.eventTag.string = "sensor:0x" + Integer.toHexString(sensor);
+        return historyItem;
+    }
+
+    private int[] states(int... states) {
+        return states;
+    }
+
+    private static PowerComponentAggregatedPowerStats createAggregatedPowerStats(
+            SensorPowerStatsProcessor processor) {
+        AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig();
+        config.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_SENSORS)
+                .trackDeviceStates(
+                        AggregatedPowerStatsConfig.STATE_POWER,
+                        AggregatedPowerStatsConfig.STATE_SCREEN)
+                .trackUidStates(
+                        AggregatedPowerStatsConfig.STATE_POWER,
+                        AggregatedPowerStatsConfig.STATE_SCREEN,
+                        AggregatedPowerStatsConfig.STATE_PROCESS_STATE)
+                .setProcessor(processor);
+
+        AggregatedPowerStats aggregatedPowerStats = new AggregatedPowerStats(config);
+        PowerComponentAggregatedPowerStats powerComponentStats =
+                aggregatedPowerStats.getPowerComponentStats(
+                        BatteryConsumer.POWER_COMPONENT_SENSORS);
+        processor.start(powerComponentStats, 0);
+
+        powerComponentStats.setState(STATE_POWER, POWER_STATE_OTHER, 0);
+        powerComponentStats.setState(STATE_SCREEN, SCREEN_STATE_ON, 0);
+        powerComponentStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND, 0);
+        powerComponentStats.setUidState(APP_UID2, STATE_PROCESS_STATE, PROCESS_STATE_CACHED, 0);
+
+        return powerComponentStats;
+    }
+
+    private Sensor createSensor(int handle, int type, String stringType, String name, float power) {
+        if (RavenwoodRule.isOnRavenwood()) {
+            Sensor sensor = mock(Sensor.class);
+            when(sensor.getHandle()).thenReturn(handle);
+            when(sensor.getType()).thenReturn(type);
+            when(sensor.getStringType()).thenReturn(stringType);
+            when(sensor.getName()).thenReturn(name);
+            when(sensor.getPower()).thenReturn(power);
+            return sensor;
+        } else {
+            return new Sensor(new InputSensorInfo(name, "vendor", 0 /* version */,
+                    handle, type, 100.0f /*maxRange */, 0.02f /* resolution */,
+                    (float) power, 1000 /* minDelay */, 0 /* fifoReservedEventCount */,
+                    0 /* fifoMaxEventCount */, stringType /* stringType */,
+                    "" /* requiredPermission */, 0 /* maxDelay */, 0 /* flags */, 0 /* id */));
+        }
+    }
+}
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index a888dad..a86289b 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -78,7 +78,6 @@
         // TODO: remove once Android migrates to JUnit 4.12,
         // which provides assertThrows
         "testng",
-        "truth",
         "junit",
         "junit-params",
         "ActivityContext",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 20b9592..1afe12f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -840,6 +840,10 @@
         info_a.setComponentName(COMPONENT_NAME);
         final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
         info_b.setComponentName(new ComponentName("package", "class"));
+        writeStringsToSetting(Set.of(
+                info_a.getComponentName().flattenToString(),
+                info_b.getComponentName().flattenToString()),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
 
         AccessibilityUserState userState = mA11yms.getCurrentUserState();
         userState.mInstalledServices.clear();
@@ -858,10 +862,9 @@
         userState = mA11yms.getCurrentUserState();
         assertThat(userState.mEnabledServices).containsExactly(info_b.getComponentName());
         //Assert setting change
-        final Set<ComponentName> componentsFromSetting = new ArraySet<>();
-        mA11yms.readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
-                userState.mUserId, componentsFromSetting);
-        assertThat(componentsFromSetting).containsExactly(info_b.getComponentName());
+        final Set<String> enabledServices =
+                readStringsFromSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+        assertThat(enabledServices).containsExactly(info_b.getComponentName().flattenToString());
     }
 
     @Test
@@ -880,6 +883,10 @@
                         info_a.getComponentName().flattenToString(),
                         info_b.getComponentName().flattenToString()),
                 SOFTWARE);
+        writeStringsToSetting(Set.of(
+                        info_a.getComponentName().flattenToString(),
+                        info_b.getComponentName().flattenToString()),
+                ShortcutUtils.convertToKey(SOFTWARE));
 
         // despite force stopping both packages, only the first service has the relevant flag,
         // so only the first should be removed.
@@ -896,13 +903,53 @@
         assertThat(userState.getShortcutTargetsLocked(SOFTWARE)).containsExactly(
                 info_b.getComponentName().flattenToString());
         //Assert setting change
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        mA11yms.readColonDelimitedSettingToSet(ShortcutUtils.convertToKey(SOFTWARE),
-                userState.mUserId, str -> str, targetsFromSetting);
+        final Set<String> targetsFromSetting = readStringsFromSetting(
+                ShortcutUtils.convertToKey(SOFTWARE));
         assertThat(targetsFromSetting).containsExactly(info_b.getComponentName().flattenToString());
     }
 
     @Test
+    public void testPackagesForceStopped_otherServiceStopped_doesNotRemoveContinuousTarget() {
+        final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
+        info_a.setComponentName(COMPONENT_NAME);
+        info_a.flags = FLAG_REQUEST_ACCESSIBILITY_BUTTON;
+        final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
+        info_b.setComponentName(new ComponentName("package", "class"));
+        writeStringsToSetting(Set.of(
+                        info_a.getComponentName().flattenToString(),
+                        info_b.getComponentName().flattenToString()),
+                ShortcutUtils.convertToKey(SOFTWARE));
+
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.clear();
+        userState.mInstalledServices.add(info_a);
+        userState.mInstalledServices.add(info_b);
+        userState.updateShortcutTargetsLocked(Set.of(
+                        info_a.getComponentName().flattenToString(),
+                        info_b.getComponentName().flattenToString()),
+                SOFTWARE);
+
+        // Force stopping a service should not disable unrelated continuous services.
+        synchronized (mA11yms.getLock()) {
+            mA11yms.onPackagesForceStoppedLocked(
+                    new String[]{info_b.getComponentName().getPackageName()},
+                    userState);
+        }
+
+        //Assert user state change
+        userState = mA11yms.getCurrentUserState();
+        assertThat(userState.getShortcutTargetsLocked(SOFTWARE)).containsExactly(
+                info_a.getComponentName().flattenToString(),
+                info_b.getComponentName().flattenToString());
+        //Assert setting unchanged
+        final Set<String> targetsFromSetting = readStringsFromSetting(
+                ShortcutUtils.convertToKey(SOFTWARE));
+        assertThat(targetsFromSetting).containsExactly(
+                info_a.getComponentName().flattenToString(),
+                info_b.getComponentName().flattenToString());
+    }
+
+    @Test
     public void testPackageMonitorScanPackages_scansWithoutHoldingLock() {
         setupAccessibilityServiceConnection(0);
         final AtomicReference<Set<Boolean>> lockState = collectLockStateWhilePackageScanning();
@@ -1844,6 +1891,11 @@
         return result;
     }
 
+    private void writeStringsToSetting(Set<String> strings, String setting) {
+        mA11yms.persistColonDelimitedSetToSettingLocked(
+                setting, UserHandle.USER_SYSTEM, strings, str -> str);
+    }
+
     private void broadcastSettingRestored(String setting, String newValue) {
         Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
                 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 3ef81fd..60bcecc 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -620,7 +620,7 @@
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE)
-    public void testTwoFingerTap_StateIsActivated_shouldInDelegating() {
+    public void testTwoFingerTap_StateIsActivated_shouldInDetecting() {
         assumeTrue(isWatch());
         enableOneFingerPanning(false);
         goFromStateIdleTo(STATE_ACTIVATED);
@@ -629,14 +629,15 @@
         send(downEvent());
         send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y));
         send(upEvent());
-        fastForward(ViewConfiguration.getDoubleTapTimeout());
+        fastForward(mMgh.mDetectingState.mMultiTapMaxDelay);
 
-        assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
+        verify(mMgh.getNext(), times(3)).onMotionEvent(any(), any(), anyInt());
+        assertTrue(mMgh.mCurrentState == mMgh.mDetectingState);
     }
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE)
-    public void testTwoFingerTap_StateIsIdle_shouldInDelegating() {
+    public void testTwoFingerTap_StateIsIdle_shouldInDetecting() {
         assumeTrue(isWatch());
         enableOneFingerPanning(false);
         goFromStateIdleTo(STATE_IDLE);
@@ -645,9 +646,10 @@
         send(downEvent());
         send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y));
         send(upEvent());
-        fastForward(ViewConfiguration.getDoubleTapTimeout());
+        fastForward(mMgh.mDetectingState.mMultiTapMaxDelay);
 
-        assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
+        verify(mMgh.getNext(), times(3)).onMotionEvent(any(), any(), anyInt());
+        assertTrue(mMgh.mCurrentState == mMgh.mDetectingState);
     }
 
     @Test
@@ -982,6 +984,53 @@
     }
 
     @Test
+    public void testSingleFingerOverscrollAtTopEdge_isWatch_scrollDiagonally_noOverscroll() {
+        assumeTrue(isWatch());
+        goFromStateIdleTo(STATE_SINGLE_PANNING);
+        float centerX =
+                (INITIAL_MAGNIFICATION_BOUNDS.right + INITIAL_MAGNIFICATION_BOUNDS.left) / 2.0f;
+        mFullScreenMagnificationController.setCenter(
+                DISPLAY_0, centerX, INITIAL_MAGNIFICATION_BOUNDS.top, false, 1);
+        final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
+        PointF initCoords =
+                new PointF(
+                        mFullScreenMagnificationController.getCenterX(DISPLAY_0),
+                        mFullScreenMagnificationController.getCenterY(DISPLAY_0));
+        PointF edgeCoords = new PointF(initCoords.x, initCoords.y);
+        // Scroll diagonally towards top-right with a bigger right delta
+        edgeCoords.offset(swipeMinDistance * 2, swipeMinDistance);
+
+        swipeAndHold(initCoords, edgeCoords);
+
+        assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_NONE);
+        assertTrue(isZoomed());
+    }
+
+    @Test
+    public void
+            testSingleFingerOverscrollAtTopEdge_isWatch_scrollDiagonally_expectedOverscrollState() {
+        assumeTrue(isWatch());
+        goFromStateIdleTo(STATE_SINGLE_PANNING);
+        float centerX =
+                (INITIAL_MAGNIFICATION_BOUNDS.right + INITIAL_MAGNIFICATION_BOUNDS.left) / 2.0f;
+        mFullScreenMagnificationController.setCenter(
+                DISPLAY_0, centerX, INITIAL_MAGNIFICATION_BOUNDS.top, false, 1);
+        final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
+        PointF initCoords =
+                new PointF(
+                        mFullScreenMagnificationController.getCenterX(DISPLAY_0),
+                        mFullScreenMagnificationController.getCenterY(DISPLAY_0));
+        PointF edgeCoords = new PointF(initCoords.x, initCoords.y);
+        // Scroll diagonally towards top-right with a bigger top delta
+        edgeCoords.offset(swipeMinDistance, swipeMinDistance * 2);
+
+        swipeAndHold(initCoords, edgeCoords);
+
+        assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_VERTICAL_EDGE);
+        assertTrue(isZoomed());
+    }
+
+    @Test
     public void testSingleFingerScrollAtEdge_isWatch_noOverscroll() {
         assumeTrue(isWatch());
         goFromStateIdleTo(STATE_SINGLE_PANNING);
@@ -1057,9 +1106,24 @@
         assumeTrue(isWatch());
         goFromStateIdleTo(STATE_ACTIVATED);
 
-        swipeAndHold();
+        PointF pointer = DEFAULT_POINT;
+        send(downEvent(pointer.x, pointer.y));
+
+        // first move triggers the panning state
+        pointer.offset(100, 100);
         fastForward(20);
-        swipe(DEFAULT_POINT, new PointF(DEFAULT_X * 2, DEFAULT_Y * 2), /* durationMs= */ 20);
+        send(moveEvent(pointer.x, pointer.y));
+
+        // second move actually pans
+        pointer.offset(100, 100);
+        fastForward(20);
+        send(moveEvent(pointer.x, pointer.y));
+        pointer.offset(100, 100);
+        fastForward(20);
+        send(moveEvent(pointer.x, pointer.y));
+
+        fastForward(20);
+        send(upEvent(pointer.x, pointer.y));
 
         verify(mMockScroller).fling(
                 /* startX= */ anyInt(),
diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java
new file mode 100644
index 0000000..bc3a5ca
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appop;
+
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
+import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
+import static android.app.AppOpsManager.OP_FLAG_SELF;
+import static android.app.AppOpsManager.UID_STATE_FOREGROUND;
+import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AppOpsManager;
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.os.FileUtils;
+import android.os.Process;
+import android.permission.flags.Flags;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class DiscreteAppOpPersistenceTest {
+    private DiscreteRegistry mDiscreteRegistry;
+    private final Object mLock = new Object();
+    private File mMockDataDirectory;
+    private final Context mContext =
+            InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Before
+    public void setUp() {
+        mMockDataDirectory = mContext.getDir("mock_data", Context.MODE_PRIVATE);
+        mDiscreteRegistry = new DiscreteRegistry(mLock, mMockDataDirectory);
+        mDiscreteRegistry.systemReady();
+    }
+
+    @After
+    public void cleanUp() {
+        mDiscreteRegistry.writeAndClearAccessHistory();
+        FileUtils.deleteContents(mMockDataDirectory);
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_DEVICE_AWARE_APP_OP_NEW_SCHEMA_ENABLED)
+    @Test
+    public void defaultDevice_recordAccess_persistToDisk() {
+        int uid = Process.myUid();
+        String packageName = mContext.getOpPackageName();
+        int op = AppOpsManager.OP_CAMERA;
+        String deviceId = VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT;
+        long accessTime = System.currentTimeMillis();
+        long duration = 60000L;
+        int uidState = UID_STATE_FOREGROUND;
+        int opFlags = OP_FLAGS_ALL_TRUSTED;
+        int attributionFlags = ATTRIBUTION_FLAG_ACCESSOR;
+        int attributionChainId = AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
+
+        mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags,
+                uidState, accessTime, duration, attributionFlags, attributionChainId);
+
+        // Verify in-memory object is correct
+        fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
+                duration, uidState, opFlags, attributionFlags, attributionChainId);
+
+        // Write to disk and clear the in-memory object
+        mDiscreteRegistry.writeAndClearAccessHistory();
+
+        // Verify the storage file is created and then verify its content is correct
+        File[] files = FileUtils.listFilesOrEmpty(mMockDataDirectory);
+        assertThat(files.length).isEqualTo(1);
+        fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
+                duration, uidState, opFlags, attributionFlags, attributionChainId);
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_DEVICE_AWARE_APP_OP_NEW_SCHEMA_ENABLED)
+    @Test
+    public void externalDevice_recordAccess_persistToDisk() {
+        int uid = Process.myUid();
+        String packageName = mContext.getOpPackageName();
+        int op = AppOpsManager.OP_CAMERA;
+        String deviceId = "companion:1";
+        long accessTime = System.currentTimeMillis();
+        long duration = -1;
+        int uidState = UID_STATE_FOREGROUND_SERVICE;
+        int opFlags = OP_FLAG_SELF;
+        int attributionFlags = ATTRIBUTION_FLAG_RECEIVER;
+        int attributionChainId = 10;
+
+        mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags,
+                uidState, accessTime, duration, attributionFlags, attributionChainId);
+
+        fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
+                duration, uidState, opFlags, attributionFlags, attributionChainId);
+
+        mDiscreteRegistry.writeAndClearAccessHistory();
+
+        File[] files = FileUtils.listFilesOrEmpty(mMockDataDirectory);
+        assertThat(files.length).isEqualTo(1);
+        fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
+                duration, uidState, opFlags, attributionFlags, attributionChainId);
+    }
+
+    private void fetchDiscreteOpsAndValidate(int expectedUid, String expectedPackageName,
+            int expectedOp, String expectedDeviceId, String expectedAttrTag,
+            long expectedAccessTime, long expectedAccessDuration, int expectedUidState,
+            int expectedOpFlags, int expectedAttrFlags, int expectedAttrChainId) {
+        DiscreteRegistry.DiscreteOps discreteOps = mDiscreteRegistry.getAllDiscreteOps();
+
+        assertThat(discreteOps.isEmpty()).isFalse();
+        assertThat(discreteOps.mUids.size()).isEqualTo(1);
+
+        DiscreteRegistry.DiscreteUidOps discreteUidOps = discreteOps.mUids.get(expectedUid);
+        assertThat(discreteUidOps.mPackages.size()).isEqualTo(1);
+
+        DiscreteRegistry.DiscretePackageOps discretePackageOps =
+                discreteUidOps.mPackages.get(expectedPackageName);
+        assertThat(discretePackageOps.mPackageOps.size()).isEqualTo(1);
+
+        DiscreteRegistry.DiscreteOp discreteOp = discretePackageOps.mPackageOps.get(expectedOp);
+        assertThat(discreteOp.mDeviceAttributedOps.size()).isEqualTo(1);
+
+        DiscreteRegistry.DiscreteDeviceOp discreteDeviceOp =
+                discreteOp.mDeviceAttributedOps.get(expectedDeviceId);
+        assertThat(discreteDeviceOp.mAttributedOps.size()).isEqualTo(1);
+
+        List<DiscreteRegistry.DiscreteOpEvent> discreteOpEvents =
+                discreteDeviceOp.mAttributedOps.get(expectedAttrTag);
+        assertThat(discreteOpEvents.size()).isEqualTo(1);
+
+        DiscreteRegistry.DiscreteOpEvent discreteOpEvent = discreteOpEvents.get(0);
+        assertThat(discreteOpEvent.mNoteTime).isEqualTo(expectedAccessTime);
+        assertThat(discreteOpEvent.mNoteDuration).isEqualTo(expectedAccessDuration);
+        assertThat(discreteOpEvent.mUidState).isEqualTo(expectedUidState);
+        assertThat(discreteOpEvent.mOpFlag).isEqualTo(expectedOpFlags);
+        assertThat(discreteOpEvent.mAttributionFlags).isEqualTo(expectedAttrFlags);
+        assertThat(discreteOpEvent.mAttributionChainId).isEqualTo(expectedAttrChainId);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
new file mode 100644
index 0000000..b5a538f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.audio;
+
+import static com.android.media.audio.Flags.asDeviceConnectionFailure;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioDeviceInventoryTest {
+
+    private static final String TAG = "AudioDeviceInventoryTest";
+
+    @Mock private AudioService mMockAudioService;
+    private AudioDeviceInventory mDevInventory;
+    @Spy private AudioDeviceBroker mSpyAudioDeviceBroker;
+    @Spy private AudioSystemAdapter mSpyAudioSystem;
+
+    private SystemServerAdapter mSystemServer;
+
+    private BluetoothDevice mFakeBtDevice;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        mMockAudioService = mock(AudioService.class);
+        mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+        mDevInventory = new AudioDeviceInventory(mSpyAudioSystem);
+        mSystemServer = new NoOpSystemServerAdapter();
+        mSpyAudioDeviceBroker = spy(new AudioDeviceBroker(context, mMockAudioService, mDevInventory,
+                mSystemServer, mSpyAudioSystem));
+        mDevInventory.setDeviceBroker(mSpyAudioDeviceBroker);
+
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05");
+    }
+
+    @After
+    public void tearDown() throws Exception { }
+
+    /**
+     * test that for DEVICE_OUT_BLUETOOTH_A2DP devices, when the device connects, it's only
+     * added to the connected devices when the connection through AudioSystem is successful
+     * @throws Exception on error
+     */
+    @Test
+    public void testSetDeviceConnectionStateA2dp() throws Exception {
+        Log.i(TAG, "starting testSetDeviceConnectionStateA2dp");
+        assertTrue("collection of connected devices not empty at start",
+                mDevInventory.getConnectedDevices().isEmpty());
+
+        final AudioDeviceAttributes ada = new AudioDeviceAttributes(
+                AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, mFakeBtDevice.getAddress());
+        AudioDeviceBroker.BtDeviceInfo btInfo =
+                new AudioDeviceBroker.BtDeviceInfo(mFakeBtDevice, BluetoothProfile.A2DP,
+                        BluetoothProfile.STATE_CONNECTED, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+                        AudioSystem.AUDIO_FORMAT_SBC);
+
+        // test that no device is added when AudioSystem returns AUDIO_STATUS_ERROR
+        // when setDeviceConnectionState is called for the connection
+        // NOTE: for now this is only when flag asDeviceConnectionFailure is true
+        if (asDeviceConnectionFailure()) {
+            when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
+                    AudioSystem.AUDIO_FORMAT_DEFAULT))
+                    .thenReturn(AudioSystem.AUDIO_STATUS_ERROR);
+            runWithBluetoothPrivilegedPermission(
+                    () ->  mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
+                        /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC));
+
+            assertEquals(0, mDevInventory.getConnectedDevices().size());
+        }
+
+        // test that the device is added when AudioSystem returns AUDIO_STATUS_OK
+        // when setDeviceConnectionState is called for the connection
+        when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
+                AudioSystem.AUDIO_FORMAT_DEFAULT))
+                .thenReturn(AudioSystem.AUDIO_STATUS_OK);
+        runWithBluetoothPrivilegedPermission(
+                () ->  mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
+                    /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC));
+        assertEquals(1, mDevInventory.getConnectedDevices().size());
+    }
+
+    // TODO add test for hearing aid
+
+    // TODO add test for BLE
+
+    /**
+     * Executes a Runnable while holding the BLUETOOTH_PRIVILEGED permission
+     * @param toRunWithPermission the runnable to run with BT privileges
+     */
+    private void runWithBluetoothPrivilegedPermission(Runnable toRunWithPermission) {
+        try {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .adoptShellPermissionIdentity(Manifest.permission.BLUETOOTH_PRIVILEGED);
+            toRunWithPermission.run();
+        } finally {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .dropShellPermissionIdentity();
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index 3789531..36a7b3d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -1296,6 +1296,11 @@
             mFingerprints.add((Fingerprint) identifier);
         }
 
+        @Override
+        protected int getModality() {
+            return 0;
+        }
+
         public List<Fingerprint> getFingerprints() {
             return mFingerprints;
         }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java
index c9482ce..a34e796 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -30,12 +31,16 @@
 import android.hardware.fingerprint.Fingerprint;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.TestableContext;
 
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
@@ -69,6 +74,10 @@
     public final TestableContext mContext = new TestableContext(
             InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Mock
     ISession mSession;
     @Mock
@@ -168,6 +177,21 @@
         assertThat(mClient.getUnknownHALTemplates()).isEmpty();
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_NOTIFY_FINGERPRINT_LOE)
+    public void invalidBiometricUserState() throws Exception {
+        mClient =  createClient();
+
+        final List<Fingerprint> list = new ArrayList<>();
+        doReturn(true).when(mFingerprintUtils)
+                .hasValidBiometricUserState(mContext, 2);
+        doReturn(list).when(mFingerprintUtils).getBiometricsForUser(mContext, 2);
+
+        mClient.start(mCallback);
+        mClient.onEnumerationResult(null, 0);
+        verify(mFingerprintUtils).deleteStateForUser(2);
+    }
+
     protected FingerprintInternalCleanupClient createClient() {
         final Map<Integer, Long> authenticatorIds = new HashMap<>();
         return new FingerprintInternalCleanupClient(mContext, () -> mAidlSession, 2 /* userId */,
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 87b52e6..f98bbf9 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -28,6 +28,7 @@
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE;
 import static com.android.server.hdmi.HdmiControlService.STANDBY_SCREEN_OFF;
 import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
+import static com.android.server.hdmi.RequestActiveSourceAction.TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -1792,7 +1793,7 @@
         mTestLooper.dispatchAll();
 
         // Skip the LauncherX API timeout.
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+        mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
         mTestLooper.dispatchAll();
 
         assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
@@ -1825,7 +1826,7 @@
         mTestLooper.dispatchAll();
 
         // Skip the LauncherX API timeout.
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+        mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
         mTestLooper.dispatchAll();
 
         assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
@@ -1861,7 +1862,7 @@
         mTestLooper.dispatchAll();
 
         // Skip the LauncherX API timeout.
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+        mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
         mTestLooper.dispatchAll();
 
         assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
@@ -1904,7 +1905,7 @@
         mTestLooper.dispatchAll();
 
         // Skip the LauncherX API timeout.
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+        mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
         mTestLooper.dispatchAll();
 
         assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
@@ -1941,7 +1942,7 @@
         mHdmiControlService.sendCecCommand(setStreamPathFromTv);
 
         // Skip the LauncherX API timeout.
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2);
+        mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS);
         mTestLooper.dispatchAll();
 
         assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestActiveSource);
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
deleted file mode 100644
index 874eec7..0000000
--- a/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.power.hint"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
-      ]
-    }
-  ]
-}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index e5c42082..fb82b872c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -888,7 +888,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
         service.addApprovedList("a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -919,7 +919,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
         service.addApprovedList("a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -950,7 +950,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
         service.addApprovedList("a/a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -981,7 +981,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
         service.addApprovedList("a/a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -1211,6 +1211,64 @@
     }
 
     @Test
+    public void testUpgradeAppNoIntentFilterNoRebind() throws Exception {
+        Context context = spy(getContext());
+        doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
+
+        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles,
+                mIpm, APPROVAL_BY_COMPONENT);
+
+        List<String> packages = new ArrayList<>();
+        packages.add("package");
+        addExpectedServices(service, packages, 0);
+
+        final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1");
+        final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2");
+
+        // Both components are approved initially
+        mExpectedPrimaryComponentNames.clear();
+        mExpectedPrimaryPackages.clear();
+        mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+        mExpectedSecondaryComponentNames.clear();
+        mExpectedSecondaryPackages.clear();
+
+        loadXml(service);
+
+        //Component package/C1 loses serviceInterface intent filter
+        ManagedServices.Config config = service.getConfig();
+        when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt())).
+            thenAnswer(new Answer<List<ResolveInfo>>() {
+                @Override
+                public List<ResolveInfo> answer(InvocationOnMock invocationOnMock)
+                    throws Throwable {
+                    Object[] args = invocationOnMock.getArguments();
+                    Intent invocationIntent = (Intent) args[0];
+                    if (invocationIntent != null) {
+                        if (invocationIntent.getAction().equals(config.serviceInterface)
+                            && packages.contains(invocationIntent.getPackage())) {
+                            List<ResolveInfo> dummyServices = new ArrayList<>();
+                            ResolveInfo resolveInfo = new ResolveInfo();
+                            ServiceInfo serviceInfo = new ServiceInfo();
+                            serviceInfo.packageName = invocationIntent.getPackage();
+                            serviceInfo.name = approvedComponent.getClassName();
+                            serviceInfo.permission = service.getConfig().bindPermission;
+                            resolveInfo.serviceInfo = serviceInfo;
+                            dummyServices.add(resolveInfo);
+                            return dummyServices;
+                        }
+                    }
+                    return new ArrayList<>();
+                }
+            });
+
+        // Trigger package update
+        service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
+
+        assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent));
+        assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent));
+    }
+
+    @Test
     public void testSetPackageOrComponentEnabled() throws Exception {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
             ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
@@ -1915,7 +1973,7 @@
         metaDataAutobindAllow.putBoolean(META_DATA_DEFAULT_AUTOBIND, true);
         metaDatas.put(cn_allowed, metaDataAutobindAllow);
 
-        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+        mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
 
         service.addApprovedList(cn_allowed.flattenToString(), 0, true);
         service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
@@ -1960,7 +2018,7 @@
         metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
         metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
 
-        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+        mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
 
         service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
 
@@ -1999,7 +2057,7 @@
         metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
         metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
 
-        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+        mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
 
         service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
 
@@ -2070,8 +2128,8 @@
     }
 
     private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
-            ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
-            throws RemoteException {
+            ManagedServices service, PackageManager packageManager,
+            ArrayMap<ComponentName, Bundle> metaDatas) throws RemoteException {
         when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
                 (Answer<ServiceInfo>) invocation -> {
                     ComponentName invocationCn = invocation.getArgument(0);
@@ -2086,6 +2144,39 @@
                     return null;
                 }
         );
+
+        // add components to queryIntentServicesAsUser response
+        final List<String> packages = new ArrayList<>();
+        for (ComponentName cn: componentNames) {
+            packages.add(cn.getPackageName());
+        }
+        ManagedServices.Config config = service.getConfig();
+        when(packageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt())).
+                thenAnswer(new Answer<List<ResolveInfo>>() {
+                @Override
+                public List<ResolveInfo> answer(InvocationOnMock invocationOnMock)
+                    throws Throwable {
+                    Object[] args = invocationOnMock.getArguments();
+                    Intent invocationIntent = (Intent) args[0];
+                    if (invocationIntent != null) {
+                        if (invocationIntent.getAction().equals(config.serviceInterface)
+                            && packages.contains(invocationIntent.getPackage())) {
+                            List<ResolveInfo> dummyServices = new ArrayList<>();
+                            for (ComponentName cn: componentNames) {
+                                ResolveInfo resolveInfo = new ResolveInfo();
+                                ServiceInfo serviceInfo = new ServiceInfo();
+                                serviceInfo.packageName = invocationIntent.getPackage();
+                                serviceInfo.name = cn.getClassName();
+                                serviceInfo.permission = service.getConfig().bindPermission;
+                                resolveInfo.serviceInfo = serviceInfo;
+                                dummyServices.add(resolveInfo);
+                            }
+                            return dummyServices;
+                        }
+                    }
+                    return new ArrayList<>();
+                }
+            });
     }
 
     private void resetComponentsAndPackages() {
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 c1f5a01..1b999e4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -3105,6 +3105,29 @@
     }
 
     @Test
+    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public void testLifetimeExtendedCancelledOnClick() throws Exception {
+        // Adds a lifetime extended notification.
+        final NotificationRecord notif = generateNotificationRecord(mTestNotificationChannel, 1,
+                null, false);
+        notif.getSbn().getNotification().flags =
+                Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        mService.addNotification(notif);
+        // Verify that the notification is posted and active.
+        assertThat(mBinderService.getActiveNotifications(mPkg).length).isEqualTo(1);
+
+        // Click the notification.
+        final NotificationVisibility nv = NotificationVisibility.obtain(notif.getKey(), 1, 2, true);
+        mService.mNotificationDelegate.onNotificationClick(mUid, Binder.getCallingPid(),
+                notif.getKey(), nv);
+        waitForIdle();
+
+        // The notification has been cancelled.
+        StatusBarNotification[] notifs = mBinderService.getActiveNotifications(mPkg);
+        assertThat(notifs.length).isEqualTo(0);
+    }
+
+    @Test
     public void testCancelNotificationWithTag_fromApp_cannotCancelFgsChild()
             throws Exception {
         when(mAmi.applyForegroundServiceNotification(
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
index 901c036..4f75931 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
@@ -20,7 +20,7 @@
 import static android.os.VibrationAttributes.CATEGORY_UNKNOWN;
 import static android.os.VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY;
 import static android.os.VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
-import static android.os.VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
 import static android.os.VibrationAttributes.USAGE_TOUCH;
 import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK;
 import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK;
@@ -346,7 +346,7 @@
     }
 
     @Test
-    public void testVibrationAttribute_keyboardCategoryOff_isIme_notUseKeyboardCategory() {
+    public void testVibrationAttribute_keyboardCategoryOff_isIme_useTouchUsage() {
         mSetFlagsRule.disableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
@@ -362,7 +362,7 @@
     }
 
     @Test
-    public void testVibrationAttribute_keyboardCategoryOn_notIme_notUseKeyboardCategory() {
+    public void testVibrationAttribute_keyboardCategoryOn_notIme_useTouchUsage() {
         mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
@@ -377,7 +377,7 @@
     }
 
     @Test
-    public void testVibrationAttribute_keyboardCategoryOn_isIme_useKeyboardCategory() {
+    public void testVibrationAttribute_keyboardCategoryOn_isIme_useImeFeedbackUsage() {
         mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
@@ -385,64 +385,14 @@
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
                     effectId, /* flags */ 0,
                     HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
-            assertWithMessage("Expected USAGE_TOUCH for effect " + effectId)
-                    .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH);
+            assertWithMessage("Expected USAGE_IME_FEEDBACK for effect " + effectId)
+                    .that(attrs.getUsage()).isEqualTo(USAGE_IME_FEEDBACK);
             assertWithMessage("Expected CATEGORY_KEYBOARD for effect " + effectId)
                     .that(attrs.getCategory()).isEqualTo(CATEGORY_KEYBOARD);
         }
     }
 
     @Test
-    public void testVibrationAttribute_noFixAmplitude_notBypassIntensityScale() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
-        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK);
-        mockKeyboardVibrationFixedAmplitude(-1);
-        HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
-
-        for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
-            VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* flags */ 0,
-                    HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
-            assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
-                    + effectId)
-                    .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse();
-        }
-    }
-
-    @Test
-    public void testVibrationAttribute_notIme_notBypassIntensityScale() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
-        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK);
-        mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE);
-        HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
-
-        for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
-            VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* flags */ 0, /* privFlags */ 0);
-            assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
-                    + effectId)
-                    .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse();
-        }
-    }
-
-    @Test
-    public void testVibrationAttribute_fixAmplitude_isIme_bypassIntensityScale() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
-        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK);
-        mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE);
-        HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
-
-        for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
-            VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* flags */ 0,
-                    HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
-            assertWithMessage("Expected FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
-                    + effectId)
-                    .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isTrue();
-        }
-    }
-
-    @Test
     public void testIsRestricted_biometricConstants_returnsTrue() {
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
index 60d8964..8d4a6aa 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
@@ -23,6 +23,7 @@
 import static android.os.VibrationAttributes.USAGE_ALARM;
 import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
 import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
 import static android.os.VibrationAttributes.USAGE_MEDIA;
 import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
 import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
@@ -893,6 +894,22 @@
     }
 
     @Test
+    public void getCurrentIntensity_ImeFeedbackValueReflectsToKeyboardVibrationSettings() {
+        setDefaultIntensity(USAGE_IME_FEEDBACK, VIBRATION_INTENSITY_MEDIUM);
+        setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH);
+
+        setKeyboardVibrationSettingsSupported(false);
+        mVibrationSettings.update();
+        assertEquals(VIBRATION_INTENSITY_HIGH,
+                mVibrationSettings.getCurrentIntensity(USAGE_IME_FEEDBACK));
+
+        setKeyboardVibrationSettingsSupported(true);
+        mVibrationSettings.update();
+        assertEquals(VIBRATION_INTENSITY_MEDIUM,
+                mVibrationSettings.getCurrentIntensity(USAGE_IME_FEEDBACK));
+    }
+
+    @Test
     public void getFallbackEffect_returnsEffectsFromSettings() {
         assertNotNull(mVibrationSettings.getFallbackEffect(VibrationEffect.EFFECT_TICK));
         assertNotNull(mVibrationSettings.getFallbackEffect(VibrationEffect.EFFECT_TEXTURE_TICK));
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index bea6917..e411a17 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -154,6 +154,9 @@
     private static final VibrationAttributes RINGTONE_ATTRS =
             new VibrationAttributes.Builder().setUsage(
                     VibrationAttributes.USAGE_RINGTONE).build();
+    private static final VibrationAttributes IME_FEEDBACK_ATTRS =
+            new VibrationAttributes.Builder().setUsage(
+                    VibrationAttributes.USAGE_IME_FEEDBACK).build();
     private static final VibrationAttributes UNKNOWN_ATTRS =
             new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_UNKNOWN).build();
 
@@ -853,6 +856,7 @@
         vibrate(service, VibrationEffect.createOneShot(2000, 200),
                 new VibrationAttributes.Builder().setUsage(
                         VibrationAttributes.USAGE_UNKNOWN).build());
+        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), IME_FEEDBACK_ATTRS);
 
         InOrder inOrderVerifier = inOrder(mAppOpsManagerMock);
         inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
@@ -868,6 +872,8 @@
                 anyInt(), anyString());
         inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
                 eq(AudioAttributes.USAGE_UNKNOWN), anyInt(), anyString());
+        inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
+                eq(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION), anyInt(), anyString());
     }
 
     @Test
@@ -1684,40 +1690,6 @@
     }
 
     @Test
-    public void vibrate_withBypassScaleFlag_ignoresIntensitySettingsAndResolvesAmplitude()
-            throws Exception {
-        // Permission needed for bypassing user settings
-        grantPermission(android.Manifest.permission.MODIFY_PHONE_STATE);
-
-        int defaultTouchIntensity =
-                mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_TOUCH);
-        // This will scale down touch vibrations.
-        setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY,
-                defaultTouchIntensity > Vibrator.VIBRATION_INTENSITY_LOW
-                        ? defaultTouchIntensity - 1
-                        : defaultTouchIntensity);
-
-        int defaultAmplitude = mContextSpy.getResources().getInteger(
-                com.android.internal.R.integer.config_defaultVibrationAmplitude);
-
-        mockVibrators(1);
-        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
-        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
-        VibratorManagerService service = createSystemReadyService();
-
-        vibrateAndWaitUntilFinished(service,
-                VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE),
-                new VibrationAttributes.Builder()
-                        .setUsage(VibrationAttributes.USAGE_TOUCH)
-                        .setFlags(VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)
-                        .build());
-
-        assertEquals(1, fakeVibrator.getAllEffectSegments().size());
-
-        assertEquals(defaultAmplitude / 255f, fakeVibrator.getAmplitudes().get(0), 1e-5);
-    }
-
-    @Test
     @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
     public void vibrate_withAdaptiveHaptics_appliesCorrectAdaptiveScales() throws Exception {
         // Keep user settings the same as device default so only adaptive scale is applied.
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index faaa80f..ea825c7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -18,10 +18,6 @@
 
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
@@ -119,7 +115,6 @@
 
 import android.app.ActivityOptions;
 import android.app.AppOpsManager;
-import android.app.ICompatCameraControlCallback;
 import android.app.PictureInPictureParams;
 import android.app.servertransaction.ActivityConfigurationChangeItem;
 import android.app.servertransaction.ClientTransaction;
@@ -3483,178 +3478,6 @@
         assertFalse(app.mActivityRecord.isSurfaceShowing());
     }
 
-    @Test
-    public void testUpdateCameraCompatState_flagIsEnabled_controlStateIsUpdated() {
-        final ActivityRecord activity = createActivityWithTask();
-        // Mock a flag being enabled.
-        doReturn(true).when(activity).isCameraCompatControlEnabled();
-
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ false, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ true, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-
-        activity.updateCameraCompatState(/* showControl */ false,
-                /* transformationApplied */ false, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN);
-
-        activity.updateCameraCompatState(/* showControl */ false,
-                /* transformationApplied */ true, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN);
-    }
-
-    @Test
-    public void testUpdateCameraCompatState_flagIsDisabled_controlStateIsHidden() {
-        final ActivityRecord activity = createActivityWithTask();
-        // Mock a flag being disabled.
-        doReturn(false).when(activity).isCameraCompatControlEnabled();
-
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ false, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN);
-
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ true, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN);
-    }
-
-    @Test
-    public void testUpdateCameraCompatStateFromUser_clickedOnDismiss() throws RemoteException {
-        final ActivityRecord activity = createActivityWithTask();
-        // Mock a flag being enabled.
-        doReturn(true).when(activity).isCameraCompatControlEnabled();
-
-        ICompatCameraControlCallback callback = getCompatCameraControlCallback();
-        spyOn(callback);
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ false, callback);
-
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-
-        // Clicking on the button.
-        activity.updateCameraCompatStateFromUser(CAMERA_COMPAT_CONTROL_DISMISSED);
-
-        verify(callback, never()).revertCameraCompatTreatment();
-        verify(callback, never()).applyCameraCompatTreatment();
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_DISMISSED);
-
-        // All following updates are ignored.
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ false, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_DISMISSED);
-
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ true, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_DISMISSED);
-
-        activity.updateCameraCompatState(/* showControl */ false,
-                /* transformationApplied */ true, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_DISMISSED);
-    }
-
-    @Test
-    public void testUpdateCameraCompatStateFromUser_clickedOnApplyTreatment()
-            throws RemoteException {
-        final ActivityRecord activity = createActivityWithTask();
-        // Mock a flag being enabled.
-        doReturn(true).when(activity).isCameraCompatControlEnabled();
-
-        ICompatCameraControlCallback callback = getCompatCameraControlCallback();
-        spyOn(callback);
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ false, callback);
-
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-
-        // Clicking on the button.
-        activity.updateCameraCompatStateFromUser(CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-
-        verify(callback, never()).revertCameraCompatTreatment();
-        verify(callback).applyCameraCompatTreatment();
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-
-        // Request from the client to show the control are ignored respecting the user choice.
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ false, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-
-        // Request from the client to hide the control is respected.
-        activity.updateCameraCompatState(/* showControl */ false,
-                /* transformationApplied */ true, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN);
-
-        // Request from the client to show the control again is respected.
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ false, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-    }
-
-    @Test
-    public void testUpdateCameraCompatStateFromUser_clickedOnRevertTreatment()
-            throws RemoteException {
-        final ActivityRecord activity = createActivityWithTask();
-        // Mock a flag being enabled.
-        doReturn(true).when(activity).isCameraCompatControlEnabled();
-
-        ICompatCameraControlCallback callback = getCompatCameraControlCallback();
-        spyOn(callback);
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ true, callback);
-
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-
-        // Clicking on the button.
-        activity.updateCameraCompatStateFromUser(CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-
-        verify(callback).revertCameraCompatTreatment();
-        verify(callback, never()).applyCameraCompatTreatment();
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-
-        // Request from the client to show the control are ignored respecting the user choice.
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ true, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-
-        // Request from the client to hide the control is respected.
-        activity.updateCameraCompatState(/* showControl */ false,
-                /* transformationApplied */ true, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(), CAMERA_COMPAT_CONTROL_HIDDEN);
-
-        // Request from the client to show the control again is respected.
-        activity.updateCameraCompatState(/* showControl */ true,
-                /* transformationApplied */ true, /* callback */ null);
-
-        assertEquals(activity.getCameraCompatControlState(),
-                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-    }
-
     @Test // b/162542125
     public void testInputDispatchTimeout() throws RemoteException {
         final ActivityRecord activity = createActivityWithTask();
@@ -3818,16 +3641,6 @@
         assertTrue(appWindow.mResizeReported);
     }
 
-    private ICompatCameraControlCallback getCompatCameraControlCallback() {
-        return new ICompatCameraControlCallback.Stub() {
-            @Override
-            public void applyCameraCompatTreatment() {}
-
-            @Override
-            public void revertCameraCompatTreatment() {}
-        };
-    }
-
     private void assertHasStartingWindow(ActivityRecord atoken) {
         assertNotNull(atoken.mStartingSurface);
         assertNotNull(atoken.mStartingData);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index ff1c6c8..d0080d2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -102,6 +102,7 @@
 import android.service.voice.IVoiceInteractionSession;
 import android.util.Pair;
 import android.util.Size;
+import android.view.Display;
 import android.view.Gravity;
 import android.view.RemoteAnimationAdapter;
 import android.window.TaskFragmentOrganizerToken;
@@ -941,6 +942,91 @@
                 notNull() /* options */);
     }
 
+
+    /**
+     * This test ensures that activity launch on a secondary display is allowed if the activity did
+     * not opt out from showing on remote devices.
+     */
+    @Test
+    public void testStartActivityOnVirtualDisplay() {
+        final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
+                false /* mockGetRootTask */);
+        starter.mRequest.activityInfo.flags |= ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
+        // Create a virtual display at bottom.
+        final TestDisplayContent secondaryDisplay =
+                new TestDisplayContent.Builder(mAtm, 1000, 1500)
+                        .setType(Display.TYPE_VIRTUAL)
+                        .setPosition(POSITION_BOTTOM).build();
+        final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea();
+        final Task stack = secondaryTaskContainer.createRootTask(
+                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+        // Create an activity record on the top of secondary display.
+        final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack);
+
+        // Put an activity on default display as the top focused activity.
+        new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        // Start activity with the same intent as {@code topActivityOnSecondaryDisplay}
+        // on secondary display.
+        final ActivityOptions options = ActivityOptions.makeBasic()
+                .setLaunchDisplayId(secondaryDisplay.mDisplayId);
+        final int result = starter.setReason("testStartActivityOnVirtualDisplay")
+                .setIntent(topActivityOnSecondaryDisplay.intent)
+                .setActivityOptions(options.toBundle())
+                .execute();
+
+        // Ensure result is delivering intent to top.
+        assertEquals(START_DELIVERED_TO_TOP, result);
+
+        // Ensure secondary display only creates one stack.
+        verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean());
+    }
+
+    /**
+     * This test ensures that activity launch on a secondary display is disallowed if the activity
+     * opted out from showing on remote devices.
+     */
+    @EnableFlags(android.companion.virtualdevice.flags.Flags
+            .FLAG_ENFORCE_REMOTE_DEVICE_OPT_OUT_ON_ALL_VIRTUAL_DISPLAYS)
+    @Test
+    public void testStartOptedOutActivityOnVirtualDisplay() {
+        final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
+                false /* mockGetRootTask */);
+        starter.mRequest.activityInfo.flags &= ~ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
+        // Create a virtual display at bottom.
+        final TestDisplayContent secondaryDisplay =
+                new TestDisplayContent.Builder(mAtm, 1000, 1500)
+                        .setType(Display.TYPE_VIRTUAL)
+                        .setPosition(POSITION_BOTTOM).build();
+        final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea();
+        final Task stack = secondaryTaskContainer.createRootTask(
+                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+        // Create an activity record on the top of secondary display.
+        final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack);
+
+        // Put an activity on default display as the top focused activity.
+        new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        // Start activity with the same intent as {@code topActivityOnSecondaryDisplay}
+        // on secondary display.
+        final ActivityOptions options = ActivityOptions.makeBasic()
+                .setLaunchDisplayId(secondaryDisplay.mDisplayId);
+        final int result = starter.setReason("testStartOptedOutActivityOnVirtualDisplay")
+                .setIntent(topActivityOnSecondaryDisplay.intent)
+                .setActivityOptions(options.toBundle())
+                .execute();
+
+        // Ensure result is canceled.
+        assertEquals(START_CANCELED, result);
+
+        // Ensure secondary display only creates one stack.
+        verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean());
+    }
+
     @Test
     public void testWasVisibleInRestartAttempt() {
         final ActivityStarter starter = prepareStarter(
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
index ddd6d56..a6fd112 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
@@ -26,6 +26,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 
 import android.compat.testing.PlatformCompatChangeRule;
 import android.platform.test.annotations.Presubmit;
@@ -246,7 +247,6 @@
         });
     }
 
-
     @Test
     @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
     public void testshouldOverrideMinAspectRatio_propertyFalse_overrideEnabled_returnsFalse() {
@@ -269,6 +269,24 @@
         });
     }
 
+    @Test
+    public void testGetFixedOrientationLetterboxAspectRatio_splitScreenAspectEnabled() {
+        runTestScenario((robot)-> {
+            robot.applyOnConf((c) -> {
+                c.enableCameraCompatTreatment(/* enabled */ true);
+                c.enableCameraCompatTreatmentAtBuildTime(/* enabled */ true);
+                c.enableCameraCompatSplitScreenAspectRatio(/* enabled */ true);
+                c.enableDisplayAspectRatioEnabledForFixedOrientationLetterbox(/* enabled */ false);
+                c.setFixedOrientationLetterboxAspectRatio(/* aspectRatio */ 1.5f);
+            });
+            robot.activity().createActivityWithComponentInNewTaskAndDisplay();
+            robot.checkFixedOrientationLetterboxAspectRatioForTopParent(/* expected */ 1.5f);
+
+            robot.activity().enableTreatmentForTopActivity(/* enabled */ true);
+            robot.checkAspectRatioForTopParentIsSplitScreenRatio(/* expected */ true);
+        });
+    }
+
     /**
      * Runs a test scenario providing a Robot.
      */
@@ -308,6 +326,28 @@
         }
 
         @NonNull
+        void checkFixedOrientationLetterboxAspectRatioForTopParent(float expected) {
+            assertEquals(expected,
+                    getTopActivityAppCompatAspectRatioOverrides()
+                            .getFixedOrientationLetterboxAspectRatio(
+                                    activity().top().getParent().getConfiguration()),
+                                        FLOAT_TOLLERANCE);
+        }
+
+        void checkAspectRatioForTopParentIsSplitScreenRatio(boolean expected) {
+            final AppCompatAspectRatioOverrides aspectRatioOverrides =
+                    getTopActivityAppCompatAspectRatioOverrides();
+            if (expected) {
+                assertEquals(aspectRatioOverrides.getSplitScreenAspectRatio(),
+                        aspectRatioOverrides.getFixedOrientationLetterboxAspectRatio(
+                                activity().top().getParent().getConfiguration()), FLOAT_TOLLERANCE);
+            } else {
+                assertNotEquals(aspectRatioOverrides.getSplitScreenAspectRatio(),
+                        aspectRatioOverrides.getFixedOrientationLetterboxAspectRatio(
+                                activity().top().getParent().getConfiguration()), FLOAT_TOLLERANCE);
+            }
+        }
+
         private AppCompatAspectRatioOverrides getTopActivityAppCompatAspectRatioOverrides() {
             return activity().top().mAppCompatController.getAppCompatAspectRatioOverrides();
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
index 00a8771..6592f26 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
@@ -70,4 +70,14 @@
     void enableCompatFakeFocus(boolean enabled) {
         doReturn(enabled).when(mAppCompatConfiguration).isCompatFakeFocusEnabled();
     }
+
+    void enableDisplayAspectRatioEnabledForFixedOrientationLetterbox(boolean enabled) {
+        doReturn(enabled).when(mAppCompatConfiguration)
+                .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
+    }
+
+    void setFixedOrientationLetterboxAspectRatio(float aspectRatio) {
+        doReturn(aspectRatio).when(mAppCompatConfiguration)
+                .getFixedOrientationLetterboxAspectRatio();
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
index 92f246b..6939f97 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
@@ -28,6 +28,8 @@
     private static final int DEFAULT_DISPLAY_WIDTH = 1000;
     private static final int DEFAULT_DISPLAY_HEIGHT = 2000;
 
+    static final float FLOAT_TOLLERANCE = 0.01f;
+
     @NonNull
     private final AppCompatActivityRobot mActivityRobot;
     @NonNull
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index afa22bc..a159ce3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -72,7 +72,6 @@
 import android.window.WindowOnBackInvokedDispatcher;
 
 import com.android.server.LocalServices;
-import com.android.window.flags.Flags;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -672,7 +671,6 @@
 
     @Test
     public void testBackOnMostRecentWindowInActivityEmbedding() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG);
         final Task task = createTask(mDefaultDisplay);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TaskFragment primaryTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java
index 366e519..6e48818 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java
@@ -16,6 +16,9 @@
 
 package com.android.server.wm;
 
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
+
 import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_ALLOWLISTED_COMPONENT;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_FOREGROUND;
@@ -23,7 +26,6 @@
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_SAW_PERMISSION;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -58,7 +60,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.quality.Strictness;
 
 import java.lang.reflect.Field;
@@ -134,9 +135,8 @@
 
     ActivityOptions mCheckedOptions = ActivityOptions.makeBasic()
             .setPendingIntentCreatorBackgroundActivityStartMode(
-                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
-            .setPendingIntentBackgroundActivityStartMode(
-                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+                    MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+            .setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
 
     class TestableBackgroundActivityStartController extends BackgroundActivityStartController {
         private Set<Pair<Integer, Integer>> mBalPermissionUidPidPairs = new HashSet<>();
@@ -175,7 +175,6 @@
         when(mService.getAppOpsManager()).thenReturn(mAppOpsManager);
         setViaReflection(mService, "mProcessMap", mProcessMap);
 
-        //Mockito.when(mSupervisor.getBackgroundActivityLaunchController()).thenReturn(mController);
         setViaReflection(mSupervisor, "mRecentTasks", mRecentTasks);
 
         mController = new TestableBackgroundActivityStartController(mService, mSupervisor);
@@ -397,7 +396,7 @@
 
         // setup state
         WindowProcessControllerMap mProcessMap = new WindowProcessControllerMap();
-        WindowProcessController otherProcess = Mockito.mock(WindowProcessController.class);
+        WindowProcessController otherProcess = mock(WindowProcessController.class);
         mProcessMap.put(callingPid, mCallerApp);
         mProcessMap.put(REGULAR_PID_1_1, otherProcess);
         setViaReflection(mService, "mProcessMap", mProcessMap);
@@ -516,14 +515,13 @@
         BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE;
         Intent intent = TEST_INTENT;
         ActivityOptions checkedOptions = mCheckedOptions;
-        checkedOptions.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+        checkedOptions.setPendingIntentBackgroundActivityStartMode(
+                MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
         BackgroundActivityStartController.BalState balState = mController.new BalState(callingUid,
                 callingPid, callingPackage, realCallingUid, realCallingPid, null,
                 originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent,
                 checkedOptions);
 
-        assertThat(balState.isPendingIntentBalAllowedByPermission()).isTrue();
-
         // call
         BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedBySender(
                 balState);
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
index f110c69..e364264 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
@@ -547,7 +547,7 @@
         assertThat(balState.callerExplicitOptInOrOut()).isFalse();
         assertThat(balState.realCallerExplicitOptInOrAutoOptIn()).isTrue();
         assertThat(balState.realCallerExplicitOptInOrOut()).isFalse();
-        assertThat(balState.toString()).contains(
+        assertThat(balState.toString()).startsWith(
                 "[callingPackage: package.app1; "
                         + "callingPackageTargetSdk: -1; "
                         + "callingUid: 10001; "
@@ -563,6 +563,7 @@
                         + "balAllowedByPiCreator: BSP.ALLOW_BAL; "
                         + "balAllowedByPiCreatorWithHardening: BSP.ALLOW_BAL; "
                         + "resultIfPiCreatorAllowsBal: null; "
+                        + "callerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; "
                         + "hasRealCaller: true; "
                         + "isCallForResult: false; "
                         + "isPendingIntent: false; "
@@ -646,7 +647,7 @@
         assertThat(balState.callerExplicitOptInOrOut()).isFalse();
         assertThat(balState.realCallerExplicitOptInOrAutoOptIn()).isFalse();
         assertThat(balState.realCallerExplicitOptInOrOut()).isFalse();
-        assertThat(balState.toString()).contains(
+        assertThat(balState.toString()).startsWith(
                 "[callingPackage: package.app1; "
                         + "callingPackageTargetSdk: -1; "
                         + "callingUid: 10001; "
@@ -662,6 +663,7 @@
                         + "balAllowedByPiCreator: BSP.NONE; "
                         + "balAllowedByPiCreatorWithHardening: BSP.NONE; "
                         + "resultIfPiCreatorAllowsBal: null; "
+                        + "callerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; "
                         + "hasRealCaller: true; "
                         + "isCallForResult: false; "
                         + "isPendingIntent: true; "
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
index 57118f2..f843386 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
@@ -63,11 +63,6 @@
 
     private final Message mScreenUnblocker = mock(Message.class);
 
-    @Override
-    protected void onBeforeSystemServicesCreated() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_DEFER_DISPLAY_UPDATES);
-    }
-
     @Before
     public void before() {
         doReturn(true).when(mDisplayContent).getLastHasContent();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
index 3fcf304..2e0d4d4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
@@ -24,12 +24,15 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.testng.Assert.assertFalse;
 
 import android.annotation.Nullable;
@@ -58,6 +61,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
 import java.util.function.Consumer;
 
 /**
@@ -352,20 +356,58 @@
     }
 
     @Test
-    public void testRemovesStaleDisplaySettings() {
+    public void testRemovesStaleDisplaySettings_defaultDisplay_removesStaleDisplaySettings() {
         assumeTrue(com.android.window.flags.Flags.perUserDisplayWindowSettings());
 
-        final DisplayWindowSettingsProvider provider =
-                new DisplayWindowSettingsProvider(mDefaultVendorSettingsStorage,
-                        mOverrideSettingsStorage);
-        final DisplayInfo displayInfo = mSecondaryDisplay.getDisplayInfo();
-        updateOverrideSettings(provider, displayInfo, settings -> settings.mForcedDensity = 356);
+        // Write density setting for second display then remove it.
+        final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
+                mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
+        final DisplayInfo secDisplayInfo = mSecondaryDisplay.getDisplayInfo();
+        updateOverrideSettings(provider, secDisplayInfo, setting -> setting.mForcedDensity = 356);
         mRootWindowContainer.removeChild(mSecondaryDisplay);
 
-        provider.removeStaleDisplaySettings(mRootWindowContainer);
+        // Write density setting for inner and outer default display.
+        final DisplayInfo innerDisplayInfo = mPrimaryDisplay.getDisplayInfo();
+        final DisplayInfo outerDisplayInfo = new DisplayInfo(secDisplayInfo);
+        outerDisplayInfo.displayId = mPrimaryDisplay.mDisplayId;
+        outerDisplayInfo.uniqueId = "TEST_OUTER_DISPLAY_" + System.currentTimeMillis();
+        updateOverrideSettings(provider, innerDisplayInfo, setting -> setting.mForcedDensity = 490);
+        updateOverrideSettings(provider, outerDisplayInfo, setting -> setting.mForcedDensity = 420);
+        final List<DisplayInfo> possibleDisplayInfos = List.of(innerDisplayInfo, outerDisplayInfo);
+        doReturn(possibleDisplayInfos)
+                .when(mWm).getPossibleDisplayInfoLocked(eq(innerDisplayInfo.displayId));
+
+        provider.removeStaleDisplaySettingsLocked(mWm, mRootWindowContainer);
 
         assertThat(mOverrideSettingsStorage.wasWriteSuccessful()).isTrue();
-        assertThat(provider.getOverrideSettingsSize()).isEqualTo(0);
+        assertThat(provider.getOverrideSettingsSize()).isEqualTo(2);
+        assertThat(provider.getOverrideSettings(innerDisplayInfo).mForcedDensity).isEqualTo(490);
+        assertThat(provider.getOverrideSettings(outerDisplayInfo).mForcedDensity).isEqualTo(420);
+    }
+
+    @Test
+    public void testRemovesStaleDisplaySettings_displayNotInLayout_keepsDisplaySettings() {
+        assumeTrue(com.android.window.flags.Flags.perUserDisplayWindowSettings());
+
+        // Write density setting for primary display.
+        final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
+                mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
+        final DisplayInfo primDisplayInfo = mPrimaryDisplay.getDisplayInfo();
+        updateOverrideSettings(provider, primDisplayInfo, setting -> setting.mForcedDensity = 420);
+
+        // Add a virtual display and write density setting for it.
+        final DisplayInfo virtDisplayInfo = new DisplayInfo(primDisplayInfo);
+        virtDisplayInfo.uniqueId = "TEST_VIRTUAL_DISPLAY_" + System.currentTimeMillis();
+        createNewDisplay(virtDisplayInfo);
+        waitUntilHandlersIdle();  // Wait until unfrozen after a display is added.
+        updateOverrideSettings(provider, virtDisplayInfo, setting -> setting.mForcedDensity = 490);
+
+        provider.removeStaleDisplaySettingsLocked(mWm, mRootWindowContainer);
+
+        assertThat(mOverrideSettingsStorage.wasWriteSuccessful()).isTrue();
+        assertThat(provider.getOverrideSettingsSize()).isEqualTo(2);
+        assertThat(provider.getOverrideSettings(primDisplayInfo).mForcedDensity).isEqualTo(420);
+        assertThat(provider.getOverrideSettings(virtDisplayInfo).mForcedDensity).isEqualTo(490);
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index 1072ef0..4a9d5c7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -156,7 +156,6 @@
         window.openInputChannel(channel);
         window.mHasSurface = true;
         mWm.mWindowMap.put(window.mClient.asBinder(), window);
-        mWm.mInputToWindowMap.put(window.mInputChannelToken, window);
         return window;
     }
 
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 e2c0f6c2..61a6f31 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -19,7 +19,6 @@
 import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
@@ -297,37 +296,6 @@
     }
 
     @Test
-    public void testgetFixedOrientationLetterboxAspectRatio_splitScreenAspectEnabled() {
-        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
-                .isCameraCompatTreatmentEnabled();
-        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
-                .isCameraCompatTreatmentEnabledAtBuildTime();
-        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
-                .isCameraCompatSplitScreenAspectRatioEnabled();
-        doReturn(false).when(mActivity.mWmService.mAppCompatConfiguration)
-                .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
-        doReturn(1.5f).when(mActivity.mWmService.mAppCompatConfiguration)
-                .getFixedOrientationLetterboxAspectRatio();
-
-        // Recreate DisplayContent with DisplayRotationCompatPolicy
-        mActivity = setUpActivityWithComponent();
-        mController = new LetterboxUiController(mWm, mActivity);
-
-        assertEquals(1.5f, mController.getFixedOrientationLetterboxAspectRatio(
-                mActivity.getParent().getConfiguration()), /* delta */ 0.01);
-
-        spyOn(mDisplayContent.mAppCompatCameraPolicy);
-        doReturn(true).when(mDisplayContent.mAppCompatCameraPolicy)
-                .isTreatmentEnabledForActivity(eq(mActivity));
-
-        final AppCompatAspectRatioOverrides aspectRatioOverrides =
-                mActivity.mAppCompatController.getAppCompatAspectRatioOverrides();
-        assertEquals(aspectRatioOverrides.getSplitScreenAspectRatio(),
-                aspectRatioOverrides.getFixedOrientationLetterboxAspectRatio(
-                        mActivity.getParent().getConfiguration()), /* delta */  0.01);
-    }
-
-    @Test
     public void testIsVerticalThinLetterboxed() {
         // Vertical thin letterbox disabled
         doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration)
diff --git a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
deleted file mode 100644
index 78509db..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static com.android.internal.R.bool.config_unfoldTransitionEnabled;
-import static com.android.server.wm.DeviceStateController.DeviceState.REAR;
-import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.OPEN;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.when;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Tests for the {@link WindowToken} class.
- *
- * Build/Install/Run:
- * atest WmTests:PhysicalDisplaySwitchTransitionLauncherTest
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase {
-
-    @Mock
-    Context mContext;
-    @Mock
-    Resources mResources;
-    @Mock
-    BLASTSyncEngine mSyncEngine;
-
-    WindowTestsBase.TestTransitionPlayer mPlayer;
-    TransitionController mTransitionController;
-    DisplayContent mDisplayContent;
-
-    private PhysicalDisplaySwitchTransitionLauncher mTarget;
-    private float mOriginalAnimationScale;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mTransitionController = new WindowTestsBase.TestTransitionController(mAtm);
-        mTransitionController.setSyncEngine(mSyncEngine);
-        mPlayer = new WindowTestsBase.TestTransitionPlayer(
-                mTransitionController, mAtm.mWindowOrganizerController);
-        when(mContext.getResources()).thenReturn(mResources);
-        mDisplayContent = new TestDisplayContent.Builder(mAtm, 100, 150).build();
-        mTarget = new PhysicalDisplaySwitchTransitionLauncher(mDisplayContent, mAtm, mContext,
-                mTransitionController);
-        mOriginalAnimationScale = ValueAnimator.getDurationScale();
-    }
-
-    @After
-    public void after() {
-        ValueAnimator.setDurationScale(mOriginalAnimationScale);
-    }
-
-    @Test
-    public void testDisplaySwitchAfterUnfoldToOpen_animationsEnabled_requestsTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        final Rect origBounds = new Rect();
-        mDisplayContent.getBounds(origBounds);
-        origBounds.offsetTo(0, 0);
-        mTarget.requestDisplaySwitchTransitionIfNeeded(
-                mDisplayContent.getDisplayId(),
-                origBounds.width(),
-                origBounds.height(),
-                /* newDisplayWidth= */ 200,
-                /* newDisplayHeight= */ 250
-        );
-
-        assertNotNull(mPlayer.mLastRequest);
-        assertEquals(mDisplayContent.getDisplayId(),
-                mPlayer.mLastRequest.getDisplayChange().getDisplayId());
-        assertEquals(origBounds, mPlayer.mLastRequest.getDisplayChange().getStartAbsBounds());
-        assertEquals(new Rect(0, 0, 200, 250),
-                mPlayer.mLastRequest.getDisplayChange().getEndAbsBounds());
-    }
-
-    @Test
-    public void testDisplaySwitchAfterFolding_animationEnabled_doesNotRequestTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(OPEN);
-
-        mTarget.foldStateChanged(FOLDED);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitchAfterUnfoldingToHalf_animationEnabled_requestsTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(HALF_FOLDED);
-        requestDisplaySwitch();
-
-        assertTransitionRequested();
-    }
-
-    @Test
-    public void testDisplaySwitchSecondTimeAfterUnfolding_animationEnabled_noTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-        mPlayer.mLastRequest = null;
-
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-
-    @Test
-    public void testDisplaySwitchAfterGoingToRearAndBack_animationEnabled_noTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(OPEN);
-
-        mTarget.foldStateChanged(REAR);
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitchAfterUnfoldingAndFolding_animationEnabled_noTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-        mTarget.foldStateChanged(OPEN);
-        // No request display switch event (simulate very fast fold after unfold, even before
-        // the displays switched)
-        mTarget.foldStateChanged(FOLDED);
-
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitch_whenShellTransitionsNotEnabled_noTransition() {
-        givenAllAnimationsEnabled();
-        givenShellTransitionsEnabled(false);
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitch_whenAnimationsDisabled_noTransition() {
-        givenAllAnimationsEnabled();
-        givenAnimationsEnabled(false);
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitch_whenUnfoldAnimationDisabled_noTransition() {
-        givenAllAnimationsEnabled();
-        givenUnfoldTransitionEnabled(false);
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitchAfterUnfolding_otherCollectingTransition_collectsDisplaySwitch() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        // Collects to the current transition
-        assertTrue(mTransitionController.getCollectingTransition().mParticipants.contains(
-                mDisplayContent));
-    }
-
-
-    @Test
-    public void testDisplaySwitch_whenNoContentInDisplayContent_noTransition() {
-        givenAllAnimationsEnabled();
-        givenDisplayContentHasContent(false);
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    private void assertTransitionRequested() {
-        assertNotNull(mPlayer.mLastRequest);
-    }
-
-    private void assertTransitionNotRequested() {
-        assertNull(mPlayer.mLastRequest);
-    }
-
-    private void requestDisplaySwitch() {
-        mTarget.requestDisplaySwitchTransitionIfNeeded(
-                mDisplayContent.getDisplayId(),
-                mDisplayContent.getBounds().width(),
-                mDisplayContent.getBounds().height(),
-                /* newDisplayWidth= */ 200,
-                /* newDisplayHeight= */ 250
-        );
-    }
-
-    private void givenAllAnimationsEnabled() {
-        givenAnimationsEnabled(true);
-        givenUnfoldTransitionEnabled(true);
-        givenShellTransitionsEnabled(true);
-        givenDisplayContentHasContent(true);
-    }
-
-    private void givenUnfoldTransitionEnabled(boolean enabled) {
-        when(mResources.getBoolean(config_unfoldTransitionEnabled)).thenReturn(enabled);
-    }
-
-    private void givenAnimationsEnabled(boolean enabled) {
-        ValueAnimator.setDurationScale(enabled ? 1.0f : 0.0f);
-    }
-
-    private void givenShellTransitionsEnabled(boolean enabled) {
-        if (enabled) {
-            mTransitionController.registerTransitionPlayer(mPlayer, null /* proc */);
-        } else {
-            mTransitionController.unregisterTransitionPlayer(mPlayer);
-        }
-    }
-
-    private void givenDisplayContentHasContent(boolean hasContent) {
-        when(mDisplayContent.getLastHasContent()).thenReturn(hasContent);
-    }
-}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 3c247a0..6be1af2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -995,16 +995,14 @@
         // The focus should change.
         assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
 
-        if (Flags.embeddedActivityBackNavFlag()) {
-            // Move focus if the adjacent activity is more recently active.
-            doReturn(1L).when(appLeftTop).getLastWindowCreateTime();
-            doReturn(2L).when(appRightTop).getLastWindowCreateTime();
-            assertTrue(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
+        // Move focus if the adjacent activity is more recently active.
+        doReturn(1L).when(appLeftTop).getLastWindowCreateTime();
+        doReturn(2L).when(appRightTop).getLastWindowCreateTime();
+        assertTrue(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
 
-            // Do not move the focus if the adjacent activity is less recently active.
-            doReturn(3L).when(appLeftTop).getLastWindowCreateTime();
-            assertFalse(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
-        }
+        // Do not move the focus if the adjacent activity is less recently active.
+        doReturn(3L).when(appLeftTop).getLastWindowCreateTime();
+        assertFalse(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
     }
 
     @Test
diff --git a/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt
index c1a86b3..015e188 100644
--- a/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt
+++ b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt
@@ -18,12 +18,20 @@
 
 import android.view.InputDevice.SOURCE_MOUSE
 import android.view.InputDevice.SOURCE_TOUCHSCREEN
+import android.view.InputDevice.SOURCE_STYLUS
+import android.view.InputDevice.SOURCE_TOUCHPAD
+
 import android.view.InputEventAssigner
 import android.view.KeyEvent
 import android.view.MotionEvent
 import org.junit.Assert.assertEquals
 import org.junit.Test
 
+sealed class StreamEvent
+private data object Vsync : StreamEvent()
+data class MotionEventData(val action: Int, val source: Int, val id: Int, val expectedId: Int) :
+    StreamEvent()
+
 /**
  * Create a MotionEvent with the provided action, eventTime, and source
  */
@@ -49,64 +57,164 @@
     return KeyEvent(eventTime, eventTime, action, code, repeat)
 }
 
+/**
+ * Check that the correct eventIds are assigned in a stream. The stream consists of motion
+ * events or vsync (processed frame)
+ * Each streamEvent should have unique ids when writing tests
+ * The test passes even if two events get assigned the same eventId, since the mapping is
+ * streamEventId -> motionEventId and streamEvents have unique ids
+ */
+private fun checkEventStream(vararg streamEvents: StreamEvent) {
+    val assigner = InputEventAssigner()
+    var eventTime = 10L
+    // Maps MotionEventData.id to MotionEvent.id
+    // We can't control the event id of the generated motion events but for testing it's easier
+    // to label the events with a custom id for readability
+    val eventIdMap: HashMap<Int, Int> = HashMap()
+    for (streamEvent in streamEvents) {
+        when (streamEvent) {
+            is MotionEventData -> {
+                val event = createMotionEvent(streamEvent.action, eventTime, streamEvent.source)
+                eventIdMap[streamEvent.id] = event.id
+                val eventId = assigner.processEvent(event)
+                assertEquals(eventIdMap[streamEvent.expectedId], eventId)
+            }
+            is Vsync -> assigner.notifyFrameProcessed()
+        }
+        eventTime += 1
+    }
+}
+
 class InputEventAssignerTest {
     companion object {
         private const val TAG = "InputEventAssignerTest"
     }
 
     /**
-     * A single MOVE event should be assigned to the next available frame.
+     * A single event should be assigned to the next available frame.
      */
     @Test
-    fun testTouchGesture() {
-        val assigner = InputEventAssigner()
-        val event = createMotionEvent(MotionEvent.ACTION_MOVE, 10, SOURCE_TOUCHSCREEN)
-        val eventId = assigner.processEvent(event)
-        assertEquals(event.id, eventId)
+    fun testTouchMove() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN, id = 1, expectedId = 1)
+        )
+    }
+
+    @Test
+    fun testMouseMove() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_MOUSE, id = 1, expectedId = 1)
+        )
+    }
+
+    @Test
+    fun testMouseScroll() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 1, expectedId = 1)
+        )
+    }
+
+    @Test
+    fun testStylusMove() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_STYLUS, id = 1, expectedId = 1)
+        )
+    }
+
+    @Test
+    fun testStylusHover() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_HOVER_MOVE, SOURCE_STYLUS, id = 1, expectedId = 1)
+        )
+    }
+
+    @Test
+    fun testTouchpadMove() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_STYLUS, id = 1, expectedId = 1)
+        )
     }
 
     /**
-     * DOWN event should be used until a vsync comes in. After vsync, the latest event should be
-     * produced.
+     * Test that before a VSYNC the event id generated by input event assigner for move events is
+     * the id of the down event. Move events coming after a VSYNC should be assigned their own event
+     * id
      */
-    @Test
-    fun testTouchDownWithMove() {
-        val assigner = InputEventAssigner()
-        val down = createMotionEvent(MotionEvent.ACTION_DOWN, 10, SOURCE_TOUCHSCREEN)
-        val move1 = createMotionEvent(MotionEvent.ACTION_MOVE, 12, SOURCE_TOUCHSCREEN)
-        val move2 = createMotionEvent(MotionEvent.ACTION_MOVE, 13, SOURCE_TOUCHSCREEN)
-        val move3 = createMotionEvent(MotionEvent.ACTION_MOVE, 14, SOURCE_TOUCHSCREEN)
-        val move4 = createMotionEvent(MotionEvent.ACTION_MOVE, 15, SOURCE_TOUCHSCREEN)
-        var eventId = assigner.processEvent(down)
-        assertEquals(down.id, eventId)
-        eventId = assigner.processEvent(move1)
-        assertEquals(down.id, eventId)
-        eventId = assigner.processEvent(move2)
-        // Even though we already had 2 move events, there was no choreographer callback yet.
-        // Therefore, we should still get the id of the down event
-        assertEquals(down.id, eventId)
+    private fun testDownAndMove(source: Int) {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_DOWN, source, id = 1, expectedId = 1),
+            MotionEventData(MotionEvent.ACTION_MOVE, source, id = 2, expectedId = 1),
+            Vsync,
+            MotionEventData(MotionEvent.ACTION_MOVE, source, id = 4, expectedId = 4)
+        )
+    }
 
-        // Now send CALLBACK_INPUT to the assigner. It should provide the latest motion event
-        assigner.notifyFrameProcessed()
-        eventId = assigner.processEvent(move3)
-        assertEquals(move3.id, eventId)
-        eventId = assigner.processEvent(move4)
-        assertEquals(move4.id, eventId)
+    @Test
+    fun testTouchDownAndMove() {
+        testDownAndMove(SOURCE_TOUCHSCREEN)
+    }
+
+    @Test
+    fun testMouseDownAndMove() {
+        testDownAndMove(SOURCE_MOUSE)
+    }
+
+    @Test
+    fun testStylusDownAndMove() {
+        testDownAndMove(SOURCE_STYLUS)
+    }
+
+    @Test
+    fun testTouchpadDownAndMove() {
+        testDownAndMove(SOURCE_TOUCHPAD)
     }
 
     /**
-     * Similar to the above test, but with SOURCE_MOUSE. Since we don't have down latency
-     * concept for non-touchscreens, the latest input event will be used.
+     * After an up event, motion events should be assigned their own event id
      */
     @Test
-    fun testMouseDownWithMove() {
-        val assigner = InputEventAssigner()
-        val down = createMotionEvent(MotionEvent.ACTION_DOWN, 10, SOURCE_MOUSE)
-        val move1 = createMotionEvent(MotionEvent.ACTION_MOVE, 12, SOURCE_MOUSE)
-        var eventId = assigner.processEvent(down)
-        assertEquals(down.id, eventId)
-        eventId = assigner.processEvent(move1)
-        assertEquals(move1.id, eventId)
+    fun testMouseDownUpAndScroll() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_MOUSE, id = 1, expectedId = 1),
+            MotionEventData(MotionEvent.ACTION_UP, SOURCE_MOUSE, id = 2, expectedId = 2),
+            MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3)
+        )
+    }
+
+    /**
+     * After an up event, motion events should be assigned their own event id
+     */
+    @Test
+    fun testStylusDownUpAndHover() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_STYLUS, id = 1, expectedId = 1),
+            MotionEventData(MotionEvent.ACTION_UP, SOURCE_STYLUS, id = 2, expectedId = 2),
+            MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3)
+        )
+    }
+
+    /**
+     * After a cancel event, motion events should be assigned their own event id
+     */
+    @Test
+    fun testMouseDownCancelAndScroll() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_MOUSE, id = 1, expectedId = 1),
+            MotionEventData(MotionEvent.ACTION_CANCEL, SOURCE_MOUSE, id = 2, expectedId = 2),
+            MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3)
+        )
+    }
+
+    /**
+     * After a cancel event, motion events should be assigned their own event id
+     */
+    @Test
+    fun testStylusDownCancelAndHover() {
+        checkEventStream(
+            MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_STYLUS, id = 1, expectedId = 1),
+            MotionEventData(MotionEvent.ACTION_CANCEL, SOURCE_STYLUS, id = 2, expectedId = 2),
+            MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3)
+        )
     }
 
     /**