Merge "AudioService: handle SCO audio activaton for regular VoIP apps" into main
diff --git a/api/gen_combined_removed_dex.sh b/api/gen_combined_removed_dex.sh
index e0153f7..2860e2e 100755
--- a/api/gen_combined_removed_dex.sh
+++ b/api/gen_combined_removed_dex.sh
@@ -6,6 +6,6 @@
 
 # Convert each removed.txt to the "dex format" equivalent, and print all output.
 for f in "$@"; do
-    "$metalava_path" signature-to-dex "$f" "${tmp_dir}/tmp"
+    "$metalava_path" signature-to-dex "$f" --out "${tmp_dir}/tmp"
     cat "${tmp_dir}/tmp"
 done
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index a9c07d1..6f5bb4a 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -126,7 +126,7 @@
     description: "Talkback focus doesn't move to the 'If you change your Google Account picture…' after swiping next to move the focus from 'Choose a picture'"
     bug: "330835921"
     metadata {
-    	purpose: PURPOSE_BUGFIX
+        purpose: PURPOSE_BUGFIX
   }
 }
 
@@ -136,7 +136,7 @@
     description: "Talkback doesn't announce 'selected' after double tapping the button in the picture list in 'Choose a picture' page."
     bug: "330840549"
     metadata {
-    	purpose: PURPOSE_BUGFIX
+        purpose: PURPOSE_BUGFIX
   }
 }
 
@@ -146,10 +146,21 @@
     description: "Fix potential unexpected behavior due to concurrent file writing"
     bug: "339351031"
     metadata {
-    	purpose: PURPOSE_BUGFIX
+        purpose: PURPOSE_BUGFIX
   }
 }
 
+flag {
+    name: "cache_user_serial_number"
+    namespace: "multiuser"
+    description: "Optimise user serial number retrieval"
+    bug: "340018451"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+  }
+}
+
+
 # This flag guards the private space feature and all its implementations excluding the APIs. APIs are guarded by android.os.Flags.allow_private_profile.
 flag {
     name: "enable_private_space_features"
diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java
index 5031faa..7b18117 100644
--- a/core/java/android/content/res/ColorStateList.java
+++ b/core/java/android/content/res/ColorStateList.java
@@ -32,6 +32,9 @@
 import android.util.SparseArray;
 import android.util.StateSet;
 import android.util.Xml;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
 
 import com.android.internal.R;
 import com.android.internal.graphics.ColorUtils;
@@ -44,7 +47,9 @@
 
 import java.io.IOException;
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 /**
  *
@@ -793,4 +798,61 @@
             return new ColorStateList(stateSpecs, colors);
         }
     };
+
+    /** @hide */
+    public void writeToProto(ProtoOutputStream out) {
+        for (int[] states : mStateSpecs) {
+            long specToken = out.start(ColorStateListProto.STATE_SPECS);
+            for (int state : states) {
+                out.write(ColorStateListProto.StateSpec.STATE, state);
+            }
+            out.end(specToken);
+        }
+        for (int color : mColors) {
+            out.write(ColorStateListProto.COLORS, color);
+        }
+    }
+
+    /** @hide */
+    public static ColorStateList createFromProto(ProtoInputStream in)
+            throws Exception {
+        List<int[]> stateSpecs = new ArrayList<>();
+        List<Integer> colors = new ArrayList<>();
+
+
+        while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            switch (in.getFieldNumber()) {
+                case (int) ColorStateListProto.COLORS:
+                    colors.add(in.readInt(ColorStateListProto.COLORS));
+                    break;
+                case (int) ColorStateListProto.STATE_SPECS:
+                    final long stateToken = in.start(ColorStateListProto.STATE_SPECS);
+                    List<Integer> states = new ArrayList<>();
+                    while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                        switch (in.getFieldNumber()) {
+                            case (int) ColorStateListProto.StateSpec.STATE:
+                                states.add(in.readInt(ColorStateListProto.StateSpec.STATE));
+                                break;
+                            default:
+                                Log.w(TAG, "Unhandled field while reading Icon proto!\n"
+                                        + ProtoUtils.currentFieldToString(in));
+                        }
+                    }
+                    int[] statesArray = new int[states.size()];
+                    Arrays.setAll(statesArray, states::get);
+                    stateSpecs.add(statesArray);
+                    in.end(stateToken);
+                    break;
+                default:
+                    Log.w(TAG, "Unhandled field while reading Icon proto!\n"
+                            + ProtoUtils.currentFieldToString(in));
+            }
+        }
+
+        int[][] stateSpecsArray = new int[stateSpecs.size()][];
+        Arrays.setAll(stateSpecsArray, stateSpecs::get);
+        int[] colorsArray = new int[colors.size()];
+        Arrays.setAll(colorsArray, colors::get);
+        return new ColorStateList(stateSpecsArray, colorsArray);
+    }
 }
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index c2424e8..cbac912 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -32,6 +32,7 @@
 import android.app.AppOpsManager;
 import android.companion.virtual.VirtualDeviceManager;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource.ScopedParcelState;
 import android.content.Context;
 import android.graphics.ImageFormat;
 import android.graphics.Point;
@@ -45,6 +46,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Parcel;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -293,10 +295,14 @@
     @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
     @TestApi
     public static int getNumberOfCameras(@NonNull Context context) {
-        return _getNumberOfCameras(context.getDeviceId(), getDevicePolicyFromContext(context));
+        try (ScopedParcelState clientAttribution =
+                context.getAttributionSource().asScopedParcelState()) {
+            return _getNumberOfCameras(
+                    clientAttribution.getParcel(), getDevicePolicyFromContext(context));
+        }
     }
 
-    private static native int _getNumberOfCameras(int deviceId, int devicePolicy);
+    private static native int _getNumberOfCameras(Parcel clientAttributionParcel, int devicePolicy);
 
     /**
      * Returns the information about a particular camera.
@@ -321,8 +327,16 @@
     @TestApi
     public static void getCameraInfo(int cameraId, @NonNull Context context,
             int rotationOverride, CameraInfo cameraInfo) {
-        _getCameraInfo(cameraId, rotationOverride, context.getDeviceId(),
-                getDevicePolicyFromContext(context), cameraInfo);
+        try (ScopedParcelState clientAttribution =
+                context.getAttributionSource().asScopedParcelState()) {
+            _getCameraInfo(
+                    cameraId,
+                    rotationOverride,
+                    clientAttribution.getParcel(),
+                    getDevicePolicyFromContext(context),
+                    cameraInfo);
+        }
+
         IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
         IAudioService audioService = IAudioService.Stub.asInterface(b);
         try {
@@ -336,8 +350,12 @@
         }
     }
 
-    private native static void _getCameraInfo(int cameraId, int rotationOverride,
-            int deviceId, int devicePolicy, CameraInfo cameraInfo);
+    private native static void _getCameraInfo(
+            int cameraId,
+            int rotationOverride,
+            Parcel clientAttributionParcel,
+            int devicePolicy,
+            CameraInfo cameraInfo);
 
     private static int getDevicePolicyFromContext(Context context) {
         if (context.getDeviceId() == DEVICE_ID_DEFAULT
@@ -545,9 +563,18 @@
         }
 
         boolean forceSlowJpegMode = shouldForceSlowJpegMode();
-        return native_setup(new WeakReference<>(this), cameraId,
-                ActivityThread.currentOpPackageName(), rotationOverride, forceSlowJpegMode,
-                context.getDeviceId(), getDevicePolicyFromContext(context));
+
+        try (ScopedParcelState clientAttribution =
+                context.getAttributionSource().asScopedParcelState()) {
+            return native_setup(
+                    new WeakReference<>(this),
+                    cameraId,
+                    ActivityThread.currentOpPackageName(),
+                    rotationOverride,
+                    forceSlowJpegMode,
+                    clientAttribution.getParcel(),
+                    getDevicePolicyFromContext(context));
+        }
     }
 
     private boolean shouldForceSlowJpegMode() {
@@ -630,8 +657,14 @@
     }
 
     @UnsupportedAppUsage
-    private native int native_setup(Object cameraThis, int cameraId, String packageName,
-            int rotationOverride, boolean forceSlowJpegMode, int deviceId, int devicePolicy);
+    private native int native_setup(
+            Object cameraThis,
+            int cameraId,
+            String packageName,
+            int rotationOverride,
+            boolean forceSlowJpegMode,
+            Parcel clientAttributionParcel,
+            int devicePolicy);
 
     private native final void native_release();
 
@@ -2267,9 +2300,11 @@
         private static final String KEY_MIN_EXPOSURE_COMPENSATION = "min-exposure-compensation";
         private static final String KEY_EXPOSURE_COMPENSATION_STEP = "exposure-compensation-step";
         private static final String KEY_AUTO_EXPOSURE_LOCK = "auto-exposure-lock";
-        private static final String KEY_AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported";
+        private static final String KEY_AUTO_EXPOSURE_LOCK_SUPPORTED =
+                "auto-exposure-lock-supported";
         private static final String KEY_AUTO_WHITEBALANCE_LOCK = "auto-whitebalance-lock";
-        private static final String KEY_AUTO_WHITEBALANCE_LOCK_SUPPORTED = "auto-whitebalance-lock-supported";
+        private static final String KEY_AUTO_WHITEBALANCE_LOCK_SUPPORTED =
+                "auto-whitebalance-lock-supported";
         private static final String KEY_METERING_AREAS = "metering-areas";
         private static final String KEY_MAX_NUM_METERING_AREAS = "max-num-metering-areas";
         private static final String KEY_ZOOM = "zoom";
@@ -2286,7 +2321,8 @@
         private static final String KEY_RECORDING_HINT = "recording-hint";
         private static final String KEY_VIDEO_SNAPSHOT_SUPPORTED = "video-snapshot-supported";
         private static final String KEY_VIDEO_STABILIZATION = "video-stabilization";
-        private static final String KEY_VIDEO_STABILIZATION_SUPPORTED = "video-stabilization-supported";
+        private static final String KEY_VIDEO_STABILIZATION_SUPPORTED =
+                "video-stabilization-supported";
 
         // Parameter key suffix for supported values.
         private static final String SUPPORTED_VALUES_SUFFIX = "-values";
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 9eb9745..2dbd4b8 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -35,6 +35,7 @@
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.Overridable;
+import android.content.AttributionSourceState;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.graphics.Point;
@@ -106,6 +107,7 @@
     private final boolean DEBUG = false;
 
     private static final int USE_CALLING_UID = -1;
+    private static final int USE_CALLING_PID = -1;
 
     @SuppressWarnings("unused")
     private static final int API_VERSION_1 = 1;
@@ -418,9 +420,12 @@
     public boolean isConcurrentSessionConfigurationSupported(
             @NonNull Map<String, SessionConfiguration> cameraIdAndSessionConfig)
             throws CameraAccessException {
-        return CameraManagerGlobal.get().isConcurrentSessionConfigurationSupported(
-                cameraIdAndSessionConfig, mContext.getApplicationInfo().targetSdkVersion,
-                mContext.getDeviceId(), getDevicePolicyFromContext(mContext));
+        return CameraManagerGlobal.get()
+                .isConcurrentSessionConfigurationSupported(
+                        cameraIdAndSessionConfig,
+                        mContext.getApplicationInfo().targetSdkVersion,
+                        getClientAttribution(),
+                        getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -660,11 +665,14 @@
         }
         try {
             for (String physicalCameraId : physicalCameraIds) {
+                AttributionSourceState clientAttribution = getClientAttribution();
+                clientAttribution.deviceId = DEVICE_ID_DEFAULT;
                 CameraMetadataNative physicalCameraInfo =
-                        cameraService.getCameraCharacteristics(physicalCameraId,
+                        cameraService.getCameraCharacteristics(
+                                physicalCameraId,
                                 mContext.getApplicationInfo().targetSdkVersion,
                                 /*rotationOverride*/ ICameraService.ROTATION_OVERRIDE_NONE,
-                                DEVICE_ID_DEFAULT,
+                                clientAttribution,
                                 DEVICE_POLICY_DEFAULT);
                 StreamConfiguration[] configs = physicalCameraInfo.get(
                         CameraCharacteristics.
@@ -756,9 +764,13 @@
                         "Camera service is currently unavailable");
             }
             try {
-                CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId,
-                        mContext.getApplicationInfo().targetSdkVersion, rotationOverride,
-                        mContext.getDeviceId(), getDevicePolicyFromContext(mContext));
+                CameraMetadataNative info =
+                        cameraService.getCameraCharacteristics(
+                                cameraId,
+                                mContext.getApplicationInfo().targetSdkVersion,
+                                rotationOverride,
+                                getClientAttribution(),
+                                getDevicePolicyFromContext(mContext));
                 characteristics = prepareCameraCharacteristics(cameraId, info, cameraService);
             } catch (ServiceSpecificException e) {
                 throw ExceptionUtils.throwAsPublicException(e);
@@ -951,15 +963,33 @@
     }
 
     /**
+     * Constructs an AttributionSourceState with only the uid, pid, and deviceId fields set
+     *
+     * <p>This method is a temporary stopgap in the transition to using AttributionSource. Currently
+     * AttributionSourceState is only used as a vehicle for passing deviceId, uid, and pid
+     * arguments.</p>
+     *
+     * @hide
+     */
+    public AttributionSourceState getClientAttribution() {
+        // TODO: Send the full contextAttribution over aidl, remove USE_CALLING_*
+        AttributionSourceState contextAttribution =
+                mContext.getAttributionSource().asState();
+        AttributionSourceState clientAttribution =
+                new AttributionSourceState();
+        clientAttribution.uid = USE_CALLING_UID;
+        clientAttribution.pid = USE_CALLING_PID;
+        clientAttribution.deviceId = contextAttribution.deviceId;
+        clientAttribution.next = new AttributionSourceState[0];
+        return clientAttribution;
+    }
+
+    /**
      * Helper for opening a connection to a camera with the given ID.
      *
      * @param cameraId The unique identifier of the camera device to open
      * @param callback The callback for the camera. Must not be null.
      * @param executor The executor to invoke the callback with. Must not be null.
-     * @param uid      The UID of the application actually opening the camera.
-     *                 Must be USE_CALLING_UID unless the caller is a service
-     *                 that is trusted to open the device on behalf of an
-     *                 application and to forward the real UID.
      *
      * @throws CameraAccessException if the camera is disabled by device policy,
      * too many camera devices are already open, or the cameraId does not match
@@ -974,7 +1004,7 @@
      * @see android.app.admin.DevicePolicyManager#setCameraDisabled
      */
     private CameraDevice openCameraDeviceUserAsync(String cameraId,
-            CameraDevice.StateCallback callback, Executor executor, final int uid,
+            CameraDevice.StateCallback callback, Executor executor,
             final int oomScoreOffset, int rotationOverride) throws CameraAccessException {
         CameraCharacteristics characteristics = getCameraCharacteristics(cameraId);
         CameraDevice device = null;
@@ -1005,11 +1035,19 @@
                         "Camera service is currently unavailable");
                 }
 
-                cameraUser = cameraService.connectDevice(callbacks, cameraId,
-                    mContext.getOpPackageName(), mContext.getAttributionTag(), uid,
-                    oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion,
-                        rotationOverride, mContext.getDeviceId(),
-                        getDevicePolicyFromContext(mContext));
+                AttributionSourceState clientAttribution =
+                        getClientAttribution();
+                cameraUser =
+                        cameraService.connectDevice(
+                                callbacks,
+                                cameraId,
+                                mContext.getOpPackageName(),
+                                mContext.getAttributionTag(),
+                                oomScoreOffset,
+                                mContext.getApplicationInfo().targetSdkVersion,
+                                rotationOverride,
+                                clientAttribution,
+                                getDevicePolicyFromContext(mContext));
             } catch (ServiceSpecificException e) {
                 if (e.errorCode == ICameraService.ERROR_DEPRECATED_HAL) {
                     throw new AssertionError("Should've gone down the shim path");
@@ -1135,8 +1173,8 @@
     public void openCamera(@NonNull String cameraId,
             @NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)
             throws CameraAccessException {
-        openCameraForUid(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler),
-                USE_CALLING_UID);
+
+        openCameraImpl(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler));
     }
 
     /**
@@ -1172,8 +1210,8 @@
     public void openCamera(@NonNull String cameraId, boolean overrideToPortrait,
             @Nullable Handler handler,
             @NonNull final CameraDevice.StateCallback callback) throws CameraAccessException {
-        openCameraForUid(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler),
-                         USE_CALLING_UID, /*oomScoreOffset*/0,
+        openCameraImpl(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler),
+                         /*oomScoreOffset*/0,
                          overrideToPortrait
                                  ? ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT
                                  : ICameraService.ROTATION_OVERRIDE_NONE);
@@ -1221,7 +1259,7 @@
         if (executor == null) {
             throw new IllegalArgumentException("executor was null");
         }
-        openCameraForUid(cameraId, callback, executor, USE_CALLING_UID);
+        openCameraImpl(cameraId, callback, executor);
     }
 
     /**
@@ -1289,13 +1327,13 @@
             throw new IllegalArgumentException(
                     "oomScoreOffset < 0, cannot increase priority of camera client");
         }
-        openCameraForUid(cameraId, callback, executor, USE_CALLING_UID, oomScoreOffset,
+        openCameraImpl(cameraId, callback, executor, oomScoreOffset,
                 getRotationOverride(mContext));
     }
 
     /**
-     * Open a connection to a camera with the given ID, on behalf of another application
-     * specified by clientUid. Also specify the minimum oom score and process state the application
+     * Open a connection to a camera with the given ID, on behalf of another application.
+     * Also specify the minimum oom score and process state the application
      * should have, as seen by the cameraserver.
      *
      * <p>The behavior of this method matches that of {@link #openCamera}, except that it allows
@@ -1303,9 +1341,6 @@
      * done by services trusted by the camera subsystem to act on behalf of applications and
      * to forward the real UID.</p>
      *
-     * @param clientUid
-     *             The UID of the application on whose behalf the camera is being opened.
-     *             Must be USE_CALLING_UID unless the caller is a trusted service.
      * @param oomScoreOffset
      *             The minimum oom score that cameraservice must see for this client.
      * @param rotationOverride
@@ -1313,9 +1348,9 @@
      *             that should be followed for this camera id connection
      * @hide
      */
-    public void openCameraForUid(@NonNull String cameraId,
+    public void openCameraImpl(@NonNull String cameraId,
             @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor,
-            int clientUid, int oomScoreOffset, int rotationOverride)
+            int oomScoreOffset, int rotationOverride)
             throws CameraAccessException {
 
         if (cameraId == null) {
@@ -1327,29 +1362,24 @@
             throw new IllegalArgumentException("No cameras available on device");
         }
 
-        openCameraDeviceUserAsync(cameraId, callback, executor, clientUid, oomScoreOffset,
+        openCameraDeviceUserAsync(cameraId, callback, executor, oomScoreOffset,
                 rotationOverride);
     }
 
     /**
-     * Open a connection to a camera with the given ID, on behalf of another application
-     * specified by clientUid.
+     * Open a connection to a camera with the given ID, on behalf of another application.
      *
      * <p>The behavior of this method matches that of {@link #openCamera}, except that it allows
      * the caller to specify the UID to use for permission/etc verification. This can only be
      * done by services trusted by the camera subsystem to act on behalf of applications and
      * to forward the real UID.</p>
      *
-     * @param clientUid
-     *             The UID of the application on whose behalf the camera is being opened.
-     *             Must be USE_CALLING_UID unless the caller is a trusted service.
-     *
      * @hide
      */
-    public void openCameraForUid(@NonNull String cameraId,
-            @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor,
-            int clientUid) throws CameraAccessException {
-        openCameraForUid(cameraId, callback, executor, clientUid, /*oomScoreOffset*/0,
+    public void openCameraImpl(@NonNull String cameraId,
+            @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor)
+            throws CameraAccessException {
+        openCameraImpl(cameraId, callback, executor, /*oomScoreOffset*/0,
                 getRotationOverride(mContext));
     }
 
@@ -1397,8 +1427,12 @@
         if (CameraManagerGlobal.sCameraServiceDisabled) {
             throw new IllegalArgumentException("No cameras available on device");
         }
-        CameraManagerGlobal.get().setTorchMode(cameraId, enabled, mContext.getDeviceId(),
-                getDevicePolicyFromContext(mContext));
+        CameraManagerGlobal.get()
+                .setTorchMode(
+                        cameraId,
+                        enabled,
+                        getClientAttribution(),
+                        getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -1461,8 +1495,12 @@
         if (CameraManagerGlobal.sCameraServiceDisabled) {
             throw new IllegalArgumentException("No camera available on device");
         }
-        CameraManagerGlobal.get().turnOnTorchWithStrengthLevel(cameraId, torchStrength,
-                mContext.getDeviceId(), getDevicePolicyFromContext(mContext));
+        CameraManagerGlobal.get()
+                .turnOnTorchWithStrengthLevel(
+                        cameraId,
+                        torchStrength,
+                        getClientAttribution(),
+                        getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -1488,8 +1526,11 @@
         if (CameraManagerGlobal.sCameraServiceDisabled) {
             throw new IllegalArgumentException("No camera available on device.");
         }
-        return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId, mContext.getDeviceId(),
-                getDevicePolicyFromContext(mContext));
+        return CameraManagerGlobal.get()
+                .getTorchStrengthLevel(
+                        cameraId,
+                        getClientAttribution(),
+                        getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -2499,7 +2540,9 @@
 
         public boolean isConcurrentSessionConfigurationSupported(
                 @NonNull Map<String, SessionConfiguration> cameraIdsAndSessionConfigurations,
-                int targetSdkVersion, int deviceId, int devicePolicy)
+                int targetSdkVersion,
+                AttributionSourceState clientAttribution,
+                int devicePolicy)
                 throws CameraAccessException {
             if (cameraIdsAndSessionConfigurations == null) {
                 throw new IllegalArgumentException("cameraIdsAndSessionConfigurations was null");
@@ -2517,9 +2560,12 @@
                 for (Set<DeviceCameraInfo> combination : mConcurrentCameraIdCombinations) {
                     Set<DeviceCameraInfo> infos = new ArraySet<>();
                     for (String cameraId : cameraIdsAndSessionConfigurations.keySet()) {
-                        infos.add(new DeviceCameraInfo(cameraId,
-                                devicePolicy == DEVICE_POLICY_DEFAULT
-                                        ? DEVICE_ID_DEFAULT : deviceId));
+                        infos.add(
+                                new DeviceCameraInfo(
+                                        cameraId,
+                                        devicePolicy == DEVICE_POLICY_DEFAULT
+                                                ? DEVICE_ID_DEFAULT
+                                                : clientAttribution.deviceId));
                     }
                     if (combination.containsAll(infos)) {
                         subsetFound = true;
@@ -2541,7 +2587,7 @@
                 }
                 try {
                     return mCameraService.isConcurrentSessionConfigurationSupported(
-                            cameraIdsAndConfigs, targetSdkVersion, deviceId, devicePolicy);
+                            cameraIdsAndConfigs, targetSdkVersion, clientAttribution, devicePolicy);
                 } catch (ServiceSpecificException e) {
                     throw ExceptionUtils.throwAsPublicException(e);
                 } catch (RemoteException e) {
@@ -2580,7 +2626,11 @@
             return false;
         }
 
-        public void setTorchMode(String cameraId, boolean enabled, int deviceId, int devicePolicy)
+        public void setTorchMode(
+                String cameraId,
+                boolean enabled,
+                AttributionSourceState clientAttribution,
+                int devicePolicy)
                 throws CameraAccessException {
             synchronized (mLock) {
                 if (cameraId == null) {
@@ -2594,8 +2644,8 @@
                 }
 
                 try {
-                    cameraService.setTorchMode(cameraId, enabled, mTorchClientBinder, deviceId,
-                            devicePolicy);
+                    cameraService.setTorchMode(
+                            cameraId, enabled, mTorchClientBinder, clientAttribution, devicePolicy);
                 } catch(ServiceSpecificException e) {
                     throw ExceptionUtils.throwAsPublicException(e);
                 } catch (RemoteException e) {
@@ -2605,7 +2655,10 @@
             }
         }
 
-        public void turnOnTorchWithStrengthLevel(String cameraId, int torchStrength, int deviceId,
+        public void turnOnTorchWithStrengthLevel(
+                String cameraId,
+                int torchStrength,
+                AttributionSourceState clientAttribution,
                 int devicePolicy)
                 throws CameraAccessException {
             synchronized (mLock) {
@@ -2620,8 +2673,12 @@
                 }
 
                 try {
-                    cameraService.turnOnTorchWithStrengthLevel(cameraId, torchStrength,
-                            mTorchClientBinder, deviceId, devicePolicy);
+                    cameraService.turnOnTorchWithStrengthLevel(
+                            cameraId,
+                            torchStrength,
+                            mTorchClientBinder,
+                            clientAttribution,
+                            devicePolicy);
                 } catch(ServiceSpecificException e) {
                     throw ExceptionUtils.throwAsPublicException(e);
                 } catch (RemoteException e) {
@@ -2631,7 +2688,8 @@
             }
         }
 
-        public int getTorchStrengthLevel(String cameraId, int deviceId, int devicePolicy)
+        public int getTorchStrengthLevel(
+                String cameraId, AttributionSourceState clientAttribution, int devicePolicy)
                 throws CameraAccessException {
             int torchStrength;
             synchronized (mLock) {
@@ -2646,8 +2704,9 @@
                 }
 
                 try {
-                    torchStrength = cameraService.getTorchStrengthLevel(cameraId, deviceId,
-                            devicePolicy);
+                    torchStrength =
+                            cameraService.getTorchStrengthLevel(
+                                    cameraId, clientAttribution, devicePolicy);
                 } catch(ServiceSpecificException e) {
                     throw ExceptionUtils.throwAsPublicException(e);
                 } catch (RemoteException e) {
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
index df057a1..4ddf602 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
@@ -71,9 +71,12 @@
             }
 
             try {
-                CameraMetadataNative defaultRequest = cameraService.createDefaultRequest(mCameraId,
-                        templateType, mContext.getDeviceId(),
-                        mCameraManager.getDevicePolicyFromContext(mContext));
+                CameraMetadataNative defaultRequest =
+                        cameraService.createDefaultRequest(
+                                mCameraId,
+                                templateType,
+                                mCameraManager.getClientAttribution(),
+                                mCameraManager.getDevicePolicyFromContext(mContext));
                 CameraDeviceImpl.disableZslIfNeeded(defaultRequest, mTargetSdkVersion,
                         templateType);
 
@@ -104,9 +107,11 @@
             }
 
             try {
-                return cameraService.isSessionConfigurationWithParametersSupported(mCameraId,
-                        mTargetSdkVersion, config,
-                        mContext.getDeviceId(),
+                return cameraService.isSessionConfigurationWithParametersSupported(
+                        mCameraId,
+                        mTargetSdkVersion,
+                        config,
+                        mCameraManager.getClientAttribution(),
                         mCameraManager.getDevicePolicyFromContext(mContext));
             } catch (ServiceSpecificException e) {
                 throw ExceptionUtils.throwAsPublicException(e);
@@ -133,12 +138,14 @@
             }
 
             try {
-                CameraMetadataNative metadata = cameraService.getSessionCharacteristics(
-                        mCameraId, mTargetSdkVersion,
-                        CameraManager.getRotationOverride(mContext),
-                        sessionConfig,
-                        mContext.getDeviceId(),
-                        mCameraManager.getDevicePolicyFromContext(mContext));
+                CameraMetadataNative metadata =
+                        cameraService.getSessionCharacteristics(
+                                mCameraId,
+                                mTargetSdkVersion,
+                                CameraManager.getRotationOverride(mContext),
+                                sessionConfig,
+                                mCameraManager.getClientAttribution(),
+                                mCameraManager.getDevicePolicyFromContext(mContext));
 
                 return mCameraManager.prepareCameraCharacteristics(mCameraId, metadata,
                         cameraService);
diff --git a/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java b/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java
index b8f2c00..3be911abe7 100644
--- a/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java
+++ b/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java
@@ -69,8 +69,13 @@
     }
 
     /**
-     * Returns the scroll amount, normalized from -1.0 to 1.0, inclusive. Positive values
-     * indicate scrolling forward (e.g. down in a vertical list); negative values, backward.
+     * Returns the scroll amount, normalized from -1.0 to 1.0, inclusive.
+     * <p>
+     * Positive values indicate scrolling forward (e.g. down in a vertical list); negative values,
+     * backward.
+     * <p>
+     * Values of 1.0 or -1.0 represent the maximum supported scroll.
+     * </p>
      */
     public @FloatRange(from = -1.0f, to = 1.0f) float getScrollAmount() {
         return mScrollAmount;
@@ -91,7 +96,7 @@
      */
     public static final class Builder {
 
-        private float mScrollAmount;
+        @FloatRange(from = -1.0f, to = 1.0f) private float mScrollAmount = 0.0f;
         private long mEventTimeNanos = 0L;
 
         /**
@@ -102,9 +107,13 @@
         }
 
         /**
-         * Sets the scroll amount, normalized from -1.0 to 1.0, inclusive. Positive values
-         * indicate scrolling forward (e.g. down in a vertical list); negative values, backward.
-         *
+         * Sets the scroll amount, normalized from -1.0 to 1.0, inclusive.
+         * <p>
+         * Positive values indicate scrolling forward (e.g. down in a vertical list); negative
+         * values, backward.
+         * <p>
+         * Values of 1.0 or -1.0 represent the maximum supported scroll.
+         * </p>
          * @return this builder, to allow for chaining of calls
          */
         public @NonNull Builder setScrollAmount(
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 90d82e7..61cc23d 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -870,6 +870,16 @@
         }
 
         /**
+         * Returns true if this Builder is configured to hold data for the specified
+         * custom power component ID.
+         */
+        public boolean isSupportedCustomPowerComponent(int componentId) {
+            return componentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
+                    && componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
+                    + mBatteryConsumerDataLayout.customPowerComponentCount;
+        }
+
+        /**
          * Constructs a read-only object using the Builder values.
          */
         @NonNull
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 493d676..57853e7 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1403,6 +1403,19 @@
             "android.settings.QUICK_LAUNCH_SETTINGS";
 
     /**
+     * Activity Action: Showing settings to manage adaptive notifications.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_MANAGE_ADAPTIVE_NOTIFICATIONS =
+            "android.settings.MANAGE_ADAPTIVE_NOTIFICATIONS";
+
+    /**
      * Activity Action: Show settings to manage installed applications.
      * <p>
      * In some cases, a matching Activity may not exist, so ensure you
@@ -20094,7 +20107,7 @@
              * (0 = false, 1 = true)
              * @hide
              */
-            @Readable(maxTargetSdk = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+            @Readable
             public static final String REDUCE_MOTION = "reduce_motion";
 
             /**
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 21d6184..9512347 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -62,6 +62,7 @@
 import android.content.res.loader.ResourcesLoader;
 import android.content.res.loader.ResourcesProvider;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.BlendMode;
 import android.graphics.Outline;
 import android.graphics.PorterDuff;
@@ -90,6 +91,7 @@
 import android.util.IntArray;
 import android.util.Log;
 import android.util.LongArray;
+import android.util.LongSparseArray;
 import android.util.Pair;
 import android.util.SizeF;
 import android.util.SparseArray;
@@ -98,6 +100,7 @@
 import android.util.TypedValue.ComplexDimensionUnit;
 import android.util.proto.ProtoInputStream;
 import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoStream;
 import android.util.proto.ProtoUtils;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
@@ -1266,11 +1269,16 @@
                 int intentId = in.readInt();
                 String intentUri = in.readString8();
                 RemoteCollectionItems items = new RemoteCollectionItems(in, currentRootData);
-                mIdToUriMapping.put(intentId, intentUri);
-                mUriToCollectionMapping.put(intentUri, items);
+                addMapping(intentId, intentUri, items);
             }
         }
 
+        void addMapping(int intentId, String intentUri, RemoteCollectionItems items) {
+            mIdToUriMapping.put(intentId, intentUri);
+            mUriToCollectionMapping.put(intentUri, items);
+        }
+
+
         void setHierarchyDataForId(int intentId, HierarchyRootData data) {
             String uri = mIdToUriMapping.get(intentId);
             if (mUriToCollectionMapping.get(uri) == null) {
@@ -1465,6 +1473,87 @@
                 mUriToCollectionMapping.get(intentUri).writeToParcel(out, flags, true);
             }
         }
+
+        public void writeToProto(Context context, ProtoOutputStream out) {
+            final long token = out.start(RemoteViewsProto.REMOTE_COLLECTION_CACHE);
+            for (int i = 0; i < mIdToUriMapping.size(); i++) {
+                final long entryToken = out.start(RemoteViewsProto.RemoteCollectionCache.ENTRIES);
+                out.write(RemoteViewsProto.RemoteCollectionCache.Entry.ID,
+                        mIdToUriMapping.keyAt(i));
+                String intentUri = mIdToUriMapping.valueAt(i);
+                out.write(RemoteViewsProto.RemoteCollectionCache.Entry.URI, intentUri);
+                final long itemsToken = out.start(
+                        RemoteViewsProto.RemoteCollectionCache.Entry.ITEMS);
+                mUriToCollectionMapping.get(intentUri).writeToProto(context, out, /* attached= */
+                        true);
+                out.end(itemsToken);
+                out.end(entryToken);
+            }
+            out.end(token);
+        }
+    }
+
+    private PendingResources<RemoteCollectionCache> populateRemoteCollectionCacheFromProto(
+            ProtoInputStream in) throws Exception {
+        final ArrayList<LongSparseArray<Object>> entries = new ArrayList<>();
+        final long token = in.start(RemoteViewsProto.REMOTE_COLLECTION_CACHE);
+        while (in.nextField() != NO_MORE_FIELDS) {
+            switch (in.getFieldNumber()) {
+                case (int) RemoteViewsProto.RemoteCollectionCache.ENTRIES:
+                    final LongSparseArray<Object> entry = new LongSparseArray<>();
+                    final long entryToken = in.start(
+                            RemoteViewsProto.RemoteCollectionCache.ENTRIES);
+                    while (in.nextField() != NO_MORE_FIELDS) {
+                        switch (in.getFieldNumber()) {
+                            case (int) RemoteViewsProto.RemoteCollectionCache.Entry.ID:
+                                entry.put(RemoteViewsProto.RemoteCollectionCache.Entry.ID,
+                                        in.readInt(
+                                                RemoteViewsProto.RemoteCollectionCache.Entry.ID));
+                                break;
+                            case (int) RemoteViewsProto.RemoteCollectionCache.Entry.URI:
+                                entry.put(RemoteViewsProto.RemoteCollectionCache.Entry.URI,
+                                        in.readString(
+                                                RemoteViewsProto.RemoteCollectionCache.Entry.URI));
+                                break;
+                            case (int) RemoteViewsProto.RemoteCollectionCache.Entry.ITEMS:
+                                final long itemsToken = in.start(
+                                        RemoteViewsProto.RemoteCollectionCache.Entry.ITEMS);
+                                entry.put(RemoteViewsProto.RemoteCollectionCache.Entry.ITEMS,
+                                        RemoteCollectionItems.createFromProto(in));
+                                in.end(itemsToken);
+                                break;
+                            default:
+                                Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                        + ProtoUtils.currentFieldToString(in));
+                        }
+                    }
+                    in.end(entryToken);
+                    checkContainsKeys(entry,
+                            new long[]{RemoteViewsProto.RemoteCollectionCache.Entry.ID,
+                                    RemoteViewsProto.RemoteCollectionCache.Entry.URI,
+                                    RemoteViewsProto.RemoteCollectionCache.Entry.ITEMS});
+                    entries.add(entry);
+                    break;
+                default:
+                    Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                            + ProtoUtils.currentFieldToString(in));
+            }
+        }
+        in.end(token);
+
+        return (context, resources, rootData, depth) -> {
+            for (LongSparseArray<Object> entry : entries) {
+                int id = (int) entry.get(RemoteViewsProto.RemoteCollectionCache.Entry.ID);
+                String uri = (String) entry.get(RemoteViewsProto.RemoteCollectionCache.Entry.URI);
+                // Depth resets to 0 for RemoteCollectionItems
+                RemoteCollectionItems items = ((PendingResources<RemoteCollectionItems>) entry.get(
+                        RemoteViewsProto.RemoteCollectionCache.Entry.ITEMS)).create(context,
+                        resources, rootData, depth);
+                rootData.mRemoteCollectionCache.addMapping(id, uri, items);
+            }
+            // Redundant return, but type signature requires we return something.
+            return rootData.mRemoteCollectionCache;
+        };
     }
 
     private class SetRemoteViewsAdapterIntent extends Action {
@@ -2080,6 +2169,15 @@
             dest.writeTypedList(mBitmaps, flags);
         }
 
+        public void writeBitmapsToProto(ProtoOutputStream out) {
+            for (int i = 0; i < mBitmaps.size(); i++) {
+                final Bitmap bitmap = mBitmaps.get(i);
+                final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+                bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 100, bytes);
+                out.write(RemoteViewsProto.BITMAP_CACHE, bytes.toByteArray());
+            }
+        }
+
         public int getBitmapMemory() {
             if (mBitmapMemory < 0) {
                 mBitmapMemory = 0;
@@ -7522,6 +7620,127 @@
             dest.restoreAllowSquashing(prevAllowSquashing);
         }
 
+        /** @hide */
+        public void writeToProto(Context context, ProtoOutputStream out) {
+            writeToProto(context, out, /* attached= */ false);
+        }
+
+        private void writeToProto(Context context, ProtoOutputStream out, boolean attached) {
+            for (long id : mIds) {
+                out.write(RemoteViewsProto.RemoteCollectionItems.IDS, id);
+            }
+
+            boolean restoreRoot = false;
+            out.write(RemoteViewsProto.RemoteCollectionItems.ATTACHED, attached);
+            if (!attached && mViews.length > 0 && !mViews[0].mIsRoot) {
+                restoreRoot = true;
+                mViews[0].mIsRoot = true;
+            }
+            for (RemoteViews view : mViews) {
+                final long viewsToken = out.start(RemoteViewsProto.RemoteCollectionItems.VIEWS);
+                view.writePreviewToProto(context, out);
+                out.end(viewsToken);
+            }
+            if (restoreRoot) mViews[0].mIsRoot = false;
+            out.write(RemoteViewsProto.RemoteCollectionItems.HAS_STABLE_IDS, mHasStableIds);
+            out.write(RemoteViewsProto.RemoteCollectionItems.VIEW_TYPE_COUNT, mViewTypeCount);
+        }
+
+        /**
+         * Overload used for testing unattached RemoteCollectionItems serialization.
+         *
+         * @hide
+         */
+        public static RemoteCollectionItems createFromProto(Context context, ProtoInputStream in)
+                throws Exception {
+            return createFromProto(in).create(context, context.getResources(), /* rootData= */
+                    null, 0);
+        }
+
+        /** @hide */
+        public static PendingResources<RemoteCollectionItems> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+            values.put(RemoteViewsProto.RemoteCollectionItems.IDS, new ArrayList<Long>());
+            values.put(RemoteViewsProto.RemoteCollectionItems.VIEWS,
+                    new ArrayList<PendingResources<RemoteViews>>());
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.RemoteCollectionItems.IDS:
+                        ((ArrayList<Long>) values.get(
+                                RemoteViewsProto.RemoteCollectionItems.IDS)).add(
+                                in.readLong(RemoteViewsProto.RemoteCollectionItems.IDS));
+                        break;
+                    case (int) RemoteViewsProto.RemoteCollectionItems.VIEWS:
+                        final long viewsToken = in.start(
+                                RemoteViewsProto.RemoteCollectionItems.VIEWS);
+                        ((ArrayList<PendingResources<RemoteViews>>) values.get(
+                                RemoteViewsProto.RemoteCollectionItems.VIEWS)).add(
+                                RemoteViews.createFromProto(in));
+                        in.end(viewsToken);
+                        break;
+                    case (int) RemoteViewsProto.RemoteCollectionItems.HAS_STABLE_IDS:
+                        values.put(RemoteViewsProto.RemoteCollectionItems.HAS_STABLE_IDS,
+                                in.readBoolean(
+                                        RemoteViewsProto.RemoteCollectionItems.HAS_STABLE_IDS));
+                        break;
+                    case (int) RemoteViewsProto.RemoteCollectionItems.VIEW_TYPE_COUNT:
+                        values.put(RemoteViewsProto.RemoteCollectionItems.VIEW_TYPE_COUNT,
+                                in.readInt(RemoteViewsProto.RemoteCollectionItems.VIEW_TYPE_COUNT));
+                        break;
+                    case (int) RemoteViewsProto.RemoteCollectionItems.ATTACHED:
+                        values.put(RemoteViewsProto.RemoteCollectionItems.ATTACHED,
+                                in.readBoolean(RemoteViewsProto.RemoteCollectionItems.ATTACHED));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+
+            checkContainsKeys(values,
+                    new long[]{RemoteViewsProto.RemoteCollectionItems.VIEW_TYPE_COUNT});
+            return (context, resources, rootData, depth) -> {
+                List<Long> idList = (List<Long>) values.get(
+                        RemoteViewsProto.RemoteCollectionItems.IDS);
+                long[] ids = new long[idList.size()];
+                for (int i = 0; i < idList.size(); i++) {
+                    ids[i] = idList.get(i);
+                }
+                boolean attached = (boolean) values.get(
+                        RemoteViewsProto.RemoteCollectionItems.ATTACHED, false);
+                List<PendingResources<RemoteViews>> pendingViews =
+                        (List<PendingResources<RemoteViews>>) values.get(
+                                RemoteViewsProto.RemoteCollectionItems.VIEWS);
+                RemoteViews[] views = new RemoteViews[pendingViews.size()];
+
+                if (attached && rootData == null) {
+                    throw new IllegalStateException("Cannot create a RemoteCollectionItems from "
+                            + "proto that was attached without providing HierarchyRootData");
+                }
+
+                int firstChildIndex = 0;
+                if (!attached && pendingViews.size() > 0) {
+                    // If written as unattached, get HierarchyRootData from first view
+                    views[0] = pendingViews.get(0).create(context, resources, /* rootData= */ null,
+                            /* depth= */ 0);
+                    rootData = views[0].getHierarchyRootData();
+                    firstChildIndex = 1;
+                }
+                for (int i = firstChildIndex; i < views.length; i++) {
+                    // Depth is reset to 0 for RemoteCollectionItems item views, see Parcel
+                    // constructor.
+                    views[i] = pendingViews.get(i).create(context, resources, rootData,
+                            /* depth= */ 0);
+                }
+                return new RemoteCollectionItems(ids, views,
+                        (boolean) values.get(RemoteViewsProto.RemoteCollectionItems.HAS_STABLE_IDS,
+                                false),
+                        (int) values.get(RemoteViewsProto.RemoteCollectionItems.VIEW_TYPE_COUNT,
+                                0));
+            };
+        }
+
         /**
          * Returns the id for {@code position}. See {@link #hasStableIds()} for whether this id
          * should be considered meaningful across collection updates.
@@ -7907,6 +8126,10 @@
         if (mViewId != 0 && mViewId != -1) {
             out.write(RemoteViewsProto.VIEW_ID, appResources.getResourceName(mViewId));
         }
+        if (mIsRoot) {
+            mBitmapCache.writeBitmapsToProto(out);
+            mCollectionCache.writeToProto(context, out);
+        }
         out.write(RemoteViewsProto.IS_ROOT, mIsRoot);
         out.write(RemoteViewsProto.APPLY_FLAGS, mApplyFlags);
         out.write(RemoteViewsProto.HAS_DRAW_INSTRUCTIONS, mHasDrawInstructions);
@@ -7968,6 +8191,7 @@
             final List<PendingResources<RemoteViews>> mSizedRemoteViews = new ArrayList<>();
             PendingResources<RemoteViews> mLandscapeViews = null;
             PendingResources<RemoteViews> mPortraitViews = null;
+            PendingResources<RemoteCollectionCache> mPopulateRemoteCollectionCache = null;
             boolean mIsRoot = false;
             boolean mHasDrawInstructions = false;
         };
@@ -8018,6 +8242,18 @@
                         ref.mPortraitViews = createFromProto(in);
                         in.end(portraitToken);
                         break;
+                    case (int) RemoteViewsProto.BITMAP_CACHE:
+                        byte[] src = in.readBytes(RemoteViewsProto.BITMAP_CACHE);
+                        Bitmap bitmap = BitmapFactory.decodeByteArray(src, 0, src.length);
+                        ref.mRv.mBitmapCache.getBitmapId(bitmap);
+                        break;
+                    case (int) RemoteViewsProto.REMOTE_COLLECTION_CACHE:
+                        final long collectionToken = in.start(
+                                RemoteViewsProto.REMOTE_COLLECTION_CACHE);
+                        ref.mPopulateRemoteCollectionCache =
+                                ref.mRv.populateRemoteCollectionCacheFromProto(in);
+                        in.end(collectionToken);
+                        break;
                     case (int) RemoteViewsProto.IS_ROOT:
                         ref.mIsRoot = in.readBoolean(RemoteViewsProto.IS_ROOT);
                         break;
@@ -8087,6 +8323,9 @@
                     rv.setLightBackgroundLayoutId(lightBackgroundLayoutId);
                 }
             }
+            if (ref.mPopulateRemoteCollectionCache != null) {
+                ref.mPopulateRemoteCollectionCache.create(context, resources, rootData, depth);
+            }
             if (ref.mProviderInstanceId != -1) {
                 rv.mProviderInstanceId = ref.mProviderInstanceId;
             }
@@ -8139,6 +8378,16 @@
         }
     }
 
+    private static void checkContainsKeys(LongSparseArray<?> array, long[] requiredFields) {
+        for (long requiredField : requiredFields) {
+            if (array.indexOfKey(requiredField) < 0) {
+                throw new IllegalArgumentException(
+                        "RemoteViews proto missing field: " + ProtoStream.getFieldIdString(
+                                requiredField));
+            }
+        }
+    }
+
     private static SizeF createSizeFFromProto(ProtoInputStream in) throws Exception {
         float width = 0;
         float height = 0;
diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig
index 69cac6f..94f6503 100644
--- a/core/java/android/window/flags/responsible_apis.aconfig
+++ b/core/java/android/window/flags/responsible_apis.aconfig
@@ -56,3 +56,11 @@
     description: "Improved metrics."
     bug: "339245692"
 }
+
+flag {
+    name: "bal_send_intent_with_options"
+    namespace: "responsible_apis"
+    description: "Add options parameter to IntentSender.sendIntent."
+    bug: "339720406"
+}
+
diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java
index 0068490..9f5ed65 100644
--- a/core/java/com/android/internal/display/BrightnessSynchronizer.java
+++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java
@@ -78,9 +78,9 @@
     // Feature flag that will eventually be removed
     private final boolean mIntRangeUserPerceptionEnabled;
 
-    public BrightnessSynchronizer(Context context, boolean intRangeUserPerceptionEnabled) {
-        this(context, Looper.getMainLooper(), SystemClock::uptimeMillis,
-                intRangeUserPerceptionEnabled);
+    public BrightnessSynchronizer(Context context, Looper looper,
+            boolean intRangeUserPerceptionEnabled) {
+        this(context, looper, SystemClock::uptimeMillis, intRangeUserPerceptionEnabled);
     }
 
     @VisibleForTesting
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 618f622..ab04851 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -160,8 +160,20 @@
      */
     public static final int CUJ_DESKTOP_MODE_RESIZE_WINDOW = 106;
 
+    /** Track entering desktop mode interaction. */
+    public static final int CUJ_DESKTOP_MODE_ENTER_MODE = 107;
+
+    /** Track exiting desktop mode interaction. */
+    public static final int CUJ_DESKTOP_MODE_EXIT_MODE = 108;
+
+    /** Track minimize window interaction in desktop mode. */
+    public static final int CUJ_DESKTOP_MODE_MINIMIZE_WINDOW = 109;
+
+    /** Track window drag interaction in desktop mode. */
+    public static final int CUJ_DESKTOP_MODE_DRAG_WINDOW = 110;
+
     // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
-    @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_RESIZE_WINDOW;
+    @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_DRAG_WINDOW;
 
     /** @hide */
     @IntDef({
@@ -259,7 +271,11 @@
             CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK,
             CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW,
             CUJ_FOLD_ANIM,
-            CUJ_DESKTOP_MODE_RESIZE_WINDOW
+            CUJ_DESKTOP_MODE_RESIZE_WINDOW,
+            CUJ_DESKTOP_MODE_ENTER_MODE,
+            CUJ_DESKTOP_MODE_EXIT_MODE,
+            CUJ_DESKTOP_MODE_MINIMIZE_WINDOW,
+            CUJ_DESKTOP_MODE_DRAG_WINDOW
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {}
@@ -368,6 +384,10 @@
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_MAXIMIZE_WINDOW;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_FOLD_ANIM] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__FOLD_ANIM;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_RESIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_RESIZE_WINDOW;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_MODE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_MODE;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_EXIT_MODE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_EXIT_MODE;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_MINIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_MINIMIZE_WINDOW;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_DRAG_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_DRAG_WINDOW;
     }
 
     private Cuj() {
@@ -576,6 +596,14 @@
                 return "FOLD_ANIM";
             case CUJ_DESKTOP_MODE_RESIZE_WINDOW:
                 return "DESKTOP_MODE_RESIZE_WINDOW";
+            case CUJ_DESKTOP_MODE_ENTER_MODE:
+                return "DESKTOP_MODE_ENTER_MODE";
+            case CUJ_DESKTOP_MODE_EXIT_MODE:
+                return "DESKTOP_MODE_EXIT_MODE";
+            case CUJ_DESKTOP_MODE_MINIMIZE_WINDOW:
+                return "DESKTOP_MODE_MINIMIZE_WINDOW";
+            case CUJ_DESKTOP_MODE_DRAG_WINDOW:
+                return "DESKTOP_MODE_DRAG_WINDOW";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 66b2a9c..238e6f5 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -75,10 +75,11 @@
 /** @hide */
 public class TransitionAnimation {
     public static final int WALLPAPER_TRANSITION_NONE = 0;
-    public static final int WALLPAPER_TRANSITION_OPEN = 1;
-    public static final int WALLPAPER_TRANSITION_CLOSE = 2;
-    public static final int WALLPAPER_TRANSITION_INTRA_OPEN = 3;
-    public static final int WALLPAPER_TRANSITION_INTRA_CLOSE = 4;
+    public static final int WALLPAPER_TRANSITION_CHANGE = 1;
+    public static final int WALLPAPER_TRANSITION_OPEN = 2;
+    public static final int WALLPAPER_TRANSITION_CLOSE = 3;
+    public static final int WALLPAPER_TRANSITION_INTRA_OPEN = 4;
+    public static final int WALLPAPER_TRANSITION_INTRA_CLOSE = 5;
 
     // These are the possible states for the enter/exit activities during a thumbnail transition
     private static final int THUMBNAIL_TRANSITION_ENTER_SCALE_UP = 0;
diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp
index 2316f4c..b8fd3d0 100644
--- a/core/jni/android_hardware_Camera.cpp
+++ b/core/jni/android_hardware_Camera.cpp
@@ -17,9 +17,12 @@
 
 //#define LOG_NDEBUG 0
 #define LOG_TAG "Camera-JNI"
+#include <android/content/AttributionSourceState.h>
+#include <android_os_Parcel.h>
 #include <android_runtime/android_graphics_SurfaceTexture.h>
 #include <android_runtime/android_view_Surface.h>
 #include <binder/IMemory.h>
+#include <binder/Parcel.h>
 #include <camera/Camera.h>
 #include <camera/StringUtils.h>
 #include <cutils/properties.h>
@@ -523,22 +526,45 @@
     }
 }
 
-static jint android_hardware_Camera_getNumberOfCameras(JNIEnv *env, jobject thiz, jint deviceId,
+static bool attributionSourceStateForJavaParcel(JNIEnv *env, jobject jClientAttributionParcel,
+                                                AttributionSourceState &clientAttribution) {
+    const Parcel *clientAttributionParcel = parcelForJavaObject(env, jClientAttributionParcel);
+    if (clientAttribution.readFromParcel(clientAttributionParcel) != ::android::OK) {
+        jniThrowRuntimeException(env, "Fail to unparcel AttributionSourceState");
+        return false;
+    }
+    clientAttribution.uid = Camera::USE_CALLING_UID;
+    clientAttribution.pid = Camera::USE_CALLING_PID;
+    return true;
+}
+
+static jint android_hardware_Camera_getNumberOfCameras(JNIEnv *env, jobject thiz,
+                                                       jobject jClientAttributionParcel,
                                                        jint devicePolicy) {
-    return Camera::getNumberOfCameras(deviceId, devicePolicy);
+    AttributionSourceState clientAttribution;
+    if (!attributionSourceStateForJavaParcel(env, jClientAttributionParcel, clientAttribution)) {
+        return 0;
+    }
+    return Camera::getNumberOfCameras(clientAttribution, devicePolicy);
 }
 
 static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, jint cameraId,
-                                                  jint rotationOverride, jint deviceId,
+                                                  jint rotationOverride,
+                                                  jobject jClientAttributionParcel,
                                                   jint devicePolicy, jobject info_obj) {
+    AttributionSourceState clientAttribution;
+    if (!attributionSourceStateForJavaParcel(env, jClientAttributionParcel, clientAttribution)) {
+        return;
+    }
+
     CameraInfo cameraInfo;
-    if (cameraId >= Camera::getNumberOfCameras(deviceId, devicePolicy) || cameraId < 0) {
+    if (cameraId >= Camera::getNumberOfCameras(clientAttribution, devicePolicy) || cameraId < 0) {
         ALOGE("%s: Unknown camera ID %d", __FUNCTION__, cameraId);
         jniThrowRuntimeException(env, "Unknown camera ID");
         return;
     }
 
-    status_t rc = Camera::getCameraInfo(cameraId, rotationOverride, deviceId, devicePolicy,
+    status_t rc = Camera::getCameraInfo(cameraId, rotationOverride, clientAttribution, devicePolicy,
                                         &cameraInfo);
     if (rc != NO_ERROR) {
         jniThrowRuntimeException(env, "Fail to get camera info");
@@ -557,9 +583,14 @@
 // connect to camera service
 static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,
                                                  jint cameraId, jstring clientPackageName,
-                                                 jint rotationOverride,
-                                                 jboolean forceSlowJpegMode, jint deviceId,
+                                                 jint rotationOverride, jboolean forceSlowJpegMode,
+                                                 jobject jClientAttributionParcel,
                                                  jint devicePolicy) {
+    AttributionSourceState clientAttribution;
+    if (!attributionSourceStateForJavaParcel(env, jClientAttributionParcel, clientAttribution)) {
+        return -EACCES;
+    }
+
     // Convert jstring to String16
     const char16_t *rawClientName = reinterpret_cast<const char16_t*>(
         env->GetStringChars(clientPackageName, NULL));
@@ -569,10 +600,8 @@
                             reinterpret_cast<const jchar*>(rawClientName));
 
     int targetSdkVersion = android_get_application_target_sdk_version();
-    sp<Camera> camera =
-            Camera::connect(cameraId, clientName, Camera::USE_CALLING_UID, Camera::USE_CALLING_PID,
-                            targetSdkVersion, rotationOverride, forceSlowJpegMode, deviceId,
-                            devicePolicy);
+    sp<Camera> camera = Camera::connect(cameraId, clientName, targetSdkVersion, rotationOverride,
+                                        forceSlowJpegMode, clientAttribution, devicePolicy);
     if (camera == NULL) {
         return -EACCES;
     }
@@ -600,7 +629,7 @@
 
     // Update default display orientation in case the sensor is reverse-landscape
     CameraInfo cameraInfo;
-    status_t rc = Camera::getCameraInfo(cameraId, rotationOverride, deviceId, devicePolicy,
+    status_t rc = Camera::getCameraInfo(cameraId, rotationOverride, clientAttribution, devicePolicy,
                                         &cameraInfo);
     if (rc != NO_ERROR) {
         ALOGE("%s: getCameraInfo error: %d", __FUNCTION__, rc);
@@ -1056,10 +1085,11 @@
 //-------------------------------------------------
 
 static const JNINativeMethod camMethods[] = {
-        {"_getNumberOfCameras", "(II)I", (void *)android_hardware_Camera_getNumberOfCameras},
-        {"_getCameraInfo", "(IIIILandroid/hardware/Camera$CameraInfo;)V",
+        {"_getNumberOfCameras", "(Landroid/os/Parcel;I)I",
+         (void *)android_hardware_Camera_getNumberOfCameras},
+        {"_getCameraInfo", "(IILandroid/os/Parcel;ILandroid/hardware/Camera$CameraInfo;)V",
          (void *)android_hardware_Camera_getCameraInfo},
-        {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;IZII)I",
+        {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;IZLandroid/os/Parcel;I)I",
          (void *)android_hardware_Camera_native_setup},
         {"native_release", "()V", (void *)android_hardware_Camera_release},
         {"setPreviewSurface", "(Landroid/view/Surface;)V",
diff --git a/core/proto/android/content/res/color_state_list.proto b/core/proto/android/content/res/color_state_list.proto
new file mode 100644
index 0000000..3d0d8a8
--- /dev/null
+++ b/core/proto/android/content/res/color_state_list.proto
@@ -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 optional 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.
+ */
+
+syntax = "proto2";
+
+option java_multiple_files = true;
+
+package android.content.res;
+
+import "frameworks/base/core/proto/android/privacy.proto";
+
+/**
+ * An android.content.res.ColorStateList object.
+ */
+message ColorStateListProto {
+    option (android.msg_privacy).dest = DEST_AUTOMATIC;
+    repeated StateSpec state_specs = 1;
+    repeated int32 colors = 2 [packed = true];
+
+    message StateSpec {
+        repeated int32 state = 1 [packed = true];
+    }
+}
diff --git a/core/proto/android/widget/remoteviews.proto b/core/proto/android/widget/remoteviews.proto
index d24da03..f08ea1b 100644
--- a/core/proto/android/widget/remoteviews.proto
+++ b/core/proto/android/widget/remoteviews.proto
@@ -51,6 +51,26 @@
     optional RemoteViewsProto landscape_remoteviews = 11;
     optional bool is_root = 12;
     optional bool has_draw_instructions = 13;
+    repeated bytes bitmap_cache = 14;
+    optional RemoteCollectionCache remote_collection_cache = 15;
+
+    message RemoteCollectionCache {
+        message Entry {
+            optional int64 id = 1;
+            optional string uri = 2;
+            optional RemoteCollectionItems items = 3;
+        }
+
+        repeated Entry entries = 1;
+    }
+
+    message RemoteCollectionItems {
+        repeated int64 ids = 1 [packed = true];
+        repeated RemoteViewsProto views = 2;
+        optional bool has_stable_ids = 3;
+        optional int32 view_type_count = 4;
+        optional bool attached = 5;
+    }
 }
 
 
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index 37412a0..f5bb554 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -480,17 +480,17 @@
 
     <!-- Colors used in Android system, from design system.
      These values can be overlaid at runtime by OverlayManager RROs. -->
-    <color name="system_primary_container_light">#5E73A9</color>
-    <color name="system_on_primary_container_light">#FFFFFF</color>
-    <color name="system_primary_light">#2A4174</color>
+    <color name="system_primary_container_light">#D9E2FF</color>
+    <color name="system_on_primary_container_light">#001945</color>
+    <color name="system_primary_light">#475D92</color>
     <color name="system_on_primary_light">#FFFFFF</color>
-    <color name="system_secondary_container_light">#6E7488</color>
-    <color name="system_on_secondary_container_light">#FFFFFF</color>
-    <color name="system_secondary_light">#3C4255</color>
+    <color name="system_secondary_container_light">#DCE2F9</color>
+    <color name="system_on_secondary_container_light">#151B2C</color>
+    <color name="system_secondary_light">#575E71</color>
     <color name="system_on_secondary_light">#FFFFFF</color>
-    <color name="system_tertiary_container_light">#8A6A89</color>
-    <color name="system_on_tertiary_container_light">#FFFFFF</color>
-    <color name="system_tertiary_light">#553A55</color>
+    <color name="system_tertiary_container_light">#FDD7FA</color>
+    <color name="system_on_tertiary_container_light">#2A122C</color>
+    <color name="system_tertiary_light">#725572</color>
     <color name="system_on_tertiary_light">#FFFFFF</color>
     <color name="system_background_light">#FAF8FF</color>
     <color name="system_on_background_light">#1A1B20</color>
@@ -504,17 +504,17 @@
     <color name="system_surface_bright_light">#FAF8FF</color>
     <color name="system_surface_dim_light">#DAD9E0</color>
     <color name="system_surface_variant_light">#E1E2EC</color>
-    <color name="system_on_surface_variant_light">#40434B</color>
-    <color name="system_outline_light">#5D5F67</color>
-    <color name="system_outline_variant_light">#797A83</color>
-    <color name="system_error_light">#8C0009</color>
+    <color name="system_on_surface_variant_light">#44464F</color>
+    <color name="system_outline_light">#757780</color>
+    <color name="system_outline_variant_light">#C5C6D0</color>
+    <color name="system_error_light">#BA1A1A</color>
     <color name="system_on_error_light">#FFFFFF</color>
-    <color name="system_error_container_light">#DA342E</color>
-    <color name="system_on_error_container_light">#FFFFFF</color>
+    <color name="system_error_container_light">#FFDAD6</color>
+    <color name="system_on_error_container_light">#410002</color>
     <color name="system_control_activated_light">#D9E2FF</color>
     <color name="system_control_normal_light">#44464F</color>
     <color name="system_control_highlight_light">#000000</color>
-    <color name="system_text_primary_inverse_light">#E2E2E9</color>
+<color name="system_text_primary_inverse_light">#E2E2E9</color>
     <color name="system_text_secondary_and_tertiary_inverse_light">#C5C6D0</color>
     <color name="system_text_primary_inverse_disable_only_light">#E2E2E9</color>
     <color name="system_text_secondary_and_tertiary_inverse_disabled_light">#E2E2E9</color>
@@ -524,22 +524,22 @@
     <color name="system_palette_key_color_tertiary_light">#8C6D8C</color>
     <color name="system_palette_key_color_neutral_light">#76777D</color>
     <color name="system_palette_key_color_neutral_variant_light">#757780</color>
-    <color name="system_primary_container_dark">#7A90C8</color>
-    <color name="system_on_primary_container_dark">#000000</color>
-    <color name="system_primary_dark">#B7CAFF</color>
-    <color name="system_on_primary_dark">#00143B</color>
-    <color name="system_secondary_container_dark">#8A90A5</color>
-    <color name="system_on_secondary_container_dark">#000000</color>
-    <color name="system_secondary_dark">#C4CAE1</color>
-    <color name="system_on_secondary_dark">#0F1626</color>
-    <color name="system_tertiary_container_dark">#A886A6</color>
-    <color name="system_on_tertiary_container_dark">#000000</color>
-    <color name="system_tertiary_dark">#E4BFE2</color>
-    <color name="system_on_tertiary_dark">#240D26</color>
+    <color name="system_primary_container_dark">#2F4578</color>
+    <color name="system_on_primary_container_dark">#D9E2FF</color>
+    <color name="system_primary_dark">#B0C6FF</color>
+    <color name="system_on_primary_dark">#152E60</color>
+    <color name="system_secondary_container_dark">#404659</color>
+    <color name="system_on_secondary_container_dark">#DCE2F9</color>
+    <color name="system_secondary_dark">#C0C6DC</color>
+    <color name="system_on_secondary_dark">#2A3042</color>
+    <color name="system_tertiary_container_dark">#593D59</color>
+    <color name="system_on_tertiary_container_dark">#FDD7FA</color>
+    <color name="system_tertiary_dark">#E0BBDD</color>
+    <color name="system_on_tertiary_dark">#412742</color>
     <color name="system_background_dark">#121318</color>
     <color name="system_on_background_dark">#E2E2E9</color>
     <color name="system_surface_dark">#121318</color>
-    <color name="system_on_surface_dark">#FCFAFF</color>
+    <color name="system_on_surface_dark">#E2E2E9</color>
     <color name="system_surface_container_low_dark">#1A1B20</color>
     <color name="system_surface_container_lowest_dark">#0C0E13</color>
     <color name="system_surface_container_dark">#1E1F25</color>
@@ -548,13 +548,13 @@
     <color name="system_surface_bright_dark">#38393F</color>
     <color name="system_surface_dim_dark">#121318</color>
     <color name="system_surface_variant_dark">#44464F</color>
-    <color name="system_on_surface_variant_dark">#C9CAD4</color>
-    <color name="system_outline_dark">#A1A2AC</color>
-    <color name="system_outline_variant_dark">#81838C</color>
-    <color name="system_error_dark">#FFBAB1</color>
-    <color name="system_on_error_dark">#370001</color>
-    <color name="system_error_container_dark">#FF5449</color>
-    <color name="system_on_error_container_dark">#000000</color>
+    <color name="system_on_surface_variant_dark">#C5C6D0</color>
+    <color name="system_outline_dark">#8F9099</color>
+    <color name="system_outline_variant_dark">#44464F</color>
+    <color name="system_error_dark">#FFB4AB</color>
+    <color name="system_on_error_dark">#690005</color>
+    <color name="system_error_container_dark">#93000A</color>
+    <color name="system_on_error_container_dark">#FFDAD6</color>
     <color name="system_control_activated_dark">#2F4578</color>
     <color name="system_control_normal_dark">#C5C6D0</color>
     <color name="system_control_highlight_dark">#FFFFFF</color>
@@ -568,63 +568,63 @@
     <color name="system_palette_key_color_tertiary_dark">#8C6D8C</color>
     <color name="system_palette_key_color_neutral_dark">#76777D</color>
     <color name="system_palette_key_color_neutral_variant_dark">#757780</color>
-    <color name="system_primary_fixed">#5E73A9</color>
-    <color name="system_primary_fixed_dim">#455B8F</color>
-    <color name="system_on_primary_fixed">#FFFFFF</color>
-    <color name="system_on_primary_fixed_variant">#FFFFFF</color>
-    <color name="system_secondary_fixed">#6E7488</color>
-    <color name="system_secondary_fixed_dim">#555C6F</color>
-    <color name="system_on_secondary_fixed">#FFFFFF</color>
-    <color name="system_on_secondary_fixed_variant">#FFFFFF</color>
-    <color name="system_tertiary_fixed">#8A6A89</color>
-    <color name="system_tertiary_fixed_dim">#705270</color>
-    <color name="system_on_tertiary_fixed">#FFFFFF</color>
-    <color name="system_on_tertiary_fixed_variant">#FFFFFF</color>
+    <color name="system_primary_fixed">#D9E2FF</color>
+    <color name="system_primary_fixed_dim">#B0C6FF</color>
+    <color name="system_on_primary_fixed">#001945</color>
+    <color name="system_on_primary_fixed_variant">#2F4578</color>
+    <color name="system_secondary_fixed">#DCE2F9</color>
+    <color name="system_secondary_fixed_dim">#C0C6DC</color>
+    <color name="system_on_secondary_fixed">#151B2C</color>
+    <color name="system_on_secondary_fixed_variant">#404659</color>
+    <color name="system_tertiary_fixed">#FDD7FA</color>
+    <color name="system_tertiary_fixed_dim">#E0BBDD</color>
+    <color name="system_on_tertiary_fixed">#2A122C</color>
+    <color name="system_on_tertiary_fixed_variant">#593D59</color>
 
     <!--Colors used in Android system, from design system. These values can be overlaid at runtime
      by OverlayManager RROs.-->
     <color name="system_widget_background_light">#EEF0FF</color>
-    <color name="system_clock_hour_light">#1D2435</color>
-    <color name="system_clock_minute_light">#20386A</color>
-    <color name="system_clock_second_light">#000000</color>
-    <color name="system_theme_app_light">#2F4578</color>
-    <color name="system_on_theme_app_light">#D6DFFF</color>
+    <color name="system_clock_hour_light">#373D50</color>
+    <color name="system_clock_minute_light">#3D5487</color>
+    <color name="system_clock_second_light">#4F659A</color>
+    <color name="system_theme_app_light">#D9E2FF</color>
+    <color name="system_on_theme_app_light">#475D92</color>
     <color name="system_theme_app_ring_light">#94AAE4</color>
-    <color name="system_theme_notif_light">#FDD7FA</color>
-    <color name="system_brand_a_light">#3A5084</color>
+    <color name="system_theme_notif_light">#E0BBDD</color>
+    <color name="system_brand_a_light">#475D92</color>
     <color name="system_brand_b_light">#6E7488</color>
-    <color name="system_brand_c_light">#6076AC</color>
-    <color name="system_brand_d_light">#8C6D8C</color>
+    <color name="system_brand_c_light">#5E73A9</color>
+    <color name="system_brand_d_light">#8A6A89</color>
     <color name="system_under_surface_light">#000000</color>
-    <color name="system_shade_active_light">#D9E2FF</color>
+<color name="system_shade_active_light">#D9E2FF</color>
     <color name="system_on_shade_active_light">#152E60</color>
     <color name="system_on_shade_active_variant_light">#2F4578</color>
     <color name="system_shade_inactive_light">#2F3036</color>
     <color name="system_on_shade_inactive_light">#E1E2EC</color>
     <color name="system_on_shade_inactive_variant_light">#C5C6D0</color>
     <color name="system_shade_disabled_light">#0C0E13</color>
-    <color name="system_overview_background_light">#50525A</color>
+    <color name="system_overview_background_light">#C5C6D0</color>
     <color name="system_widget_background_dark">#152E60</color>
-    <color name="system_clock_hour_dark">#9AA0B6</color>
-    <color name="system_clock_minute_dark">#D8E1FF</color>
-    <color name="system_clock_second_dark">#FFFFFF</color>
-    <color name="system_theme_app_dark">#D9E2FF</color>
-    <color name="system_on_theme_app_dark">#304679</color>
+    <color name="system_clock_hour_dark">#8A90A5</color>
+    <color name="system_clock_minute_dark">#D9E2FF</color>
+    <color name="system_clock_second_dark">#B0C6FF</color>
+    <color name="system_theme_app_dark">#2F4578</color>
+    <color name="system_on_theme_app_dark">#B0C6FF</color>
     <color name="system_theme_app_ring_dark">#94AAE4</color>
-    <color name="system_theme_notif_dark">#E0BBDD</color>
-    <color name="system_brand_a_dark">#90A6DF</color>
-    <color name="system_brand_b_dark">#A4ABC1</color>
+    <color name="system_theme_notif_dark">#FDD7FA</color>
+    <color name="system_brand_a_dark">#B0C6FF</color>
+    <color name="system_brand_b_dark">#DCE2F9</color>
     <color name="system_brand_c_dark">#7A90C8</color>
-    <color name="system_brand_d_dark">#A886A6</color>
+    <color name="system_brand_d_dark">#FDD7FA</color>
     <color name="system_under_surface_dark">#000000</color>
-    <color name="system_shade_active_dark">#D9E2FF</color>
+<color name="system_shade_active_dark">#D9E2FF</color>
     <color name="system_on_shade_active_dark">#001945</color>
     <color name="system_on_shade_active_variant_dark">#2F4578</color>
     <color name="system_shade_inactive_dark">#2F3036</color>
     <color name="system_on_shade_inactive_dark">#E1E2EC</color>
     <color name="system_on_shade_inactive_variant_dark">#C5C6D0</color>
     <color name="system_shade_disabled_dark">#0C0E13</color>
-    <color name="system_overview_background_dark">#C5C6D0</color>
+    <color name="system_overview_background_dark">#50525A</color>
 
     <!-- Accessibility shortcut icon background color -->
     <color name="accessibility_feature_background">#5F6368</color> <!-- Google grey 700 -->
diff --git a/core/tests/coretests/src/android/graphics/ColorStateListTest.java b/core/tests/coretests/src/android/graphics/ColorStateListTest.java
index a3d52ea..ab41bd0 100644
--- a/core/tests/coretests/src/android/graphics/ColorStateListTest.java
+++ b/core/tests/coretests/src/android/graphics/ColorStateListTest.java
@@ -19,6 +19,8 @@
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.test.AndroidTestCase;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
 
 import androidx.test.filters.SmallTest;
 
@@ -49,6 +51,15 @@
     }
 
     @SmallTest
+    public void testStateIsInList_proto() throws Exception {
+        ColorStateList colorStateList = recreateFromProto(
+                mResources.getColorStateList(R.color.color1));
+        int[] focusedState = {android.R.attr.state_focused};
+        int focusColor = colorStateList.getColorForState(focusedState, R.color.failColor);
+        assertEquals(mResources.getColor(R.color.testcolor1), focusColor);
+    }
+
+    @SmallTest
     public void testEmptyState() throws Exception {
         ColorStateList colorStateList = mResources.getColorStateList(R.color.color1);
         int[] emptyState = {};
@@ -57,6 +68,15 @@
     }
 
     @SmallTest
+    public void testEmptyState_proto() throws Exception {
+        ColorStateList colorStateList = recreateFromProto(
+                mResources.getColorStateList(R.color.color1));
+        int[] emptyState = {};
+        int defaultColor = colorStateList.getColorForState(emptyState, mFailureColor);
+        assertEquals(mResources.getColor(R.color.testcolor2), defaultColor);
+    }
+
+    @SmallTest
     public void testGetColor() throws Exception {
         int defaultColor = mResources.getColor(R.color.color1);
         assertEquals(mResources.getColor(R.color.testcolor2), defaultColor);
@@ -73,4 +93,11 @@
         int defaultColor = mResources.getColor(R.color.color_with_lstar);
         assertEquals(mResources.getColor(R.color.testcolor3), defaultColor);
     }
+
+    private ColorStateList recreateFromProto(ColorStateList colorStateList) throws Exception {
+        ProtoOutputStream out = new ProtoOutputStream();
+        colorStateList.writeToProto(out);
+        ProtoInputStream in = new ProtoInputStream(out.getBytes());
+        return ColorStateList.createFromProto(in);
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java
index 06d888b..7ffc7b2 100644
--- a/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java
@@ -18,7 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
@@ -77,7 +76,6 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "b/321832617")
     public void corruptedFile() throws IOException {
         // Create an invalid binary XML file to cause IOException: "Unexpected magic number"
         try (FileWriter w = new FileWriter(mFile)) {
diff --git a/data/keyboards/Vendor_18d1_Product_4f60.idc b/data/keyboards/Vendor_18d1_Product_4f60.idc
new file mode 100644
index 0000000..b9fd406
--- /dev/null
+++ b/data/keyboards/Vendor_18d1_Product_4f60.idc
@@ -0,0 +1,18 @@
+# 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.
+
+# Increase palm thresholds, since this touchpad has a tendency to overstate
+# touch sizes.
+gestureProp.Palm_Width = 40.0
+gestureProp.Multiple_Palm_Width = 40.0
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 7d5f9cd..5fe3f2a 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -14,88 +14,100 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/maximize_menu"
-    style="?android:attr/buttonBarStyle"
     android:layout_width="@dimen/desktop_mode_maximize_menu_width"
     android:layout_height="@dimen/desktop_mode_maximize_menu_height"
-    android:orientation="horizontal"
-    android:gravity="center"
-    android:padding="16dp"
     android:background="@drawable/desktop_mode_maximize_menu_background"
     android:elevation="1dp">
 
     <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:orientation="vertical">
+        android:id="@+id/container"
+        android:layout_width="@dimen/desktop_mode_maximize_menu_width"
+        android:layout_height="@dimen/desktop_mode_maximize_menu_height"
+        android:orientation="horizontal"
+        android:padding="16dp"
+        android:gravity="center">
 
-        <Button
-            android:layout_width="94dp"
-            android:layout_height="60dp"
-            android:id="@+id/maximize_menu_maximize_button"
-            style="?android:attr/buttonBarButtonStyle"
-            android:stateListAnimator="@null"
-            android:layout_marginRight="8dp"
-            android:layout_marginBottom="4dp"
-            android:alpha="0"/>
-
-        <TextView
-            android:id="@+id/maximize_menu_maximize_window_text"
-            android:layout_width="94dp"
-            android:layout_height="18dp"
-            android:textSize="11sp"
-            android:layout_marginBottom="76dp"
-            android:gravity="center"
-            android:fontFamily="google-sans-text"
-            android:text="@string/desktop_mode_maximize_menu_maximize_text"
-            android:textColor="?androidprv:attr/materialColorOnSurface"
-            android:alpha="0"/>
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:orientation="vertical">
         <LinearLayout
-            android:id="@+id/maximize_menu_snap_menu_layout"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:orientation="horizontal"
-            android:padding="4dp"
-            android:background="@drawable/desktop_mode_maximize_menu_layout_background"
-            android:layout_marginBottom="4dp"
-            android:alpha="0">
-            <Button
-                android:id="@+id/maximize_menu_snap_left_button"
-                style="?android:attr/buttonBarButtonStyle"
-                android:layout_width="41dp"
-                android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
-                android:layout_marginRight="4dp"
-                android:background="@drawable/desktop_mode_maximize_menu_button_background"
-                android:stateListAnimator="@null"/>
+            android:orientation="vertical">
 
             <Button
-                android:id="@+id/maximize_menu_snap_right_button"
+                android:layout_width="94dp"
+                android:layout_height="60dp"
+                android:id="@+id/maximize_menu_maximize_button"
                 style="?android:attr/buttonBarButtonStyle"
-                android:layout_width="41dp"
-                android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
-                android:background="@drawable/desktop_mode_maximize_menu_button_background"
-                android:stateListAnimator="@null"/>
+                android:stateListAnimator="@null"
+                android:layout_marginRight="8dp"
+                android:layout_marginBottom="4dp"
+                android:alpha="0"/>
+
+            <TextView
+                android:id="@+id/maximize_menu_maximize_window_text"
+                android:layout_width="94dp"
+                android:layout_height="18dp"
+                android:textSize="11sp"
+                android:layout_marginBottom="76dp"
+                android:gravity="center"
+                android:fontFamily="google-sans-text"
+                android:text="@string/desktop_mode_maximize_menu_maximize_text"
+                android:textColor="?androidprv:attr/materialColorOnSurface"
+                android:alpha="0"/>
         </LinearLayout>
-        <TextView
-            android:id="@+id/maximize_menu_snap_window_text"
-            android:layout_width="94dp"
-            android:layout_height="18dp"
-            android:textSize="11sp"
-            android:layout_marginBottom="76dp"
-            android:layout_gravity="center"
-            android:gravity="center"
-            android:fontFamily="google-sans-text"
-            android:text="@string/desktop_mode_maximize_menu_snap_text"
-            android:textColor="?androidprv:attr/materialColorOnSurface"
-            android:alpha="0"/>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+            <LinearLayout
+                android:id="@+id/maximize_menu_snap_menu_layout"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:padding="4dp"
+                android:background="@drawable/desktop_mode_maximize_menu_layout_background"
+                android:layout_marginBottom="4dp"
+                android:alpha="0">
+                <Button
+                    android:id="@+id/maximize_menu_snap_left_button"
+                    style="?android:attr/buttonBarButtonStyle"
+                    android:layout_width="41dp"
+                    android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
+                    android:layout_marginRight="4dp"
+                    android:background="@drawable/desktop_mode_maximize_menu_button_background"
+                    android:stateListAnimator="@null"/>
+
+                <Button
+                    android:id="@+id/maximize_menu_snap_right_button"
+                    style="?android:attr/buttonBarButtonStyle"
+                    android:layout_width="41dp"
+                    android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
+                    android:background="@drawable/desktop_mode_maximize_menu_button_background"
+                    android:stateListAnimator="@null"/>
+            </LinearLayout>
+            <TextView
+                android:id="@+id/maximize_menu_snap_window_text"
+                android:layout_width="94dp"
+                android:layout_height="18dp"
+                android:textSize="11sp"
+                android:layout_marginBottom="76dp"
+                android:layout_gravity="center"
+                android:gravity="center"
+                android:fontFamily="google-sans-text"
+                android:text="@string/desktop_mode_maximize_menu_snap_text"
+                android:textColor="?androidprv:attr/materialColorOnSurface"
+                android:alpha="0"/>
+        </LinearLayout>
     </LinearLayout>
-</LinearLayout>
+
+    <!-- Empty view intentionally placed in front of everything else and matching the menu size
+     used to monitor input events over the entire menu. -->
+    <View
+        android:id="@+id/maximize_menu_overlay"
+        android:layout_width="@dimen/desktop_mode_maximize_menu_width"
+        android:layout_height="@dimen/desktop_mode_maximize_menu_height"/>
+</FrameLayout>
 
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 1fcfa7f..43cdcca 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
@@ -17,6 +17,7 @@
 package com.android.wm.shell.dagger;
 
 import android.annotation.Nullable;
+import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.pm.LauncherApps;
 import android.os.Handler;
@@ -514,6 +515,7 @@
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             DragAndDropController dragAndDropController,
             Transitions transitions,
+            KeyguardManager keyguardManager,
             EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler,
             ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler,
             ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
@@ -528,7 +530,7 @@
             Optional<RecentTasksController> recentTasksController) {
         return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
                 displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
-                dragAndDropController, transitions, enterDesktopTransitionHandler,
+                dragAndDropController, transitions, keyguardManager, enterDesktopTransitionHandler,
                 exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler,
                 dragToDesktopTransitionHandler, desktopModeTaskRepository,
                 desktopModeLoggerTransitionObserver, launchAdjacentController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
index 677fd5d..240cf3b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
@@ -212,12 +212,13 @@
     @WMSingleton
     @Provides
     static PipMotionHelper providePipMotionHelper(Context context,
+            @ShellMainThread ShellExecutor mainExecutor,
             PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer,
             PhonePipMenuController menuController, PipSnapAlgorithm pipSnapAlgorithm,
             PipTransitionController pipTransitionController,
             FloatingContentCoordinator floatingContentCoordinator,
             Optional<PipPerfHintController> pipPerfHintControllerOptional) {
-        return new PipMotionHelper(context, pipBoundsState, pipTaskOrganizer,
+        return new PipMotionHelper(context, mainExecutor, pipBoundsState, pipTaskOrganizer,
                 menuController, pipSnapAlgorithm, pipTransitionController,
                 floatingContentCoordinator, pipPerfHintControllerOptional);
     }
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 1965382..5813f85 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
@@ -18,6 +18,7 @@
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.app.ActivityOptions
+import android.app.KeyguardManager
 import android.app.PendingIntent
 import android.app.TaskInfo
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
@@ -108,6 +109,7 @@
     private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
     private val dragAndDropController: DragAndDropController,
     private val transitions: Transitions,
+    private val keyguardManager: KeyguardManager,
     private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
     private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
     private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
@@ -972,6 +974,12 @@
         transition: IBinder
     ): WindowContainerTransaction? {
         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch")
+        if (keyguardManager.isKeyguardLocked) {
+            // Do NOT handle freeform task launch when locked.
+            // It will be launched in fullscreen windowing mode (Details: b/160925539)
+            KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: skip keyguard is locked")
+            return null
+        }
         if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) {
             KtProtoLog.d(
                 WM_SHELL_DESKTOP_MODE,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index a749019..b27c428 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -16,10 +16,12 @@
 
 package com.android.wm.shell.pip;
 
+import android.annotation.NonNull;
 import android.graphics.Rect;
 
 import com.android.wm.shell.shared.annotations.ExternalThread;
 
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -69,9 +71,10 @@
     default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { }
 
     /**
-     * @return {@link PipTransitionController} instance.
+     * Register {@link PipTransitionController.PipTransitionCallback} to listen on PiP transition
+     * started / finished callbacks.
      */
-    default PipTransitionController getPipTransitionController() {
-        return null;
-    }
+    default void registerPipTransitionCallback(
+            @NonNull PipTransitionController.PipTransitionCallback callback,
+            @NonNull Executor executor) { }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index e2e1ecd..3fae370 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -423,7 +423,8 @@
             });
             mPipTransitionController.setPipOrganizer(this);
             displayController.addDisplayWindowListener(this);
-            pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback);
+            pipTransitionController.registerPipTransitionCallback(
+                    mPipTransitionCallback, mMainExecutor);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index 6eefdcf..a7c47f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -53,8 +53,9 @@
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
 
 /**
  * Responsible supplying PiP Transitions.
@@ -66,7 +67,7 @@
     protected final ShellTaskOrganizer mShellTaskOrganizer;
     protected final PipMenuController mPipMenuController;
     protected final Transitions mTransitions;
-    private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
+    private final Map<PipTransitionCallback, Executor> mPipTransitionCallbacks = new HashMap<>();
     protected PipTaskOrganizer mPipOrganizer;
     protected DefaultMixedHandler mMixedHandler;
 
@@ -181,16 +182,18 @@
     /**
      * Registers {@link PipTransitionCallback} to receive transition callbacks.
      */
-    public void registerPipTransitionCallback(PipTransitionCallback callback) {
-        mPipTransitionCallbacks.add(callback);
+    public void registerPipTransitionCallback(
+            @NonNull PipTransitionCallback callback, @NonNull Executor executor) {
+        mPipTransitionCallbacks.put(callback, executor);
     }
 
     protected void sendOnPipTransitionStarted(
             @PipAnimationController.TransitionDirection int direction) {
         final Rect pipBounds = mPipBoundsState.getBounds();
-        for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
-            final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
-            callback.onPipTransitionStarted(direction, pipBounds);
+        for (Map.Entry<PipTransitionCallback, Executor> entry
+                : mPipTransitionCallbacks.entrySet()) {
+            entry.getValue().execute(
+                    () -> entry.getKey().onPipTransitionStarted(direction, pipBounds));
         }
         if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) {
             try {
@@ -207,9 +210,10 @@
 
     protected void sendOnPipTransitionFinished(
             @PipAnimationController.TransitionDirection int direction) {
-        for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
-            final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
-            callback.onPipTransitionFinished(direction);
+        for (Map.Entry<PipTransitionCallback, Executor> entry
+                : mPipTransitionCallbacks.entrySet()) {
+            entry.getValue().execute(
+                    () -> entry.getKey().onPipTransitionFinished(direction));
         }
         if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) {
             try {
@@ -226,9 +230,10 @@
 
     protected void sendOnPipTransitionCancelled(
             @PipAnimationController.TransitionDirection int direction) {
-        for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
-            final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
-            callback.onPipTransitionCanceled(direction);
+        for (Map.Entry<PipTransitionCallback, Executor> entry
+                : mPipTransitionCallbacks.entrySet()) {
+            entry.getValue().execute(
+                    () -> entry.getKey().onPipTransitionCanceled(direction));
         }
     }
 
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 8c4bf76..5d1b4da 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
@@ -106,6 +106,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -487,7 +488,7 @@
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(),
                 INPUT_CONSUMER_PIP, mMainExecutor);
-        mPipTransitionController.registerPipTransitionCallback(this);
+        mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor);
         mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> {
             mPipDisplayLayoutState.setDisplayId(displayId);
             onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
@@ -1229,8 +1230,11 @@
         }
 
         @Override
-        public PipTransitionController getPipTransitionController() {
-            return mPipTransitionController;
+        public void registerPipTransitionCallback(
+                PipTransitionController.PipTransitionCallback callback,
+                Executor executor) {
+            mMainExecutor.execute(() -> mPipTransitionController.registerPipTransitionCallback(
+                    callback, executor));
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index ef46843..f5bd006 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -38,6 +38,7 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.FloatProperties;
 import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.common.pip.PipBoundsState;
@@ -47,6 +48,7 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.shared.animation.PhysicsAnimator;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 import kotlin.Unit;
 import kotlin.jvm.functions.Function0;
@@ -171,7 +173,9 @@
         public void onPipTransitionCanceled(int direction) {}
     };
 
-    public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState,
+    public PipMotionHelper(Context context,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @NonNull PipBoundsState pipBoundsState,
             PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController,
             PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController,
             FloatingContentCoordinator floatingContentCoordinator,
@@ -183,7 +187,7 @@
         mSnapAlgorithm = snapAlgorithm;
         mFloatingContentCoordinator = floatingContentCoordinator;
         mPipPerfHintController = pipPerfHintControllerOptional.orElse(null);
-        pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback);
+        pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback, mainExecutor);
         mResizePipUpdateListener = (target, values) -> {
             if (mPipBoundsState.getMotionBoundsState().isInMotion()) {
                 mPipTaskOrganizer.scheduleUserResizePip(getBounds(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 3d28646..b6a7c56 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -257,7 +257,7 @@
     }
 
     private void onInit() {
-        mPipTransitionController.registerPipTransitionCallback(this);
+        mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor);
 
         reloadResources();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
index 7c5f10a..8ee72b4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
@@ -76,21 +76,40 @@
                     continue
                 }
 
+                // Filter out changes that we care about
                 if (change.mode == WindowManager.TRANSIT_OPEN) {
                     change.taskInfo?.let { taskInfoList.add(it) }
                     transitionTypeList.add(change.mode)
                 }
             }
-            transitionToTransitionChanges.put(
-                transition,
-                TransitionChanges(taskInfoList, transitionTypeList)
-            )
+            // Only add the transition to map if it has a change we care about
+            if (taskInfoList.isNotEmpty()) {
+                transitionToTransitionChanges.put(
+                    transition,
+                    TransitionChanges(taskInfoList, transitionTypeList)
+                )
+            }
         }
     }
 
     override fun onTransitionStarting(transition: IBinder) {}
 
-    override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
+    override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
+        val mergedTransitionChanges =
+            transitionToTransitionChanges.get(merged)
+                ?:
+                // We are adding changes of the merged transition to changes of the playing
+                // transition so if there is no changes nothing to do.
+                return
+
+        transitionToTransitionChanges.remove(merged)
+        val playingTransitionChanges = transitionToTransitionChanges.get(playing)
+        if (playingTransitionChanges != null) {
+            playingTransitionChanges.merge(mergedTransitionChanges)
+        } else {
+            transitionToTransitionChanges.put(playing, mergedTransitionChanges)
+        }
+    }
 
     override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
         val taskInfoList =
@@ -138,6 +157,11 @@
 
     private data class TransitionChanges(
         val taskInfoList: MutableList<RunningTaskInfo> = ArrayList(),
-        val transitionTypeList: MutableList<Int> = ArrayList()
-    )
+        val transitionTypeList: MutableList<Int> = ArrayList(),
+    ) {
+        fun merge(transitionChanges: TransitionChanges) {
+            taskInfoList.addAll(transitionChanges.taskInfoList)
+            transitionTypeList.addAll(transitionChanges.transitionTypeList)
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 9412b2b..9db153f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -53,6 +53,7 @@
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
+import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN;
@@ -944,12 +945,15 @@
     }
 
     private static int getWallpaperTransitType(TransitionInfo info) {
+        boolean hasWallpaper = false;
         boolean hasOpenWallpaper = false;
         boolean hasCloseWallpaper = false;
 
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
-            if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0) {
+            if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0
+                    || (change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
+                hasWallpaper = true;
                 if (TransitionUtil.isOpeningType(change.getMode())) {
                     hasOpenWallpaper = true;
                 } else if (TransitionUtil.isClosingType(change.getMode())) {
@@ -965,6 +969,8 @@
             return WALLPAPER_TRANSITION_OPEN;
         } else if (hasCloseWallpaper) {
             return WALLPAPER_TRANSITION_CLOSE;
+        } else if (hasWallpaper) {
+            return WALLPAPER_TRANSITION_CHANGE;
         } else {
             return WALLPAPER_TRANSITION_NONE;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index e1009a0..180e4f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -26,7 +26,6 @@
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_HOVER_ENTER;
 import static android.view.MotionEvent.ACTION_HOVER_EXIT;
-import static android.view.MotionEvent.ACTION_HOVER_MOVE;
 import static android.view.MotionEvent.ACTION_MOVE;
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.WindowInsets.Type.statusBars;
@@ -103,6 +102,7 @@
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener;
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
+import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
 
 import java.io.PrintWriter;
 import java.util.Objects;
@@ -383,10 +383,32 @@
         mWindowDecorByTaskId.remove(taskInfo.taskId);
     }
 
+    private void onMaximizeOrRestore(int taskId, String tag) {
+        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+        if (decoration == null) {
+            return;
+        }
+        InteractionJankMonitorUtils.beginTracing(
+                Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext, decoration.mTaskSurface, tag);
+        mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
+        decoration.closeHandleMenu();
+        decoration.closeMaximizeMenu();
+    }
+
+    private void onSnapResize(int taskId, boolean left) {
+        final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+        if (decoration == null) {
+            return;
+        }
+        mDesktopTasksController.snapToHalfScreen(decoration.mTaskInfo,
+                left ? SnapPosition.LEFT : SnapPosition.RIGHT);
+        decoration.closeHandleMenu();
+        decoration.closeMaximizeMenu();
+    }
+
     private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
             implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
             View.OnGenericMotionListener, DragDetector.MotionEventHandler {
-        private static final int CLOSE_MAXIMIZE_MENU_DELAY_MS = 150;
 
         private final int mTaskId;
         private final WindowContainerToken mTaskToken;
@@ -405,7 +427,6 @@
         private boolean mTouchscreenInUse;
         private boolean mHasLongClicked;
         private int mDragPointerId = -1;
-        private final Runnable mCloseMaximizeWindowRunnable;
 
         private DesktopModeTouchEventListener(
                 RunningTaskInfo taskInfo,
@@ -416,11 +437,6 @@
             mDragDetector = new DragDetector(this);
             mGestureDetector = new GestureDetector(mContext, this);
             mDisplayId = taskInfo.displayId;
-            mCloseMaximizeWindowRunnable = () -> {
-                final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
-                if (decoration == null) return;
-                decoration.closeMaximizeMenu();
-            };
         }
 
         @Override
@@ -472,31 +488,12 @@
             } else if (id == R.id.collapse_menu_button) {
                 decoration.closeHandleMenu();
             } else if (id == R.id.maximize_window) {
-                InteractionJankMonitorUtils.beginTracing(
-                        Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
-                        /* tag= */ "caption_bar_button");
-                final RunningTaskInfo taskInfo = decoration.mTaskInfo;
-                decoration.closeHandleMenu();
-                decoration.closeMaximizeMenu();
-                mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
-            } else if (id == R.id.maximize_menu_maximize_button) {
-                InteractionJankMonitorUtils.beginTracing(
-                        Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
-                        /* tag= */ "maximize_menu_option");
-                final RunningTaskInfo taskInfo = decoration.mTaskInfo;
-                mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
-                decoration.closeHandleMenu();
-                decoration.closeMaximizeMenu();
-            } else if (id == R.id.maximize_menu_snap_left_button) {
-                final RunningTaskInfo taskInfo = decoration.mTaskInfo;
-                mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.LEFT);
-                decoration.closeHandleMenu();
-                decoration.closeMaximizeMenu();
-            } else if (id == R.id.maximize_menu_snap_right_button) {
-                final RunningTaskInfo taskInfo = decoration.mTaskInfo;
-                mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.RIGHT);
-                decoration.closeHandleMenu();
-                decoration.closeMaximizeMenu();
+                // TODO(b/346441962): move click detection logic into the decor's
+                //  {@link AppHeaderViewHolder}. Let it encapsulate the that and have it report
+                //  back to the decoration using
+                //  {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which
+                //  should shared with the maximize menu's maximize/restore actions.
+                onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
             }
         }
 
@@ -578,40 +575,26 @@
             return false;
         }
 
+        /**
+         * TODO(b/346441962): move this hover detection logic into the decor's
+         * {@link AppHeaderViewHolder}.
+         */
         @Override
         public boolean onGenericMotion(View v, MotionEvent ev) {
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             final int id = v.getId();
-            if (ev.getAction() == ACTION_HOVER_ENTER) {
-                if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
-                    decoration.onMaximizeWindowHoverEnter();
-                } else if (id == R.id.maximize_window
-                        || MaximizeMenu.Companion.isMaximizeMenuView(id)) {
-                    // Re-hovering over any of the maximize menu views should keep the menu open by
-                    // cancelling any attempts to close the menu.
-                    mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
-                    if (id != R.id.maximize_window) {
-                        decoration.onMaximizeMenuHoverEnter(id, ev);
-                    }
+            if (ev.getAction() == ACTION_HOVER_ENTER && id == R.id.maximize_window) {
+                decoration.setAppHeaderMaximizeButtonHovered(true);
+                if (!decoration.isMaximizeMenuActive()) {
+                    decoration.onMaximizeButtonHoverEnter();
                 }
                 return true;
-            } else if (ev.getAction() == ACTION_HOVER_MOVE
-                    && MaximizeMenu.Companion.isMaximizeMenuView(id)) {
-                decoration.onMaximizeMenuHoverMove(id, ev);
-                mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
-            } else if (ev.getAction() == ACTION_HOVER_EXIT) {
-                if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
-                    decoration.onMaximizeWindowHoverExit();
-                } else if (id == R.id.maximize_window
-                        || MaximizeMenu.Companion.isMaximizeMenuView(id)) {
-                    // Close menu if not hovering over maximize menu or maximize button after a
-                    // delay to give user a chance to re-enter view or to move from one maximize
-                    // menu view to another.
-                    mMainHandler.postDelayed(mCloseMaximizeWindowRunnable,
-                            CLOSE_MAXIMIZE_MENU_DELAY_MS);
-                    if (id != R.id.maximize_window) {
-                        decoration.onMaximizeMenuHoverExit(id, ev);
-                    }
+            }
+            if (ev.getAction() == ACTION_HOVER_EXIT && id == R.id.maximize_window) {
+                decoration.setAppHeaderMaximizeButtonHovered(false);
+                decoration.onMaximizeHoverStateChanged();
+                if (!decoration.isMaximizeMenuActive()) {
+                    decoration.onMaximizeButtonHoverExit();
                 }
                 return true;
             }
@@ -719,11 +702,7 @@
                     && action != MotionEvent.ACTION_CANCEL)) {
                 return false;
             }
-            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
-            InteractionJankMonitorUtils.beginTracing(
-                    Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext,
-                    /* surface= */ decoration.mTaskSurface, /* tag= */ "double_tap");
-            mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
+            onMaximizeOrRestore(mTaskId, "double_tap");
             return true;
         }
     }
@@ -1105,7 +1084,13 @@
 
         final DesktopModeTouchEventListener touchEventListener =
                 new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback);
-
+        windowDecoration.setOnMaximizeOrRestoreClickListener(this::onMaximizeOrRestore);
+        windowDecoration.setOnLeftSnapClickListener((taskId, tag) -> {
+            onSnapResize(taskId, true /* isLeft */);
+        });
+        windowDecoration.setOnRightSnapClickListener((taskId, tag) -> {
+            onSnapResize(taskId, false /* isLeft */);
+        });
         windowDecoration.setCaptionListeners(
                 touchEventListener, touchEventListener, touchEventListener, touchEventListener);
         windowDecoration.setExclusionRegionListener(mExclusionRegionListener);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 4d597ca..f53c21d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -69,6 +69,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.shared.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
 import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder;
 import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
@@ -87,6 +88,9 @@
 public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
     private static final String TAG = "DesktopModeWindowDecoration";
 
+    @VisibleForTesting
+    static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L;
+
     private final Handler mHandler;
     private final Choreographer mChoreographer;
     private final SyncTransactionQueue mSyncQueue;
@@ -96,6 +100,9 @@
     private View.OnTouchListener mOnCaptionTouchListener;
     private View.OnLongClickListener mOnCaptionLongClickListener;
     private View.OnGenericMotionListener mOnCaptionGenericMotionListener;
+    private OnTaskActionClickListener mOnMaximizeOrRestoreClickListener;
+    private OnTaskActionClickListener mOnLeftSnapClickListener;
+    private OnTaskActionClickListener mOnRightSnapClickListener;
     private DragPositioningCallback mDragPositioningCallback;
     private DragResizeInputListener mDragResizeListener;
     private DragDetector mDragDetector;
@@ -120,6 +127,16 @@
     private ExclusionRegionListener mExclusionRegionListener;
 
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+    private final MaximizeMenuFactory mMaximizeMenuFactory;
+
+    // Hover state for the maximize menu and button. The menu will remain open as long as either of
+    // these is true. See {@link #onMaximizeHoverStateChanged()}.
+    private boolean mIsAppHeaderMaximizeButtonHovered = false;
+    private boolean mIsMaximizeMenuHovered = false;
+    // Used to schedule the closing of the maximize menu when neither of the button or menu are
+    // being hovered. There's a small delay after stopping the hover, to allow a quick reentry
+    // to cancel the close.
+    private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu;
 
     DesktopModeWindowDecoration(
             Context context,
@@ -135,7 +152,8 @@
                 handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
                 SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
                 WindowContainerTransaction::new, SurfaceControl::new,
-                new SurfaceControlViewHostFactory() {});
+                new SurfaceControlViewHostFactory() {},
+                DefaultMaximizeMenuFactory.INSTANCE);
     }
 
     DesktopModeWindowDecoration(
@@ -152,7 +170,8 @@
             Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
             Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
             Supplier<SurfaceControl> surfaceControlSupplier,
-            SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
+            SurfaceControlViewHostFactory surfaceControlViewHostFactory,
+            MaximizeMenuFactory maximizeMenuFactory) {
         super(context, displayController, taskOrganizer, taskInfo, taskSurface,
                 surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                 windowContainerTransactionSupplier, surfaceControlSupplier,
@@ -161,6 +180,31 @@
         mChoreographer = choreographer;
         mSyncQueue = syncQueue;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+        mMaximizeMenuFactory = maximizeMenuFactory;
+    }
+
+    /**
+     * Register a listener to be called back when one of the tasks' maximize/restore action is
+     * triggered.
+     * TODO(b/346441962): hook this up to double-tap and the header's maximize button, instead of
+     *  having the ViewModel deal with parsing motion events.
+     */
+    void setOnMaximizeOrRestoreClickListener(OnTaskActionClickListener listener) {
+        mOnMaximizeOrRestoreClickListener = listener;
+    }
+
+    /**
+     * Register a listener to be called back when one of the tasks snap-left action is triggered.
+     */
+    void setOnLeftSnapClickListener(OnTaskActionClickListener listener) {
+        mOnLeftSnapClickListener = listener;
+    }
+
+    /**
+     * Register a listener to be called back when one of the tasks' snap-right action is triggered.
+     */
+    void setOnRightSnapClickListener(OnTaskActionClickListener listener) {
+        mOnRightSnapClickListener = listener;
     }
 
     void setCaptionListeners(
@@ -714,11 +758,41 @@
      * Create and display maximize menu window
      */
     void createMaximizeMenu() {
-        mMaximizeMenu = new MaximizeMenu(mSyncQueue, mRootTaskDisplayAreaOrganizer,
-                mDisplayController, mTaskInfo, mOnCaptionButtonClickListener,
-                mOnCaptionGenericMotionListener, mOnCaptionTouchListener, mContext,
+        mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer,
+                mDisplayController, mTaskInfo, mContext,
                 calculateMaximizeMenuPosition(), mSurfaceControlTransactionSupplier);
-        mMaximizeMenu.show();
+        mMaximizeMenu.show(
+                mOnMaximizeOrRestoreClickListener,
+                mOnLeftSnapClickListener,
+                mOnRightSnapClickListener,
+                hovered -> {
+                    mIsMaximizeMenuHovered = hovered;
+                    onMaximizeHoverStateChanged();
+                    return null;
+                }
+        );
+    }
+
+    /** Set whether the app header's maximize button is hovered. */
+    void setAppHeaderMaximizeButtonHovered(boolean hovered) {
+        mIsAppHeaderMaximizeButtonHovered = hovered;
+        onMaximizeHoverStateChanged();
+    }
+
+    /**
+     * Called when either one of the maximize button in the app header or the maximize menu has
+     * changed its hover state.
+     */
+    void onMaximizeHoverStateChanged() {
+        if (!mIsMaximizeMenuHovered && !mIsAppHeaderMaximizeButtonHovered) {
+            // Neither is hovered, close the menu.
+            if (isMaximizeMenuActive()) {
+                mHandler.postDelayed(mCloseMaximizeWindowRunnable, CLOSE_MAXIMIZE_MENU_DELAY_MS);
+            }
+            return;
+        }
+        // At least one of the two is hovered, cancel the close if needed.
+        mHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
     }
 
     /**
@@ -992,34 +1066,22 @@
                 .setAnimatingTaskResize(animatingTaskResize);
     }
 
-    /** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */
-    void onMaximizeWindowHoverExit() {
+    /**
+     * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button.
+     */
+    void onMaximizeButtonHoverExit() {
         ((AppHeaderViewHolder) mWindowDecorViewHolder)
                 .onMaximizeWindowHoverExit();
     }
 
-    /** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */
-    void onMaximizeWindowHoverEnter() {
+    /**
+     * Called when there is a {@link MotionEvent#ACTION_HOVER_ENTER} on the maximize window button.
+     */
+    void onMaximizeButtonHoverEnter() {
         ((AppHeaderViewHolder) mWindowDecorViewHolder)
                 .onMaximizeWindowHoverEnter();
     }
 
-    /** Called when there is a {@Link ACTION_HOVER_ENTER} on a view in the maximize menu. */
-    void onMaximizeMenuHoverEnter(int id, MotionEvent ev) {
-        mMaximizeMenu.onMaximizeMenuHoverEnter(id, ev);
-    }
-
-    /** Called when there is a {@Link ACTION_HOVER_MOVE} on a view in the maximize menu. */
-    void onMaximizeMenuHoverMove(int id, MotionEvent ev) {
-        mMaximizeMenu.onMaximizeMenuHoverMove(id, ev);
-    }
-
-    /** Called when there is a {@Link ACTION_HOVER_EXIT} on a view in the maximize menu. */
-    void onMaximizeMenuHoverExit(int id, MotionEvent ev) {
-        mMaximizeMenu.onMaximizeMenuHoverExit(id, ev);
-    }
-
-
     @Override
     public String toString() {
         return "{"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 0470367..5f9f8d6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -20,7 +20,6 @@
 import android.animation.ObjectAnimator
 import android.animation.ValueAnimator
 import android.annotation.ColorInt
-import android.annotation.IdRes
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
 import android.content.res.ColorStateList
@@ -28,6 +27,7 @@
 import android.graphics.Paint
 import android.graphics.PixelFormat
 import android.graphics.PointF
+import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.LayerDrawable
@@ -37,16 +37,17 @@
 import android.util.StateSet
 import android.view.LayoutInflater
 import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_HOVER_ENTER
+import android.view.MotionEvent.ACTION_HOVER_EXIT
+import android.view.MotionEvent.ACTION_HOVER_MOVE
 import android.view.SurfaceControl
 import android.view.SurfaceControl.Transaction
 import android.view.SurfaceControlViewHost
 import android.view.View
-import android.view.View.OnClickListener
-import android.view.View.OnGenericMotionListener
-import android.view.View.OnTouchListener
 import android.view.View.SCALE_Y
 import android.view.View.TRANSLATION_Y
 import android.view.View.TRANSLATION_Z
+import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
 import android.widget.Button
@@ -64,10 +65,10 @@
 import com.android.wm.shell.windowdecor.common.DecorThemeUtil
 import com.android.wm.shell.windowdecor.common.OPACITY_12
 import com.android.wm.shell.windowdecor.common.OPACITY_40
+import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener
 import com.android.wm.shell.windowdecor.common.withAlpha
 import java.util.function.Supplier
 
-
 /**
  *  Menu that appears when user long clicks the maximize button. Gives the user the option to
  *  maximize the task or snap the task to the right or left half of the screen.
@@ -77,9 +78,6 @@
         private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
         private val displayController: DisplayController,
         private val taskInfo: RunningTaskInfo,
-        private val onClickListener: OnClickListener,
-        private val onGenericMotionListener: OnGenericMotionListener,
-        private val onTouchListener: OnTouchListener,
         private val decorWindowContext: Context,
         private val menuPosition: PointF,
         private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }
@@ -102,9 +100,19 @@
     }
 
     /** Creates and shows the maximize window. */
-    fun show() {
+    fun show(
+        onMaximizeClickListener: OnTaskActionClickListener,
+        onLeftSnapClickListener: OnTaskActionClickListener,
+        onRightSnapClickListener: OnTaskActionClickListener,
+        onHoverListener: (Boolean) -> Unit
+    ) {
         if (maximizeMenu != null) return
-        createMaximizeMenu()
+        createMaximizeMenu(
+            onMaximizeClickListener = onMaximizeClickListener,
+            onLeftSnapClickListener = onLeftSnapClickListener,
+            onRightSnapClickListener = onRightSnapClickListener,
+            onHoverListener = onHoverListener
+        )
         maximizeMenuView?.animateOpenMenu()
     }
 
@@ -117,7 +125,12 @@
     }
 
     /** Create a maximize menu that is attached to the display area. */
-    private fun createMaximizeMenu() {
+    private fun createMaximizeMenu(
+        onMaximizeClickListener: OnTaskActionClickListener,
+        onLeftSnapClickListener: OnTaskActionClickListener,
+        onRightSnapClickListener: OnTaskActionClickListener,
+        onHoverListener: (Boolean) -> Unit
+    ) {
         val t = transactionSupplier.get()
         val builder = SurfaceControl.Builder()
         rootTdaOrganizer.attachToDisplayArea(taskInfo.displayId, builder)
@@ -146,11 +159,19 @@
             context = decorWindowContext,
             menuHeight = menuHeight,
             menuPadding = menuPadding,
-            onClickListener = onClickListener,
-            onTouchListener = onTouchListener,
-            onGenericMotionListener = onGenericMotionListener,
         ).also { menuView ->
+            val taskId = taskInfo.taskId
             menuView.bind(taskInfo)
+            menuView.onMaximizeClickListener = {
+                onMaximizeClickListener.onClick(taskId, "maximize_menu_option")
+            }
+            menuView.onLeftSnapClickListener = {
+                onLeftSnapClickListener.onClick(taskId, "left_snap_option")
+            }
+            menuView.onRightSnapClickListener = {
+                onRightSnapClickListener.onClick(taskId, "right_snap_option")
+            }
+            menuView.onMenuHoverListener = onHoverListener
             viewHost.setView(menuView.rootView, lp)
         }
 
@@ -198,56 +219,6 @@
     }
 
     /**
-     * Called when a [MotionEvent.ACTION_HOVER_ENTER] is triggered on any of the menu's views.
-     *
-     * TODO(b/346440693): this is only needed for the left/right snap options that don't support
-     *  selector states to manage its hover state. Look into whether that can be added to avoid
-     *  manually tracking hover enter/exit motion events. Also because those button colors/states
-     *  aren't updating correctly for pressed, focused and selected states.
-     *  See also [onMaximizeMenuHoverMove] and [onMaximizeMenuHoverExit].
-     */
-    fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) {
-        setSnapButtonsColorOnHover(viewId, ev)
-    }
-
-    /** Called when a [MotionEvent.ACTION_HOVER_MOVE] is triggered on any of the menu's views. */
-    fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) {
-        setSnapButtonsColorOnHover(viewId, ev)
-    }
-
-    /** Called when a [MotionEvent.ACTION_HOVER_EXIT] is triggered on any of the menu's views. */
-    fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) {
-        val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return
-        val snapOptionsHeight = maximizeMenuView?.snapOptionsHeight ?: return
-        val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapOptionsWidth &&
-                ev.y >= 0 && ev.y <= snapOptionsHeight
-
-        if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) {
-            // After exiting the snap menu layout area, checks to see that user is not still
-            // hovering within the snap menu layout bounds which would indicate that the user is
-            // hovering over a snap button within the snap menu layout rather than having exited.
-            maximizeMenuView?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.NONE)
-        }
-    }
-
-    private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) {
-        val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return
-        val snapMenuCenter = snapOptionsWidth / 2
-        when {
-            viewId == R.id.maximize_menu_snap_left_button ||
-                    (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter) -> {
-                        maximizeMenuView
-                            ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.LEFT)
-            }
-            viewId == R.id.maximize_menu_snap_right_button ||
-                    (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter) -> {
-                        maximizeMenuView
-                            ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.RIGHT)
-                    }
-        }
-    }
-
-    /**
      * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for
      * resizing a Task.
      */
@@ -255,12 +226,11 @@
         context: Context,
         private val menuHeight: Int,
         private val menuPadding: Int,
-        onClickListener: OnClickListener,
-        onTouchListener: OnTouchListener,
-        onGenericMotionListener: OnGenericMotionListener,
     ) {
-        val rootView: View = LayoutInflater.from(context)
-            .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */)
+        val rootView = LayoutInflater.from(context)
+            .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) as ViewGroup
+        private val container = requireViewById(R.id.container)
+        private val overlay = requireViewById(R.id.maximize_menu_overlay)
         private val maximizeText =
             requireViewById(R.id.maximize_menu_maximize_window_text) as TextView
         private val maximizeButton =
@@ -285,30 +255,63 @@
         private val fillRadius = context.resources
             .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius)
 
+        private val hoverTempRect = Rect()
         private val openMenuAnimatorSet = AnimatorSet()
         private lateinit var taskInfo: RunningTaskInfo
         private lateinit var style: MenuStyle
 
-        /** The width of the snap menu option view, including both left and right snaps. */
-        val snapOptionsWidth: Int
-            get() = snapButtonsLayout.width
-        /** The height of the snap menu option view, including both left and right snaps .*/
-        val snapOptionsHeight: Int
-            get() = snapButtonsLayout.height
+        /** Invoked when the maximize or restore option is clicked. */
+        var onMaximizeClickListener: (() -> Unit)? = null
+        /** Invoked when the left snap option is clicked. */
+        var onLeftSnapClickListener: (() -> Unit)? = null
+        /** Invoked when the right snap option is clicked. */
+        var onRightSnapClickListener: (() -> Unit)? = null
+        /** Invoked whenever the hover state of the menu changes. */
+        var onMenuHoverListener: ((Boolean) -> Unit)? = null
 
         init {
-            // TODO(b/346441962): encapsulate menu hover enter/exit logic inside this class and
-            //  expose only what  is actually relevant to outside classes so that specific checks
-            //  against resource IDs aren't needed outside this class.
-            rootView.setOnGenericMotionListener(onGenericMotionListener)
-            rootView.setOnTouchListener(onTouchListener)
-            maximizeButton.setOnClickListener(onClickListener)
-            maximizeButton.setOnGenericMotionListener(onGenericMotionListener)
-            snapRightButton.setOnClickListener(onClickListener)
-            snapRightButton.setOnGenericMotionListener(onGenericMotionListener)
-            snapLeftButton.setOnClickListener(onClickListener)
-            snapLeftButton.setOnGenericMotionListener(onGenericMotionListener)
-            snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener)
+            overlay.setOnHoverListener { _, event ->
+                // The overlay covers the entire menu, so it's a convenient way to monitor whether
+                // the menu is hovered as a whole or not.
+                when (event.action) {
+                    ACTION_HOVER_ENTER -> onMenuHoverListener?.invoke(true)
+                    ACTION_HOVER_EXIT -> onMenuHoverListener?.invoke(false)
+                }
+
+                // Also check if the hover falls within the snap options layout, to manually
+                // set the left/right state based on the event's position.
+                // TODO(b/346440693): this manual hover tracking is needed for left/right snap
+                //  because its view/background(s) don't support selector states. Look into whether
+                //  that can be added to avoid manual tracking. Also because these button
+                //  colors/state logic is only being applied on hover events, but there's pressed,
+                //  focused and selected states that should be responsive too.
+                val snapLayoutBoundsRelToOverlay = hoverTempRect.also { rect ->
+                    snapButtonsLayout.getDrawingRect(rect)
+                    rootView.offsetDescendantRectToMyCoords(snapButtonsLayout, rect)
+                }
+                if (event.action == ACTION_HOVER_ENTER || event.action == ACTION_HOVER_MOVE) {
+                    if (snapLayoutBoundsRelToOverlay.contains(event.x.toInt(), event.y.toInt())) {
+                        // Hover is inside the snap layout, anything left of center is the left
+                        // snap, and anything right of center is right snap.
+                        val layoutCenter = snapLayoutBoundsRelToOverlay.centerX()
+                        if (event.x < layoutCenter) {
+                            updateSplitSnapSelection(SnapToHalfSelection.LEFT)
+                        } else {
+                            updateSplitSnapSelection(SnapToHalfSelection.RIGHT)
+                        }
+                    } else {
+                        // Any other hover is outside the snap layout, so neither is selected.
+                        updateSplitSnapSelection(SnapToHalfSelection.NONE)
+                    }
+                }
+
+                // Don't consume the event to allow child views to receive the event too.
+                return@setOnHoverListener false
+            }
+
+            maximizeButton.setOnClickListener { onMaximizeClickListener?.invoke() }
+            snapRightButton.setOnClickListener { onRightSnapClickListener?.invoke() }
+            snapLeftButton.setOnClickListener { onLeftSnapClickListener?.invoke() }
 
             // To prevent aliasing.
             maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
@@ -351,7 +354,7 @@
                             val value = animatedValue as Float
                             val topPadding = menuPadding -
                                     ((1 - value) * menuHeight).toInt()
-                            rootView.setPadding(menuPadding, topPadding,
+                            container.setPadding(menuPadding, topPadding,
                                 menuPadding, menuPadding)
                         }
                     },
@@ -410,7 +413,7 @@
         }
 
         /** Update the view state to a new snap to half selection. */
-        fun updateSplitSnapSelection(selection: SnapToHalfSelection) {
+        private fun updateSplitSnapSelection(selection: SnapToHalfSelection) {
             when (selection) {
                 SnapToHalfSelection.NONE -> deactivateSnapOptions()
                 SnapToHalfSelection.LEFT -> activateSnapOption(activateLeft = true)
@@ -638,13 +641,41 @@
         private const val ELEVATION_ANIMATION_DURATION_MS = 50L
         private const val CONTROLS_ALPHA_ANIMATION_DELAY_MS = 33L
         private const val MENU_Z_TRANSLATION = 1f
-        fun isMaximizeMenuView(@IdRes viewId: Int): Boolean {
-            return viewId == R.id.maximize_menu ||
-                    viewId == R.id.maximize_menu_maximize_button ||
-                    viewId == R.id.maximize_menu_snap_left_button ||
-                    viewId == R.id.maximize_menu_snap_right_button ||
-                    viewId == R.id.maximize_menu_snap_menu_layout ||
-                    viewId == R.id.maximize_menu_snap_menu_layout
-        }
+    }
+}
+
+/** A factory interface to create a [MaximizeMenu]. */
+interface MaximizeMenuFactory {
+    fun create(
+        syncQueue: SyncTransactionQueue,
+        rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
+        displayController: DisplayController,
+        taskInfo: RunningTaskInfo,
+        decorWindowContext: Context,
+        menuPosition: PointF,
+        transactionSupplier: Supplier<Transaction>
+    ): MaximizeMenu
+}
+
+/** A [MaximizeMenuFactory] implementation that creates a [MaximizeMenu].  */
+object DefaultMaximizeMenuFactory : MaximizeMenuFactory {
+    override fun create(
+        syncQueue: SyncTransactionQueue,
+        rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
+        displayController: DisplayController,
+        taskInfo: RunningTaskInfo,
+        decorWindowContext: Context,
+        menuPosition: PointF,
+        transactionSupplier: Supplier<Transaction>
+    ): MaximizeMenu {
+        return MaximizeMenu(
+            syncQueue,
+            rootTdaOrganizer,
+            displayController,
+            taskInfo,
+            decorWindowContext,
+            menuPosition,
+            transactionSupplier
+        )
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt
new file mode 100644
index 0000000..14b9e7f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.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.wm.shell.windowdecor.common
+
+/** A callback to be invoked when a Task's window decor element is clicked. */
+fun interface OnTaskActionClickListener {
+    /**
+     * Called when a task's decor element has been clicked.
+     *
+     * @param taskId the id of the task.
+     * @param tag a readable identifier for the element.
+     */
+    fun onClick(taskId: Int, tag: String)
+}
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 14fa0f1..0e53e10 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
@@ -18,6 +18,7 @@
 
 import android.app.ActivityManager.RecentTaskInfo
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.KeyguardManager
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -149,6 +150,7 @@
   @Mock lateinit var syncQueue: SyncTransactionQueue
   @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
   @Mock lateinit var transitions: Transitions
+  @Mock lateinit var keyguardManager: KeyguardManager
   @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
   @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
   @Mock
@@ -233,6 +235,7 @@
         rootTaskDisplayAreaOrganizer,
         dragAndDropController,
         transitions,
+        keyguardManager,
         enterDesktopTransitionHandler,
         exitDesktopTransitionHandler,
         toggleResizeDesktopTaskTransitionHandler,
@@ -1301,6 +1304,17 @@
   }
 
   @Test
+  fun handleRequest_freeformTask_keyguardLocked_returnNull() {
+    assumeTrue(ENABLE_SHELL_TRANSITIONS)
+    whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
+    val freeformTask = createFreeformTask(displayId = DEFAULT_DISPLAY)
+
+    val result = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+    assertNull(result, "Should NOT handle request")
+  }
+
+  @Test
   fun handleRequest_notOpenOrToFrontTransition_returnNull() {
     assumeTrue(ENABLE_SHELL_TRANSITIONS)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index d38fc6c..38e741a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -183,7 +183,7 @@
 
     @Test
     public void instantiatePipController_registersPipTransitionCallback() {
-        verify(mMockPipTransitionController).registerPipTransitionCallback(any());
+        verify(mMockPipTransitionController).registerPipTransitionCallback(any(), any());
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index ace09a8..66f8c0b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -114,8 +114,8 @@
         final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext,
                 mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm, mPipDisplayLayoutState,
                 mSizeSpecSource);
-        final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState,
-                mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm,
+        final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mMainExecutor,
+                mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm,
                 mMockPipTransitionController, mFloatingContentCoordinator,
                 Optional.empty() /* pipPerfHintControllerOptional */);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 92762fa..6d18e36 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -116,8 +116,8 @@
         mPipSnapAlgorithm = new PipSnapAlgorithm();
         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm,
                 new PipKeepClearAlgorithmInterface() {}, mPipDisplayLayoutState, mSizeSpecSource);
-        PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState,
-                mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
+        PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mMainExecutor,
+                mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
                 mMockPipTransitionController, mFloatingContentCoordinator,
                 Optional.empty() /* pipPerfHintControllerOptional */);
         mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
index f959970..0e5efa6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
@@ -48,7 +48,6 @@
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
-
 /**
  * Test class for {@link TaskStackTransitionObserver}
  *
@@ -168,6 +167,80 @@
             .isEqualTo(freeformOpenChange.taskInfo?.windowingMode)
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    fun transitionMerged_withChange_onlyOpenChangeIsNotified() {
+        val listener = TestListener()
+        val executor = TestShellExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Create open transition
+        val change =
+            createChange(
+                WindowManager.TRANSIT_OPEN,
+                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+            )
+        val transitionInfo =
+            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+
+        // create change transition to be merged to above transition
+        val mergedChange =
+            createChange(
+                WindowManager.TRANSIT_CHANGE,
+                createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+            )
+        val mergedTransitionInfo =
+            TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0).addChange(mergedChange).build()
+        val mergedTransition = Mockito.mock(IBinder::class.java)
+
+        callOnTransitionReady(transitionInfo)
+        callOnTransitionReady(mergedTransitionInfo, mergedTransition)
+        callOnTransitionMerged(mergedTransition)
+        callOnTransitionFinished()
+        executor.flushAll()
+
+        assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId)
+        assertThat(listener.taskInfoToBeNotified.windowingMode)
+            .isEqualTo(change.taskInfo?.windowingMode)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    fun transitionMerged_withOpen_lastOpenChangeIsNotified() {
+        val listener = TestListener()
+        val executor = TestShellExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Create open transition
+        val change =
+            createChange(
+                WindowManager.TRANSIT_OPEN,
+                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+            )
+        val transitionInfo =
+            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+
+        // create change transition to be merged to above transition
+        val mergedChange =
+            createChange(
+                WindowManager.TRANSIT_OPEN,
+                createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+            )
+        val mergedTransitionInfo =
+            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(mergedChange).build()
+        val mergedTransition = Mockito.mock(IBinder::class.java)
+
+        callOnTransitionReady(transitionInfo)
+        callOnTransitionReady(mergedTransitionInfo, mergedTransition)
+        callOnTransitionMerged(mergedTransition)
+        callOnTransitionFinished()
+        executor.flushAll()
+
+        assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(mergedChange.taskInfo?.taskId)
+        assertThat(listener.taskInfoToBeNotified.windowingMode)
+                .isEqualTo(mergedChange.taskInfo?.windowingMode)
+    }
+
     class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener {
         var taskInfoToBeNotified = ActivityManager.RunningTaskInfo()
 
@@ -179,11 +252,14 @@
     }
 
     /** Simulate calling the onTransitionReady() method */
-    private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+    private fun callOnTransitionReady(
+        transitionInfo: TransitionInfo,
+        transition: IBinder = mockTransitionBinder
+    ) {
         val startT = Mockito.mock(SurfaceControl.Transaction::class.java)
         val finishT = Mockito.mock(SurfaceControl.Transaction::class.java)
 
-        transitionObserver.onTransitionReady(mockTransitionBinder, transitionInfo, startT, finishT)
+        transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
     }
 
     /** Simulate calling the onTransitionFinished() method */
@@ -191,6 +267,11 @@
         transitionObserver.onTransitionFinished(mockTransitionBinder, false)
     }
 
+    /** Simulate calling the onTransitionMerged() method */
+    private fun callOnTransitionMerged(merged: IBinder, playing: IBinder = mockTransitionBinder) {
+        transitionObserver.onTransitionMerged(merged, playing)
+    }
+
     companion object {
         fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo {
             val taskInfo = ActivityManager.RunningTaskInfo()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ChangeBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ChangeBuilder.java
new file mode 100644
index 0000000..b54c3bf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ChangeBuilder.java
@@ -0,0 +1,72 @@
+/*
+ * 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.transition;
+
+import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
+import static org.mockito.Mockito.mock;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+
+public class ChangeBuilder {
+    final TransitionInfo.Change mChange;
+
+    ChangeBuilder(@WindowManager.TransitionType int mode) {
+        mChange = new TransitionInfo.Change(null /* token */, createMockSurface(true));
+        mChange.setMode(mode);
+    }
+
+    ChangeBuilder setFlags(@TransitionInfo.ChangeFlags int flags) {
+        mChange.setFlags(flags);
+        return this;
+    }
+
+    ChangeBuilder setTask(RunningTaskInfo taskInfo) {
+        mChange.setTaskInfo(taskInfo);
+        return this;
+    }
+
+    ChangeBuilder setRotate(int anim) {
+        return setRotate(Surface.ROTATION_90, anim);
+    }
+
+    ChangeBuilder setRotate() {
+        return setRotate(ROTATION_ANIMATION_UNSPECIFIED);
+    }
+
+    ChangeBuilder setRotate(@Surface.Rotation int target, int anim) {
+        mChange.setRotation(Surface.ROTATION_0, target);
+        mChange.setRotationAnimation(anim);
+        return this;
+    }
+
+    TransitionInfo.Change build() {
+        return mChange;
+    }
+
+    private static SurfaceControl createMockSurface(boolean valid) {
+        SurfaceControl sc = mock(SurfaceControl.class);
+        doReturn(valid).when(sc).isValid();
+        return sc;
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
new file mode 100644
index 0000000..754a173
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.transition;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_SLEEP;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.window.TransitionInfo.FLAG_SYNC;
+import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the default animation handler that is used if no other special-purpose handler picks
+ * up an animation request.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:DefaultTransitionHandlerTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DefaultTransitionHandlerTest extends ShellTestCase {
+
+    private final Context mContext =
+            InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+    private final DisplayController mDisplayController = mock(DisplayController.class);
+    private final TransactionPool mTransactionPool = new MockTransactionPool();
+    private final TestShellExecutor mMainExecutor = new TestShellExecutor();
+    private final TestShellExecutor mAnimExecutor = new TestShellExecutor();
+    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+    private ShellInit mShellInit;
+    private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+    private DefaultTransitionHandler mTransitionHandler;
+
+    @Before
+    public void setUp() {
+        mShellInit = new ShellInit(mMainExecutor);
+        mRootTaskDisplayAreaOrganizer = new RootTaskDisplayAreaOrganizer(
+                mMainExecutor,
+                mContext,
+                mShellInit);
+        mTransitionHandler = new DefaultTransitionHandler(
+                mContext, mShellInit, mDisplayController,
+                mTransactionPool, mMainExecutor, mMainHandler, mAnimExecutor,
+                mRootTaskDisplayAreaOrganizer);
+        mShellInit.init();
+    }
+
+    @After
+    public void tearDown() {
+        flushHandlers();
+    }
+
+    private void flushHandlers() {
+        mMainHandler.runWithScissors(() -> {
+            mAnimExecutor.flushAll();
+            mMainExecutor.flushAll();
+        }, 1000L);
+    }
+
+    @Test
+    public void testAnimationBackgroundCreatedForTaskTransition() {
+        final TransitionInfo.Change openTask = new ChangeBuilder(TRANSIT_OPEN)
+                .setTask(createTaskInfo(1))
+                .build();
+        final TransitionInfo.Change closeTask = new ChangeBuilder(TRANSIT_TO_BACK)
+                .setTask(createTaskInfo(2))
+                .build();
+
+        final IBinder token = new Binder();
+        final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(openTask)
+                .addChange(closeTask)
+                .build();
+        final SurfaceControl.Transaction startT = MockTransactionPool.create();
+        final SurfaceControl.Transaction finishT = MockTransactionPool.create();
+
+        mTransitionHandler.startAnimation(token, info, startT, finishT,
+                mock(Transitions.TransitionFinishCallback.class));
+
+        mergeSync(mTransitionHandler, token);
+        flushHandlers();
+
+        verify(startT).setColor(any(), any());
+    }
+
+    @Test
+    public void testNoAnimationBackgroundForTranslucentTasks() {
+        final TransitionInfo.Change openTask = new ChangeBuilder(TRANSIT_OPEN)
+                .setTask(createTaskInfo(1))
+                .setFlags(FLAG_TRANSLUCENT)
+                .build();
+        final TransitionInfo.Change closeTask = new ChangeBuilder(TRANSIT_TO_BACK)
+                .setTask(createTaskInfo(2))
+                .setFlags(FLAG_TRANSLUCENT)
+                .build();
+
+        final IBinder token = new Binder();
+        final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(openTask)
+                .addChange(closeTask)
+                .build();
+        final SurfaceControl.Transaction startT = MockTransactionPool.create();
+        final SurfaceControl.Transaction finishT = MockTransactionPool.create();
+
+        mTransitionHandler.startAnimation(token, info, startT, finishT,
+                mock(Transitions.TransitionFinishCallback.class));
+
+        mergeSync(mTransitionHandler, token);
+        flushHandlers();
+
+        verify(startT, never()).setColor(any(), any());
+    }
+
+    @Test
+    public void testNoAnimationBackgroundForWallpapers() {
+        final TransitionInfo.Change openWallpaper = new ChangeBuilder(TRANSIT_OPEN)
+                .setFlags(TransitionInfo.FLAG_IS_WALLPAPER)
+                .build();
+        final TransitionInfo.Change closeWallpaper = new ChangeBuilder(TRANSIT_TO_BACK)
+                .setFlags(TransitionInfo.FLAG_IS_WALLPAPER)
+                .build();
+
+        final IBinder token = new Binder();
+        final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(openWallpaper)
+                .addChange(closeWallpaper)
+                .build();
+        final SurfaceControl.Transaction startT = MockTransactionPool.create();
+        final SurfaceControl.Transaction finishT = MockTransactionPool.create();
+
+        mTransitionHandler.startAnimation(token, info, startT, finishT,
+                mock(Transitions.TransitionFinishCallback.class));
+
+        mergeSync(mTransitionHandler, token);
+        flushHandlers();
+
+        verify(startT, never()).setColor(any(), any());
+    }
+
+    private static void mergeSync(Transitions.TransitionHandler handler, IBinder token) {
+        handler.mergeAnimation(
+                new Binder(),
+                new TransitionInfoBuilder(TRANSIT_SLEEP, FLAG_SYNC).build(),
+                MockTransactionPool.create(),
+                token,
+                mock(Transitions.TransitionFinishCallback.class));
+    }
+
+    private static RunningTaskInfo createTaskInfo(int taskId) {
+        RunningTaskInfo taskInfo = new RunningTaskInfo();
+        taskInfo.taskId = taskId;
+        taskInfo.topActivityType = ACTIVITY_TYPE_STANDARD;
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        taskInfo.configuration.windowConfiguration.setActivityType(taskInfo.topActivityType);
+        taskInfo.token = mock(WindowContainerToken.class);
+        return taskInfo;
+    }
+}
+
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/MockTransactionPool.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/MockTransactionPool.java
new file mode 100644
index 0000000..574a87a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/MockTransactionPool.java
@@ -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.
+ */
+
+package com.android.wm.shell.transition;
+
+import static org.mockito.Mockito.RETURNS_SELF;
+import static org.mockito.Mockito.mock;
+
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.util.StubTransaction;
+
+public class MockTransactionPool extends TransactionPool {
+
+    public static SurfaceControl.Transaction create() {
+        return mock(StubTransaction.class, RETURNS_SELF);
+    }
+
+    @Override
+    public SurfaceControl.Transaction acquire() {
+        return create();
+    }
+
+    @Override
+    public void release(SurfaceControl.Transaction t) {
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 69a61ea..8331d59 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -79,7 +79,6 @@
 import android.view.IRecentsAnimationRunner;
 import android.view.Surface;
 import android.view.SurfaceControl;
-import android.view.WindowManager;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
 import android.window.IWindowContainerToken;
@@ -1615,43 +1614,6 @@
                 eq(R.styleable.WindowAnimation_activityCloseEnterAnimation), anyBoolean());
     }
 
-    class ChangeBuilder {
-        final TransitionInfo.Change mChange;
-
-        ChangeBuilder(@WindowManager.TransitionType int mode) {
-            mChange = new TransitionInfo.Change(null /* token */, createMockSurface(true));
-            mChange.setMode(mode);
-        }
-
-        ChangeBuilder setFlags(@TransitionInfo.ChangeFlags int flags) {
-            mChange.setFlags(flags);
-            return this;
-        }
-
-        ChangeBuilder setTask(RunningTaskInfo taskInfo) {
-            mChange.setTaskInfo(taskInfo);
-            return this;
-        }
-
-        ChangeBuilder setRotate(int anim) {
-            return setRotate(Surface.ROTATION_90, anim);
-        }
-
-        ChangeBuilder setRotate() {
-            return setRotate(ROTATION_ANIMATION_UNSPECIFIED);
-        }
-
-        ChangeBuilder setRotate(@Surface.Rotation int target, int anim) {
-            mChange.setRotation(Surface.ROTATION_0, target);
-            mChange.setRotationAnimation(anim);
-            return this;
-        }
-
-        TransitionInfo.Change build() {
-            return mChange;
-        }
-    }
-
     class TestTransitionHandler implements Transitions.TransitionHandler {
         ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> mFinishes =
                 new ArrayList<>();
@@ -1740,12 +1702,6 @@
                 .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
     }
 
-    private static SurfaceControl createMockSurface(boolean valid) {
-        SurfaceControl sc = mock(SurfaceControl.class);
-        doReturn(valid).when(sc).isValid();
-        return sc;
-    }
-
     private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, int activityType) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index ca1e3f1..4c94c29 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -67,6 +67,7 @@
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
 import com.android.wm.shell.shared.DesktopModeStatus
 import com.android.wm.shell.sysui.KeyguardChangeListener
@@ -75,6 +76,7 @@
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
+import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener
 import java.util.Optional
 import java.util.function.Supplier
 import org.junit.Assert.assertEquals
@@ -82,6 +84,7 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.anyInt
@@ -518,6 +521,99 @@
         }
     }
 
+    @Test
+    fun testOnDecorMaximizedOrRestored_togglesTaskSize() {
+        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
+        onTaskOpening(decor.mTaskInfo)
+        val maxOrRestoreListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
+            .let { captor ->
+                verify(decor).setOnMaximizeOrRestoreClickListener(captor.capture())
+                return@let captor.value
+            }
+
+        maxOrRestoreListener.onClick(decor.mTaskInfo.taskId, "test")
+
+        verify(mockDesktopTasksController).toggleDesktopTaskSize(decor.mTaskInfo)
+    }
+
+    @Test
+    fun testOnDecorMaximizedOrRestored_closesMenus() {
+        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
+        onTaskOpening(decor.mTaskInfo)
+        val maxOrRestoreListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
+            .let { captor ->
+                verify(decor).setOnMaximizeOrRestoreClickListener(captor.capture())
+                return@let captor.value
+            }
+
+        maxOrRestoreListener.onClick(decor.mTaskInfo.taskId, "test")
+
+        verify(decor).closeHandleMenu()
+        verify(decor).closeMaximizeMenu()
+    }
+
+    @Test
+    fun testOnDecorSnappedLeft_snapResizes() {
+        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
+        onTaskOpening(decor.mTaskInfo)
+        val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
+            .let { captor ->
+                verify(decor).setOnLeftSnapClickListener(captor.capture())
+                return@let captor.value
+            }
+
+        snapLeftListener.onClick(decor.mTaskInfo.taskId, "test")
+
+        verify(mockDesktopTasksController).snapToHalfScreen(decor.mTaskInfo, SnapPosition.LEFT)
+    }
+
+    @Test
+    fun testOnDecorSnappedLeft_closeMenus() {
+        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
+        onTaskOpening(decor.mTaskInfo)
+        val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
+            .let { captor ->
+                verify(decor).setOnLeftSnapClickListener(captor.capture())
+                return@let captor.value
+            }
+
+        snapLeftListener.onClick(decor.mTaskInfo.taskId, "test")
+
+        verify(decor).closeHandleMenu()
+        verify(decor).closeMaximizeMenu()
+    }
+
+    @Test
+    fun testOnDecorSnappedRight_snapResizes() {
+        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
+        onTaskOpening(decor.mTaskInfo)
+        val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
+            .let { captor ->
+                verify(decor).setOnRightSnapClickListener(captor.capture())
+                return@let captor.value
+            }
+
+        snapLeftListener.onClick(decor.mTaskInfo.taskId, "test")
+
+        verify(mockDesktopTasksController).snapToHalfScreen(decor.mTaskInfo, SnapPosition.RIGHT)
+    }
+
+    @Test
+    fun testOnDecorSnappedRight_closeMenus() {
+        val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM))
+        onTaskOpening(decor.mTaskInfo)
+        val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java)
+            .let { captor ->
+                verify(decor).setOnRightSnapClickListener(captor.capture())
+                return@let captor.value
+            }
+
+        snapLeftListener.onClick(decor.mTaskInfo.taskId, "test")
+
+        verify(decor).closeHandleMenu()
+        verify(decor).closeMaximizeMenu()
+    }
+
     private fun onTaskOpening(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) {
         desktopModeWindowDecorViewModel.onTaskOpening(
                 task,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 46c1589..36e8a46 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -24,9 +24,14 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction;
+import static com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.CLOSE_MAXIMIZE_MENU_DELAY_MS;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
@@ -38,11 +43,13 @@
 
 import android.app.ActivityManager;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.PointF;
 import android.os.Handler;
 import android.os.SystemProperties;
 import android.platform.test.annotations.DisableFlags;
@@ -62,6 +69,7 @@
 import android.view.WindowManager;
 import android.window.WindowContainerTransaction;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
 
@@ -76,6 +84,10 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.shared.DesktopModeStatus;
 import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
+import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
+
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
 
 import org.junit.After;
 import org.junit.Before;
@@ -84,6 +96,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.quality.Strictness;
 
@@ -112,8 +125,6 @@
     @Mock
     private ShellTaskOrganizer mMockShellTaskOrganizer;
     @Mock
-    private Handler mMockHandler;
-    @Mock
     private Choreographer mMockChoreographer;
     @Mock
     private SyncTransactionQueue mMockSyncQueue;
@@ -131,13 +142,18 @@
     private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory;
     @Mock
     private TypedArray mMockRoundedCornersRadiusArray;
-
     @Mock
     private TestTouchEventListener mMockTouchEventListener;
     @Mock
     private DesktopModeWindowDecoration.ExclusionRegionListener mMockExclusionRegionListener;
     @Mock
     private PackageManager mMockPackageManager;
+    @Mock
+    private Handler mMockHandler;
+    @Captor
+    private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
+    @Captor
+    private ArgumentCaptor<Runnable> mCloseMaxMenuRunnable;
 
     private final InsetsState mInsetsState = new InsetsState();
     private SurfaceControl.Transaction mMockTransaction;
@@ -459,6 +475,92 @@
         verify(mMockHandler).removeCallbacks(runnableArgument.getValue());
     }
 
+    @Test
+    public void createMaximizeMenu_showsMenu() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final MaximizeMenu menu = mock(MaximizeMenu.class);
+        final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
+                new FakeMaximizeMenuFactory(menu));
+        assertFalse(decoration.isMaximizeMenuActive());
+
+        createMaximizeMenu(decoration, menu);
+
+        assertTrue(decoration.isMaximizeMenuActive());
+    }
+
+    @Test
+    public void maximizeMenu_unHoversMenu_schedulesCloseMenu() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final MaximizeMenu menu = mock(MaximizeMenu.class);
+        final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
+                new FakeMaximizeMenuFactory(menu));
+        decoration.setAppHeaderMaximizeButtonHovered(false);
+        createMaximizeMenu(decoration, menu);
+
+        mOnMaxMenuHoverChangeListener.getValue().invoke(false);
+
+        verify(mMockHandler)
+                .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS));
+
+        mCloseMaxMenuRunnable.getValue().run();
+        verify(menu).close();
+        assertFalse(decoration.isMaximizeMenuActive());
+    }
+
+    @Test
+    public void maximizeMenu_unHoversButton_schedulesCloseMenu() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final MaximizeMenu menu = mock(MaximizeMenu.class);
+        final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
+                new FakeMaximizeMenuFactory(menu));
+        decoration.setAppHeaderMaximizeButtonHovered(true);
+        createMaximizeMenu(decoration, menu);
+
+        decoration.setAppHeaderMaximizeButtonHovered(false);
+
+        verify(mMockHandler)
+                .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS));
+
+        mCloseMaxMenuRunnable.getValue().run();
+        verify(menu).close();
+        assertFalse(decoration.isMaximizeMenuActive());
+    }
+
+    @Test
+    public void maximizeMenu_hoversMenu_cancelsCloseMenu() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final MaximizeMenu menu = mock(MaximizeMenu.class);
+        final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
+                new FakeMaximizeMenuFactory(menu));
+        createMaximizeMenu(decoration, menu);
+
+        mOnMaxMenuHoverChangeListener.getValue().invoke(true);
+
+        verify(mMockHandler).removeCallbacks(any());
+    }
+
+    @Test
+    public void maximizeMenu_hoversButton_cancelsCloseMenu() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final MaximizeMenu menu = mock(MaximizeMenu.class);
+        final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
+                new FakeMaximizeMenuFactory(menu));
+        createMaximizeMenu(decoration, menu);
+
+        decoration.setAppHeaderMaximizeButtonHovered(true);
+
+        verify(mMockHandler).removeCallbacks(any());
+    }
+
+    private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) {
+        final OnTaskActionClickListener l = (taskId, tag) -> {};
+        decoration.setOnMaximizeOrRestoreClickListener(l);
+        decoration.setOnLeftSnapClickListener(l);
+        decoration.setOnRightSnapClickListener(l);
+        decoration.createMaximizeMenu();
+        verify(menu).show(any(), any(), any(), mOnMaxMenuHoverChangeListener.capture());
+    }
+
     private void fillRoundedCornersResources(int fillValue) {
         when(mMockRoundedCornersRadiusArray.getDimensionPixelSize(anyInt(), anyInt()))
                 .thenReturn(fillValue);
@@ -479,12 +581,19 @@
 
     private DesktopModeWindowDecoration createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo) {
-        DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
+        return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory());
+    }
+
+    private DesktopModeWindowDecoration createWindowDecoration(
+            ActivityManager.RunningTaskInfo taskInfo,
+            MaximizeMenuFactory maximizeMenuFactory) {
+        final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
                 mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl,
                 mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
                 SurfaceControl.Builder::new, mMockTransactionSupplier,
                 WindowContainerTransaction::new, SurfaceControl::new,
-                mMockSurfaceControlViewHostFactory);
+                mMockSurfaceControlViewHostFactory,
+                maximizeMenuFactory);
         windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
                 mMockTouchEventListener, mMockTouchEventListener);
         windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
@@ -541,4 +650,27 @@
             return false;
         }
     }
+
+    private static final class FakeMaximizeMenuFactory implements MaximizeMenuFactory {
+        private final MaximizeMenu mMaximizeMenu;
+
+        FakeMaximizeMenuFactory() {
+            this(mock(MaximizeMenu.class));
+        }
+
+        FakeMaximizeMenuFactory(MaximizeMenu menu) {
+            mMaximizeMenu = menu;
+        }
+
+        @NonNull
+        @Override
+        public MaximizeMenu create(@NonNull SyncTransactionQueue syncQueue,
+                @NonNull RootTaskDisplayAreaOrganizer rootTdaOrganizer,
+                @NonNull DisplayController displayController,
+                @NonNull ActivityManager.RunningTaskInfo taskInfo,
+                @NonNull Context decorWindowContext, @NonNull PointF menuPosition,
+                @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) {
+            return mMaximizeMenu;
+        }
+    }
 }
diff --git a/media/java/android/media/AudioManagerInternal.java b/media/java/android/media/AudioManagerInternal.java
index c263245..fd71f86 100644
--- a/media/java/android/media/AudioManagerInternal.java
+++ b/media/java/android/media/AudioManagerInternal.java
@@ -44,8 +44,9 @@
      * Add the UID for a new assistant service
      *
      * @param uid UID of the newly available assistants
+     * @param owningUid UID of the actual assistant app, if {@code uid} is a isolated proc
      */
-    public abstract void addAssistantServiceUid(int uid);
+    public abstract void addAssistantServiceUid(int uid, int owningUid);
 
     /**
      * Remove the UID for an existing assistant service
diff --git a/media/jni/Android.bp b/media/jni/Android.bp
index e619e1c..7f487e5 100644
--- a/media/jni/Android.bp
+++ b/media/jni/Android.bp
@@ -79,7 +79,6 @@
         "libcamera_client",
         "libmtp",
         "libpiex",
-        "libprocessgroup",
         "libandroidfw",
         "libhidlallocatorutils",
         "libhidlbase",
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
index 00068bd..102d21a 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.mediaframeworktest.helpers;
 
+import android.content.AttributionSourceState;
+import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.ImageFormat;
@@ -2227,4 +2229,24 @@
         else
             return new Size(width, height);
     }
+
+    /**
+     * Constructs an AttributionSourceState with only the uid, pid, and deviceId fields set
+     *
+     * <p>This method is a temporary stopgap in the transition to using AttributionSource. Currently
+     * AttributionSourceState is only used as a vehicle for passing deviceId, uid, and pid
+     * arguments.</p>
+     */
+    public static AttributionSourceState getClientAttribution(Context context) {
+        // TODO: Send the full contextAttribution over aidl, remove USE_CALLING_*
+        AttributionSourceState contextAttribution =
+                context.getAttributionSource().asState();
+        AttributionSourceState clientAttribution =
+                new AttributionSourceState();
+        clientAttribution.uid = -1; // USE_CALLING_UID
+        clientAttribution.pid = -1; // USE_CALLING_PID
+        clientAttribution.deviceId = contextAttribution.deviceId;
+        clientAttribution.next = new AttributionSourceState[0];
+        return clientAttribution;
+    }
 }
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
index 353366d..ad3374a 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
@@ -19,6 +19,7 @@
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
 import static android.content.Context.DEVICE_ID_DEFAULT;
 
+import android.content.AttributionSourceState;
 import android.hardware.CameraInfo;
 import android.hardware.ICamera;
 import android.hardware.ICameraClient;
@@ -38,6 +39,8 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.mediaframeworktest.helpers.CameraTestUtils;
+
 /**
  * <p>
  * Junit / Instrumentation test case for the camera2 api
@@ -78,8 +81,10 @@
 
     @SmallTest
     public void testNumberOfCameras() throws Exception {
+        AttributionSourceState clientAttribution = CameraTestUtils.getClientAttribution(mContext);
+        clientAttribution.deviceId = DEVICE_ID_DEFAULT;
         int numCameras = mUtils.getCameraService().getNumberOfCameras(CAMERA_TYPE_ALL,
-                DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
+                clientAttribution, DEVICE_POLICY_DEFAULT);
         assertTrue("At least this many cameras: " + mUtils.getGuessedNumCameras(),
                 numCameras >= mUtils.getGuessedNumCameras());
         Log.v(TAG, "Number of cameras " + numCameras);
@@ -87,9 +92,11 @@
 
     @SmallTest
     public void testCameraInfo() throws Exception {
+        AttributionSourceState clientAttribution = CameraTestUtils.getClientAttribution(mContext);
+        clientAttribution.deviceId = DEVICE_ID_DEFAULT;
         for (int cameraId = 0; cameraId < mUtils.getGuessedNumCameras(); ++cameraId) {
             CameraInfo info = mUtils.getCameraService().getCameraInfo(cameraId,
-                    ICameraService.ROTATION_OVERRIDE_NONE, DEVICE_ID_DEFAULT,
+                    ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution,
                     DEVICE_POLICY_DEFAULT);
             assertTrue("Facing was not set for camera " + cameraId, info.info.facing != -1);
             assertTrue("Orientation was not set for camera " + cameraId,
@@ -154,6 +161,10 @@
 
     @SmallTest
     public void testConnect() throws Exception {
+        AttributionSourceState clientAttribution = CameraTestUtils.getClientAttribution(mContext);
+        clientAttribution.deviceId = DEVICE_ID_DEFAULT;
+        clientAttribution.uid = ICameraService.USE_CALLING_UID;
+        clientAttribution.pid = ICameraService.USE_CALLING_PID;
         for (int cameraId = 0; cameraId < mUtils.getGuessedNumCameras(); ++cameraId) {
 
             ICameraClient dummyCallbacks = new DummyCameraClient();
@@ -162,12 +173,10 @@
 
             ICamera cameraUser = mUtils.getCameraService()
                     .connect(dummyCallbacks, cameraId, clientPackageName,
-                            ICameraService.USE_CALLING_UID,
-                            ICameraService.USE_CALLING_PID,
                             getContext().getApplicationInfo().targetSdkVersion,
                             ICameraService.ROTATION_OVERRIDE_NONE,
                             /*forceSlowJpegMode*/false,
-                            DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
+                            clientAttribution, DEVICE_POLICY_DEFAULT);
             assertNotNull(String.format("Camera %s was null", cameraId), cameraUser);
 
             Log.v(TAG, String.format("Camera %s connected", cameraId));
@@ -260,14 +269,18 @@
 
             String clientPackageName = getContext().getPackageName();
             String clientAttributionTag = getContext().getAttributionTag();
+            AttributionSourceState clientAttribution =
+                    CameraTestUtils.getClientAttribution(mContext);
+            clientAttribution.deviceId = DEVICE_ID_DEFAULT;
+            clientAttribution.uid = ICameraService.USE_CALLING_UID;
 
             ICameraDeviceUser cameraUser =
                     mUtils.getCameraService().connectDevice(
                         dummyCallbacks, String.valueOf(cameraId),
                         clientPackageName, clientAttributionTag,
-                        ICameraService.USE_CALLING_UID, 0 /*oomScoreOffset*/,
+                        0 /*oomScoreOffset*/,
                         getContext().getApplicationInfo().targetSdkVersion,
-                        ICameraService.ROTATION_OVERRIDE_NONE, DEVICE_ID_DEFAULT,
+                        ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution,
                         DEVICE_POLICY_DEFAULT);
             assertNotNull(String.format("Camera %s was null", cameraId), cameraUser);
 
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
index 6cf2a41..0ab1ee9 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 
+import android.content.AttributionSourceState;
 import android.graphics.ImageFormat;
 import android.graphics.SurfaceTexture;
 import android.hardware.ICameraService;
@@ -54,6 +55,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.mediaframeworktest.MediaFrameworkIntegrationTestRunner;
+import com.android.mediaframeworktest.helpers.CameraTestUtils;
 
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
@@ -245,10 +247,14 @@
 
         mMockCb = spy(dummyCallbacks);
 
+        AttributionSourceState clientAttribution = CameraTestUtils.getClientAttribution(mContext);
+        clientAttribution.deviceId = DEVICE_ID_DEFAULT;
+        clientAttribution.uid = ICameraService.USE_CALLING_UID;
+
         mCameraUser = mUtils.getCameraService().connectDevice(mMockCb, mCameraId,
-                clientPackageName, clientAttributionTag, ICameraService.USE_CALLING_UID,
+                clientPackageName, clientAttributionTag,
                 /*oomScoreOffset*/0, getContext().getApplicationInfo().targetSdkVersion,
-                ICameraService.ROTATION_OVERRIDE_NONE, DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
+                ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution, DEVICE_POLICY_DEFAULT);
         assertNotNull(String.format("Camera %s was null", mCameraId), mCameraUser);
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
@@ -414,10 +420,13 @@
 
     @SmallTest
     public void testCameraCharacteristics() throws RemoteException {
+        AttributionSourceState clientAttribution = CameraTestUtils.getClientAttribution(mContext);
+        clientAttribution.deviceId = DEVICE_ID_DEFAULT;
+
         CameraMetadataNative info = mUtils.getCameraService().getCameraCharacteristics(mCameraId,
                 getContext().getApplicationInfo().targetSdkVersion,
                 ICameraService.ROTATION_OVERRIDE_NONE,
-                DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
+                clientAttribution, DEVICE_POLICY_DEFAULT);
 
         assertFalse(info.isEmpty());
         assertNotNull(info.get(CameraCharacteristics.SCALER_AVAILABLE_FORMATS));
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 7e5bef1..e91c7a9 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -244,6 +244,12 @@
         ALOGE("%s: targetDurationNanos must be positive", __FUNCTION__);
         return EINVAL;
     }
+    {
+        std::scoped_lock lock(sHintMutex);
+        if (mTargetDurationNanos == targetDurationNanos) {
+            return 0;
+        }
+    }
     ndk::ScopedAStatus ret = mHintSession->updateTargetWorkDuration(targetDurationNanos);
     if (!ret.isOk()) {
         ALOGE("%s: HintSession updateTargetWorkDuration failed: %s", __FUNCTION__,
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index 78a5357..d19fa98 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -159,6 +159,10 @@
     int result = APerformanceHint_updateTargetWorkDuration(session, targetDurationNanos);
     EXPECT_EQ(0, result);
 
+    // subsequent call with same target should be ignored but return no error
+    result = APerformanceHint_updateTargetWorkDuration(session, targetDurationNanos);
+    EXPECT_EQ(0, result);
+
     usleep(2); // Sleep for longer than preferredUpdateRateNanos.
     int64_t actualDurationNanos = 20;
     std::vector<int64_t> actualDurations;
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index b242a76..95945d7 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -110,3 +110,11 @@
     bug: "321311407"
 }
 
+flag {
+    name: "nfc_persist_log"
+    is_exported: true
+    namespace: "nfc"
+    description: "Enable NFC persistent log support"
+    bug: "321310044"
+}
+
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/OWNERS b/packages/SettingsLib/src/com/android/settingslib/bluetooth/OWNERS
index 7669e79b..f8c3a93 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/OWNERS
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/OWNERS
@@ -1,9 +1,4 @@
 # Default reviewers for this and subdirectories.
-siyuanh@google.com
-hughchen@google.com
-timhypeng@google.com
-robertluo@google.com
-songferngwang@google.com
 yqian@google.com
 chelseahao@google.com
 yiyishen@google.com
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
index c5e86b4..4f2329b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
@@ -327,4 +327,12 @@
             return sInstance;
         }
     }
+
+    /** Testing only. Reset the instance to avoid tests affecting each other. */
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    public static void resetInstance() {
+        synchronized (PowerAllowlistBackend.class) {
+            sInstance = null;
+        }
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
index df0e618..8868837 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
@@ -23,19 +23,8 @@
 import android.telephony.UiccSlotInfo;
 import android.telephony.UiccSlotMapping;
 
-import java.util.List;
-
 public class DataServiceUtils {
 
-    public static <T> boolean shouldUpdateEntityList(List<T> oldList, List<T> newList) {
-        if ((oldList != null &&
-                (newList.isEmpty() || !newList.equals(oldList)))
-                || (!newList.isEmpty() && oldList == null)) {
-            return true;
-        }
-        return false;
-    }
-
     /**
      * Represents columns of the MobileNetworkInfoData table, define these columns from
      * {@see MobileNetworkUtils} or relevant common APIs.
@@ -52,73 +41,16 @@
         public static final String COLUMN_ID = "subId";
 
         /**
-         * The name of the contact discovery enabled state column,
-         * {@see MobileNetworkUtils#isContactDiscoveryEnabled(Context, int)}.
-         */
-        public static final String COLUMN_IS_CONTACT_DISCOVERY_ENABLED =
-                "isContactDiscoveryEnabled";
-
-        /**
-         * The name of the contact discovery visible state column,
-         * {@see MobileNetworkUtils#isContactDiscoveryEnabled(Context, int)}.
-         */
-        public static final String COLUMN_IS_CONTACT_DISCOVERY_VISIBLE =
-                "isContactDiscoveryVisible";
-
-        /**
          * The name of the mobile network data state column,
          * {@see MobileNetworkUtils#isMobileDataEnabled(Context)}.
          */
         public static final String COLUMN_IS_MOBILE_DATA_ENABLED = "isMobileDataEnabled";
 
         /**
-         * The name of the CDMA option state column,
-         * {@see MobileNetworkUtils#isCdmaOptions(Context, int)}.
-         */
-        public static final String COLUMN_IS_CDMA_OPTIONS = "isCdmaOptions";
-
-        /**
-         * The name of the GSM option state column,
-         * {@see MobileNetworkUtils#isGsmOptions(Context, int)}.
-         */
-        public static final String COLUMN_IS_GSM_OPTIONS = "isGsmOptions";
-
-        /**
-         * The name of the world mode state column,
-         * {@see MobileNetworkUtils#isWorldMode(Context, int)}.
-         */
-        public static final String COLUMN_IS_WORLD_MODE = "isWorldMode";
-
-        /**
-         * The name of the display network select options state column,
-         * {@see MobileNetworkUtils#shouldDisplayNetworkSelectOptions(Context, int)}.
-         */
-        public static final String COLUMN_SHOULD_DISPLAY_NETWORK_SELECT_OPTIONS =
-                "shouldDisplayNetworkSelectOptions";
-
-        /**
-         * The name of the TDSCDMA supported state column,
-         * {@see MobileNetworkUtils#isTdscdmaSupported(Context, int)}.
-         */
-        public static final String COLUMN_IS_TDSCDMA_SUPPORTED = "isTdscdmaSupported";
-
-        /**
-         * The name of the active network is cellular state column,
-         * {@see MobileNetworkUtils#activeNetworkIsCellular(Context)}.
-         */
-        public static final String COLUMN_ACTIVE_NETWORK_IS_CELLULAR = "activeNetworkIsCellular";
-
-        /**
          * The name of the show toggle for physicalSim state column,
          * {@see SubscriptionUtil#showToggleForPhysicalSim(SubscriptionManager)}.
          */
         public static final String COLUMN_SHOW_TOGGLE_FOR_PHYSICAL_SIM = "showToggleForPhysicalSim";
-
-        /**
-         * The name of the subscription's data roaming state column,
-         * {@see TelephonyManager#isDataRoamingEnabled()}.
-         */
-        public static final String COLUMN_IS_DATA_ROAMING_ENABLED = "isDataRoamingEnabled";
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java
index e72346d..13f99e9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java
@@ -26,23 +26,11 @@
 @Entity(tableName = DataServiceUtils.MobileNetworkInfoData.TABLE_NAME)
 public class MobileNetworkInfoEntity {
 
-    public MobileNetworkInfoEntity(@NonNull String subId, boolean isContactDiscoveryEnabled,
-            boolean isContactDiscoveryVisible, boolean isMobileDataEnabled, boolean isCdmaOptions,
-            boolean isGsmOptions, boolean isWorldMode, boolean shouldDisplayNetworkSelectOptions,
-            boolean isTdscdmaSupported, boolean activeNetworkIsCellular,
-            boolean showToggleForPhysicalSim, boolean isDataRoamingEnabled) {
+    public MobileNetworkInfoEntity(@NonNull String subId, boolean isMobileDataEnabled,
+            boolean showToggleForPhysicalSim) {
         this.subId = subId;
-        this.isContactDiscoveryEnabled = isContactDiscoveryEnabled;
-        this.isContactDiscoveryVisible = isContactDiscoveryVisible;
         this.isMobileDataEnabled = isMobileDataEnabled;
-        this.isCdmaOptions = isCdmaOptions;
-        this.isGsmOptions = isGsmOptions;
-        this.isWorldMode = isWorldMode;
-        this.shouldDisplayNetworkSelectOptions = shouldDisplayNetworkSelectOptions;
-        this.isTdscdmaSupported = isTdscdmaSupported;
-        this.activeNetworkIsCellular = activeNetworkIsCellular;
         this.showToggleForPhysicalSim = showToggleForPhysicalSim;
-        this.isDataRoamingEnabled = isDataRoamingEnabled;
     }
 
     @PrimaryKey
@@ -50,55 +38,18 @@
     @NonNull
     public String subId;
 
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_CONTACT_DISCOVERY_ENABLED)
-    public boolean isContactDiscoveryEnabled;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_CONTACT_DISCOVERY_VISIBLE)
-    public boolean isContactDiscoveryVisible;
-
     @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_MOBILE_DATA_ENABLED)
     public boolean isMobileDataEnabled;
 
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_CDMA_OPTIONS)
-    public boolean isCdmaOptions;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_GSM_OPTIONS)
-    public boolean isGsmOptions;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_WORLD_MODE)
-    public boolean isWorldMode;
-
-    @ColumnInfo(name =
-            DataServiceUtils.MobileNetworkInfoData.COLUMN_SHOULD_DISPLAY_NETWORK_SELECT_OPTIONS)
-    public boolean shouldDisplayNetworkSelectOptions;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_TDSCDMA_SUPPORTED)
-    public boolean isTdscdmaSupported;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_ACTIVE_NETWORK_IS_CELLULAR)
-    public boolean activeNetworkIsCellular;
-
     @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_SHOW_TOGGLE_FOR_PHYSICAL_SIM)
     public boolean showToggleForPhysicalSim;
 
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_DATA_ROAMING_ENABLED)
-    public boolean isDataRoamingEnabled;
-
     @Override
     public int hashCode() {
         int result = 17;
         result = 31 * result + subId.hashCode();
-        result = 31 * result + Boolean.hashCode(isContactDiscoveryEnabled);
-        result = 31 * result + Boolean.hashCode(isContactDiscoveryVisible);
         result = 31 * result + Boolean.hashCode(isMobileDataEnabled);
-        result = 31 * result + Boolean.hashCode(isCdmaOptions);
-        result = 31 * result + Boolean.hashCode(isGsmOptions);
-        result = 31 * result + Boolean.hashCode(isWorldMode);
-        result = 31 * result + Boolean.hashCode(shouldDisplayNetworkSelectOptions);
-        result = 31 * result + Boolean.hashCode(isTdscdmaSupported);
-        result = 31 * result + Boolean.hashCode(activeNetworkIsCellular);
         result = 31 * result + Boolean.hashCode(showToggleForPhysicalSim);
-        result = 31 * result + Boolean.hashCode(isDataRoamingEnabled);
         return result;
     }
 
@@ -113,45 +64,18 @@
 
         MobileNetworkInfoEntity info = (MobileNetworkInfoEntity) obj;
         return  TextUtils.equals(subId, info.subId)
-                && isContactDiscoveryEnabled == info.isContactDiscoveryEnabled
-                && isContactDiscoveryVisible == info.isContactDiscoveryVisible
                 && isMobileDataEnabled == info.isMobileDataEnabled
-                && isCdmaOptions == info.isCdmaOptions
-                && isGsmOptions == info.isGsmOptions
-                && isWorldMode == info.isWorldMode
-                && shouldDisplayNetworkSelectOptions == info.shouldDisplayNetworkSelectOptions
-                && isTdscdmaSupported == info.isTdscdmaSupported
-                && activeNetworkIsCellular == info.activeNetworkIsCellular
-                && showToggleForPhysicalSim == info.showToggleForPhysicalSim
-                && isDataRoamingEnabled == info.isDataRoamingEnabled;
+                && showToggleForPhysicalSim == info.showToggleForPhysicalSim;
     }
 
     public String toString() {
         StringBuilder builder = new StringBuilder();
         builder.append(" {MobileNetworkInfoEntity(subId = ")
                 .append(subId)
-                .append(", isContactDiscoveryEnabled = ")
-                .append(isContactDiscoveryEnabled)
-                .append(", isContactDiscoveryVisible = ")
-                .append(isContactDiscoveryVisible)
                 .append(", isMobileDataEnabled = ")
                 .append(isMobileDataEnabled)
-                .append(", isCdmaOptions = ")
-                .append(isCdmaOptions)
-                .append(", isGsmOptions = ")
-                .append(isGsmOptions)
-                .append(", isWorldMode = ")
-                .append(isWorldMode)
-                .append(", shouldDisplayNetworkSelectOptions = ")
-                .append(shouldDisplayNetworkSelectOptions)
-                .append(", isTdscdmaSupported = ")
-                .append(isTdscdmaSupported)
                 .append(", activeNetworkIsCellular = ")
-                .append(activeNetworkIsCellular)
-                .append(", showToggleForPhysicalSim = ")
                 .append(showToggleForPhysicalSim)
-                .append(", isDataRoamingEnabled = ")
-                .append(isDataRoamingEnabled)
                 .append(")}");
         return builder.toString();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 8ec5ba1..837c682 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -46,6 +46,7 @@
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
@@ -98,6 +99,7 @@
     private val contentResolver: ContentResolver,
     private val backgroundCoroutineContext: CoroutineContext,
     private val coroutineScope: CoroutineScope,
+    private val logger: Logger,
 ) : AudioRepository {
 
     private val streamSettingNames: Map<AudioStream, String> =
@@ -170,6 +172,7 @@
             .conflate()
             .map { getCurrentAudioStream(audioStream) }
             .onStart { emit(getCurrentAudioStream(audioStream)) }
+            .onEach { logger.onVolumeUpdateReceived(audioStream, it) }
             .flowOn(backgroundCoroutineContext)
     }
 
@@ -193,6 +196,7 @@
 
     override suspend fun setVolume(audioStream: AudioStream, volume: Int) {
         withContext(backgroundCoroutineContext) {
+            logger.onSetVolumeRequested(audioStream, volume)
             audioManager.setStreamVolume(audioStream.value, volume, 0)
         }
     }
@@ -247,4 +251,11 @@
             awaitClose { contentResolver.unregisterContentObserver(observer) }
         }
     }
+
+    interface Logger {
+
+        fun onSetVolumeRequested(audioStream: AudioStream, volume: Int)
+
+        fun onVolumeUpdateReceived(audioStream: AudioStream, model: AudioStreamModel)
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt
index 9c48299..c8e4d71 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt
@@ -17,6 +17,7 @@
 package com.android.settingslib.volume.shared.model
 
 import android.media.AudioManager
+import android.media.AudioSystem
 
 /** Type-safe wrapper for [AudioManager] audio stream. */
 @JvmInline
@@ -25,6 +26,8 @@
         require(value in supportedStreamTypes) { "Unsupported stream=$value" }
     }
 
+    override fun toString(): String = AudioSystem.streamToString(value)
+
     companion object {
         val supportedStreamTypes =
             setOf(
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
index 844dc12..0e43acb 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
@@ -64,6 +64,7 @@
     @Mock private lateinit var communicationDevice: AudioDeviceInfo
     @Mock private lateinit var contentResolver: ContentResolver
 
+    private val logger = FakeAudioRepositoryLogger()
     private val eventsReceiver = FakeAudioManagerEventsReceiver()
     private val volumeByStream: MutableMap<Int, Int> = mutableMapOf()
     private val isAffectedByRingerModeByStream: MutableMap<Int, Boolean> = mutableMapOf()
@@ -109,6 +110,7 @@
                 contentResolver,
                 testScope.testScheduler,
                 testScope.backgroundScope,
+                logger,
             )
     }
 
@@ -173,6 +175,15 @@
             underTest.setVolume(audioStream, 50)
             runCurrent()
 
+            assertThat(logger.logs)
+                .isEqualTo(
+                    listOf(
+                        "onVolumeUpdateReceived audioStream=STREAM_SYSTEM",
+                        "onSetVolumeRequested audioStream=STREAM_SYSTEM",
+                        "onVolumeUpdateReceived audioStream=STREAM_SYSTEM",
+                        "onVolumeUpdateReceived audioStream=STREAM_SYSTEM",
+                    )
+                )
             assertThat(streamModel)
                 .isEqualTo(
                     AudioStreamModel(
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt
new file mode 100644
index 0000000..389bf53
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.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.settingslib.volume.data.repository
+
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.AudioStreamModel
+
+class FakeAudioRepositoryLogger : AudioRepositoryImpl.Logger {
+
+    private val mutableLogs: MutableList<String> = mutableListOf()
+    val logs: List<String>
+        get() = mutableLogs
+
+    override fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) {
+        synchronized(mutableLogs) {
+            mutableLogs.add("onSetVolumeRequested audioStream=$audioStream")
+        }
+    }
+
+    override fun onVolumeUpdateReceived(audioStream: AudioStream, model: AudioStreamModel) {
+        synchronized(mutableLogs) {
+            mutableLogs.add("onVolumeUpdateReceived audioStream=$audioStream")
+        }
+    }
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 90885ab..23422d1 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -217,6 +217,17 @@
 }
 
 flag {
+    name: "notification_group_hun_removal_animation_fix"
+    namespace: "systemui"
+    description: "Fix the lack of hun removal animation for group notifications"
+        "(not GROUP_ALERT_SUMMARY)"
+    bug: "343475993"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "scene_container"
     namespace: "systemui"
     description: "Enables the scene container framework go/flexiglass."
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 2ed0f6c..e02e5f8 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
@@ -155,7 +155,7 @@
     val coroutineScope = rememberCoroutineScope()
     val currentSceneKey: SceneKey by
         viewModel.currentScene.collectAsStateWithLifecycle(CommunalScenes.Blank)
-    val touchesAllowed by viewModel.touchesAllowed.collectAsStateWithLifecycle(initialValue = false)
+    val touchesAllowed by viewModel.touchesAllowed.collectAsStateWithLifecycle()
     val showGestureIndicator by
         viewModel.showGestureIndicator.collectAsStateWithLifecycle(initialValue = false)
     val backgroundType by
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
index 4555f13..c34fb38 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
@@ -19,9 +19,10 @@
 package com.android.systemui.keyguard.ui.composable
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.combinedClickable
 import androidx.compose.foundation.gestures.awaitEachGesture
 import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.indication
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
@@ -33,12 +34,14 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
+import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel
 
 /** Container for lockscreen content that handles long-press to bring up the settings menu. */
 @Composable
+// TODO(b/344879669): now that it's more generic than long-press, rename it.
 fun LockscreenLongPress(
-    viewModel: KeyguardLongPressViewModel,
+    viewModel: KeyguardTouchHandlingViewModel,
     modifier: Modifier = Modifier,
     content: @Composable BoxScope.(onSettingsMenuPlaces: (coordinates: Rect?) -> Unit) -> Unit,
 ) {
@@ -50,14 +53,17 @@
     Box(
         modifier =
             modifier
-                .combinedClickable(
-                    enabled = isEnabled,
-                    onLongClick = viewModel::onLongPress,
-                    onClick = {},
-                    interactionSource = interactionSource,
-                    // Passing null for the indication removes the ripple effect.
-                    indication = null,
-                )
+                .pointerInput(isEnabled) {
+                    if (isEnabled) {
+                        detectLongPressGesture { viewModel.onLongPress() }
+                    }
+                }
+                .pointerInput(Unit) {
+                    detectTapGestures(
+                        onTap = { viewModel.onClick(it.x, it.y) },
+                        onDoubleTap = { viewModel.onDoubleClick() },
+                    )
+                }
                 .pointerInput(settingsMenuBounds) {
                     awaitEachGesture {
                         val pointerInputChange = awaitFirstDown()
@@ -65,7 +71,9 @@
                             viewModel.onTouchedOutside()
                         }
                     }
-                },
+                }
+                // Passing null for the indication removes the ripple effect.
+                .indication(interactionSource, null)
     ) {
         content(setSettingsMenuBounds)
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
index 6b210af..210ca69 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt
@@ -43,7 +43,7 @@
     @Composable
     override fun SceneScope.Content(modifier: Modifier) {
         LockscreenLongPress(
-            viewModel = viewModel.longPress,
+            viewModel = viewModel.touchHandling,
             modifier = modifier,
         ) { _ ->
             Box(modifier.background(Color.Black)) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index a39fa64..0a4c6fd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -72,7 +72,7 @@
         val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle()
 
         LockscreenLongPress(
-            viewModel = viewModel.longPress,
+            viewModel = viewModel.touchHandling,
             modifier = modifier,
         ) { onSettingsMenuPlaced ->
             Layout(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index c83f62c..065f2a2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -74,7 +74,7 @@
         val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle()
 
         LockscreenLongPress(
-            viewModel = viewModel.longPress,
+            viewModel = viewModel.touchHandling,
             modifier = modifier,
         ) { onSettingsMenuPlaced ->
             Layout(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SettingsMenuSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SettingsMenuSection.kt
index 44b0535..15032e0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SettingsMenuSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SettingsMenuSection.kt
@@ -30,8 +30,8 @@
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.core.view.isVisible
 import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
@@ -42,7 +42,7 @@
 @Inject
 constructor(
     private val viewModel: KeyguardSettingsMenuViewModel,
-    private val longPressViewModel: KeyguardLongPressViewModel,
+    private val touchHandlingViewModel: KeyguardTouchHandlingViewModel,
     private val vibratorHelper: VibratorHelper,
     private val activityStarter: ActivityStarter,
 ) {
@@ -69,7 +69,7 @@
                             KeyguardSettingsViewBinder.bind(
                                 view = this,
                                 viewModel = viewModel,
-                                longPressViewModel = longPressViewModel,
+                                touchHandlingViewModel = touchHandlingViewModel,
                                 rootViewModel = null,
                                 vibratorHelper = vibratorHelper,
                                 activityStarter = activityStarter,
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 6805888..2eea2f0 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
@@ -21,6 +21,7 @@
 import androidx.compose.animation.core.Animatable
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
@@ -233,6 +234,8 @@
     // The height of the scrim visible on screen when it is in its resting (collapsed) state.
     val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() }
 
+    val isClickable by viewModel.isClickable.collectAsStateWithLifecycle()
+
     // we are not scrolled to the top unless the scrim is at its maximum offset.
     LaunchedEffect(viewModel, scrimOffset) {
         snapshotFlow { scrimOffset.value >= 0f }
@@ -328,6 +331,9 @@
                         )
                     )
                 }
+                .thenIf(isClickable) {
+                    Modifier.clickable(onClick = { viewModel.onEmptySpaceClicked() })
+                }
     ) {
         // Creates a cutout in the background scrim in the shape of the notifications scrim.
         // Only visible when notif scrim alpha < 1, during shade expansion.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/BrightnessMirror.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/BrightnessMirror.kt
index 73a624a..aca473d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/BrightnessMirror.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/BrightnessMirror.kt
@@ -18,8 +18,9 @@
 
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.wrapContentWidth
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
@@ -39,6 +40,7 @@
     viewModel: BrightnessMirrorViewModel,
     qsSceneAdapter: QSSceneAdapter,
     modifier: Modifier = Modifier,
+    measureFromContainer: Boolean = false,
 ) {
     val isShowing by viewModel.isShowing.collectAsStateWithLifecycle()
     val mirrorAlpha by
@@ -47,9 +49,22 @@
             label = "alphaAnimationBrightnessMirrorShowing",
         )
     val mirrorOffsetAndSize by viewModel.locationAndSize.collectAsStateWithLifecycle()
-    val offset = IntOffset(0, mirrorOffsetAndSize.yOffset)
+    val yOffset =
+        if (measureFromContainer) {
+            mirrorOffsetAndSize.yOffsetFromContainer
+        } else {
+            mirrorOffsetAndSize.yOffsetFromWindow
+        }
+    val offset = IntOffset(0, yOffset)
 
-    Box(modifier = modifier.fillMaxSize().graphicsLayer { alpha = mirrorAlpha }) {
+    // Use unbounded=true as the full mirror (with paddings and background offset) may be larger
+    // than the space we have (but it will fit, because the brightness slider fits).
+    Box(
+        modifier =
+            modifier.fillMaxHeight().wrapContentWidth(unbounded = true).graphicsLayer {
+                alpha = mirrorAlpha
+            }
+    ) {
         QuickSettingsTheme {
             // The assumption for using this AndroidView is that there will be only one in view at
             // a given time (which is a reasonable assumption). Because `QSSceneAdapter` (actually
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 0b57151..2d5d259 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -185,7 +185,11 @@
 
     BrightnessMirror(
         viewModel = viewModel.brightnessMirrorViewModel,
-        qsSceneAdapter = viewModel.qsSceneAdapter
+        qsSceneAdapter = viewModel.qsSceneAdapter,
+        modifier =
+            Modifier.thenIf(cutoutLocation != CutoutLocation.CENTER) {
+                Modifier.displayCutoutPadding()
+            }
     )
 
     val shouldPunchHoleBehindScrim =
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt
index 924aa54..4eaacf3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt
@@ -86,7 +86,7 @@
  */
 @Composable
 fun <T> Session.rememberSession(vararg inputs: Any?, key: String? = null, init: () -> T): T =
-    rememberSession(key, inputs, init = init)
+    rememberSession(key, *inputs, init = init)
 
 /**
  * An explicit storage for remembering composable state outside of the lifetime of a composition.
@@ -151,7 +151,7 @@
     vararg inputs: Any?,
     key: String? = null,
 ): SaveableSession =
-    rememberSaveable(inputs, SaveableSessionImpl.SessionSaver, key) { SaveableSessionImpl() }
+    rememberSaveable(*inputs, SaveableSessionImpl.SessionSaver, key) { SaveableSessionImpl() }
 
 private class SessionImpl(
     private val storage: SessionStorage = SessionStorage(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index edef5fb..4a6599a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -36,7 +36,6 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.navigationBars
 import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -97,6 +96,7 @@
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
 import com.android.systemui.statusbar.phone.ui.TintedIconManager
@@ -138,6 +138,7 @@
     private val shadeSession: SaveableSession,
     private val notificationStackScrollView: Lazy<NotificationScrollView>,
     private val viewModel: ShadeSceneViewModel,
+    private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
     private val tintedIconManagerFactory: TintedIconManager.Factory,
     private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
     private val statusBarIconController: StatusBarIconController,
@@ -157,6 +158,7 @@
         ShadeScene(
             notificationStackScrollView.get(),
             viewModel = viewModel,
+            notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
             createTintedIconManager = tintedIconManagerFactory::create,
             createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
             statusBarIconController = statusBarIconController,
@@ -177,6 +179,7 @@
 private fun SceneScope.ShadeScene(
     notificationStackScrollView: NotificationScrollView,
     viewModel: ShadeSceneViewModel,
+    notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
     createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
     createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
     statusBarIconController: StatusBarIconController,
@@ -191,6 +194,7 @@
             SingleShade(
                 notificationStackScrollView = notificationStackScrollView,
                 viewModel = viewModel,
+                notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
                 createTintedIconManager = createTintedIconManager,
                 createBatteryMeterViewController = createBatteryMeterViewController,
                 statusBarIconController = statusBarIconController,
@@ -203,6 +207,7 @@
             SplitShade(
                 notificationStackScrollView = notificationStackScrollView,
                 viewModel = viewModel,
+                notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
                 createTintedIconManager = createTintedIconManager,
                 createBatteryMeterViewController = createBatteryMeterViewController,
                 statusBarIconController = statusBarIconController,
@@ -219,6 +224,7 @@
 private fun SceneScope.SingleShade(
     notificationStackScrollView: NotificationScrollView,
     viewModel: ShadeSceneViewModel,
+    notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
     createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
     createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
     statusBarIconController: StatusBarIconController,
@@ -330,7 +336,7 @@
                         NotificationScrollingStack(
                             shadeSession = shadeSession,
                             stackScrollView = notificationStackScrollView,
-                            viewModel = viewModel.notifications,
+                            viewModel = notificationsPlaceholderViewModel,
                             maxScrimTop = { maxNotifScrimTop.value },
                             shadeMode = ShadeMode.Single,
                             shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
@@ -354,7 +360,7 @@
         }
         NotificationStackCutoffGuideline(
             stackScrollView = notificationStackScrollView,
-            viewModel = viewModel.notifications,
+            viewModel = notificationsPlaceholderViewModel,
             modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding()
         )
     }
@@ -364,6 +370,7 @@
 private fun SceneScope.SplitShade(
     notificationStackScrollView: NotificationScrollView,
     viewModel: ShadeSceneViewModel,
+    notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
     createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
     createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
     statusBarIconController: StatusBarIconController,
@@ -431,8 +438,10 @@
             label = "alphaAnimationBrightnessMirrorContentHiding",
         )
 
-    viewModel.notifications.setAlphaForBrightnessMirror(contentAlpha)
-    DisposableEffect(Unit) { onDispose { viewModel.notifications.setAlphaForBrightnessMirror(1f) } }
+    notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(contentAlpha)
+    DisposableEffect(Unit) {
+        onDispose { notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(1f) }
+    }
 
     val isMediaVisible by viewModel.isMediaVisible.collectAsStateWithLifecycle()
 
@@ -474,9 +483,9 @@
                     BrightnessMirror(
                         viewModel = viewModel.brightnessMirrorViewModel,
                         qsSceneAdapter = viewModel.qsSceneAdapter,
-                        // Need to remove the offset of the header height, as the mirror uses
-                        // the position of the Brightness slider in the window
-                        modifier = Modifier.offset(y = -ShadeHeader.Dimensions.CollapsedHeight)
+                        // Need to use the offset measured from the container as the header
+                        // has to be accounted for
+                        measureFromContainer = true
                     )
                     Column(
                         verticalArrangement = Arrangement.Top,
@@ -533,7 +542,7 @@
                 NotificationScrollingStack(
                     shadeSession = shadeSession,
                     stackScrollView = notificationStackScrollView,
-                    viewModel = viewModel.notifications,
+                    viewModel = notificationsPlaceholderViewModel,
                     maxScrimTop = { 0f },
                     shouldPunchHoleBehindScrim = false,
                     shouldReserveSpaceForNavBar = false,
@@ -548,7 +557,7 @@
         }
         NotificationStackCutoffGuideline(
             stackScrollView = notificationStackScrollView,
-            viewModel = viewModel.notifications,
+            viewModel = notificationsPlaceholderViewModel,
             modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding()
         )
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 630bcd6..7ebc224 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
@@ -49,6 +50,7 @@
 import com.android.systemui.ambient.touch.scrim.ScrimController;
 import com.android.systemui.ambient.touch.scrim.ScrimManager;
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
+import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.FakeUserTracker;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shared.system.InputChannelCompat;
@@ -113,6 +115,9 @@
     LockPatternUtils mLockPatternUtils;
 
     @Mock
+    ActivityStarter mActivityStarter;
+
+    @Mock
     Region mRegion;
 
     @Captor
@@ -148,7 +153,8 @@
                 mFlingAnimationUtilsClosing,
                 TOUCH_REGION,
                 MIN_BOUNCER_HEIGHT,
-                mUiEventLogger);
+                mUiEventLogger,
+                mActivityStarter);
 
         when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
         when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
@@ -397,7 +403,12 @@
                 .isTrue();
         // We should not expand since the keyguard is not secure
         verify(mScrimController, never()).expand(any());
-        // Since we are swiping up, we should wake from dreams.
+
+        // Since we are swiping up, we should dismiss the keyguard and wake from dreams.
+        ArgumentCaptor<Runnable> dismissKeyguardRunnable = ArgumentCaptor.forClass(Runnable.class);
+        verify(mActivityStarter).executeRunnableDismissingKeyguard(
+                dismissKeyguardRunnable.capture(), isNull(), eq(true), eq(true), eq(false));
+        dismissKeyguardRunnable.getValue().run();
         verify(mCentralSurfaces).awakenDreams();
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 60b48f2..242e822 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -324,7 +324,6 @@
     fun showUdfpsOverlay_awake() =
         testScope.runTest {
             withReason(REASON_AUTH_KEYGUARD) {
-                mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
                 powerRepository.updateWakefulness(
                     rawState = WakefulnessState.AWAKE,
                     lastWakeReason = WakeSleepReason.POWER_BUTTON,
@@ -341,7 +340,6 @@
     fun showUdfpsOverlay_whileGoingToSleep() =
         testScope.runTest {
             withReasonSuspend(REASON_AUTH_KEYGUARD) {
-                mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
                 keyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.OFF,
                     to = KeyguardState.GONE,
@@ -370,7 +368,6 @@
     fun showUdfpsOverlay_whileAsleep() =
         testScope.runTest {
             withReasonSuspend(REASON_AUTH_KEYGUARD) {
-                mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
                 keyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.OFF,
                     to = KeyguardState.GONE,
@@ -399,7 +396,6 @@
     fun neverRemoveViewThatHasNotBeenAdded() =
         testScope.runTest {
             withReasonSuspend(REASON_AUTH_KEYGUARD) {
-                mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
                 controllerOverlay.show(udfpsController, overlayParams)
                 val view = controllerOverlay.getTouchOverlay()
                 view?.let {
@@ -414,7 +410,6 @@
     fun showUdfpsOverlay_afterFinishedTransitioningToAOD() =
         testScope.runTest {
             withReasonSuspend(REASON_AUTH_KEYGUARD) {
-                mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
                 keyguardTransitionRepository.sendTransitionSteps(
                     from = KeyguardState.OFF,
                     to = KeyguardState.GONE,
@@ -542,7 +537,6 @@
     fun addViewPending_layoutIsNotUpdated() =
         testScope.runTest {
             withReasonSuspend(REASON_AUTH_KEYGUARD) {
-                mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
                 mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
 
                 // GIVEN going to sleep
@@ -580,7 +574,6 @@
     fun updateOverlayParams_viewLayoutUpdated() =
         testScope.runTest {
             withReasonSuspend(REASON_AUTH_KEYGUARD) {
-                mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE)
                 powerRepository.updateWakefulness(
                     rawState = WakefulnessState.AWAKE,
                     lastWakeReason = WakeSleepReason.POWER_BUTTON,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 51991de..2694cab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -153,6 +153,7 @@
             CommunalViewModel(
                 kosmos.testDispatcher,
                 testScope,
+                kosmos.testScope.backgroundScope,
                 context.resources,
                 kosmos.keyguardTransitionInteractor,
                 kosmos.keyguardInteractor,
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 74eee9b..7d0f040 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
@@ -274,6 +274,29 @@
             assertThat(couldClick).isFalse()
         }
 
+    @Test
+    fun onTileClick_whileIdle_withQSTile_clicks() =
+        testWhileInState(QSLongPressEffect.State.IDLE) {
+            // GIVEN that a click was detected
+            val couldClick = longPressEffect.onTileClick()
+
+            // THEN the click is successful
+            assertThat(couldClick).isTrue()
+        }
+
+    @Test
+    fun onTileClick_whileIdle_withoutQSTile_cannotClick() =
+        testWhileInState(QSLongPressEffect.State.IDLE) {
+            // GIVEN that no QSTile has been set
+            longPressEffect.qsTile = null
+
+            // GIVEN that a click was detected
+            val couldClick = longPressEffect.onTileClick()
+
+            // THEN the click is not successful
+            assertThat(couldClick).isFalse()
+        }
+
     private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
similarity index 92%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
index 9d34903..96b4b43 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
+import com.android.systemui.shade.pulsingGestureListener
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
@@ -53,14 +54,14 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class KeyguardLongPressInteractorTest : SysuiTestCase() {
+class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
             this.accessibilityManagerWrapper = mock<AccessibilityManagerWrapper>()
             this.uiEventLogger = mock<UiEventLoggerFake>()
         }
 
-    private lateinit var underTest: KeyguardLongPressInteractor
+    private lateinit var underTest: KeyguardTouchHandlingInteractor
 
     private val logger = kosmos.uiEventLogger
     private val testScope = kosmos.testScope
@@ -209,7 +210,7 @@
             underTest.onLongPress()
             assertThat(isMenuVisible).isTrue()
 
-            advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
+            advanceTimeBy(KeyguardTouchHandlingInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
 
             assertThat(isMenuVisible).isFalse()
         }
@@ -224,11 +225,11 @@
             assertThat(isMenuVisible).isTrue()
             underTest.onMenuTouchGestureStarted()
 
-            advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
+            advanceTimeBy(KeyguardTouchHandlingInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
             assertThat(isMenuVisible).isTrue()
 
             underTest.onMenuTouchGestureEnded(/* isClick= */ false)
-            advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
+            advanceTimeBy(KeyguardTouchHandlingInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS)
             assertThat(isMenuVisible).isFalse()
         }
 
@@ -241,7 +242,7 @@
             underTest.onLongPress()
 
             verify(logger)
-                .log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
+                .log(KeyguardTouchHandlingInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
         }
 
     @Test
@@ -254,7 +255,7 @@
             underTest.onMenuTouchGestureEnded(/* isClick= */ true)
 
             verify(logger)
-                .log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
+                .log(KeyguardTouchHandlingInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
         }
 
     @Test
@@ -288,7 +289,7 @@
         // This needs to be re-created for each test outside of kosmos since the flag values are
         // read during initialization to set up flows. Maybe there is a better way to handle that.
         underTest =
-            KeyguardLongPressInteractor(
+            KeyguardTouchHandlingInteractor(
                 appContext = mContext,
                 scope = testScope.backgroundScope,
                 transitionInteractor = kosmos.keyguardTransitionInteractor,
@@ -300,7 +301,8 @@
                         set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, isOpenWppDirectlyEnabled)
                     },
                 broadcastDispatcher = fakeBroadcastDispatcher,
-                accessibilityManager = kosmos.accessibilityManagerWrapper
+                accessibilityManager = kosmos.accessibilityManagerWrapper,
+                pulsingGestureListener = kosmos.pulsingGestureListener,
             )
         setUpState()
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 61d8216..4eb146d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -19,6 +19,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.platform.test.annotations.EnableFlags
+import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.SceneKey
@@ -61,6 +62,7 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
+@RunWithLooper
 @EnableSceneContainer
 class LockscreenSceneViewModelTest : SysuiTestCase() {
 
@@ -265,8 +267,8 @@
             applicationScope = testScope.backgroundScope,
             deviceEntryInteractor = kosmos.deviceEntryInteractor,
             communalInteractor = kosmos.communalInteractor,
-            longPress =
-                KeyguardLongPressViewModel(
+            touchHandling =
+                KeyguardTouchHandlingViewModel(
                     interactor = mock(),
                 ),
             notifications = kosmos.notificationsPlaceholderViewModel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
new file mode 100644
index 0000000..1c3021e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DragAndDropStateTest : SysuiTestCase() {
+    private val listState = EditTileListState(TestEditTiles)
+    private val underTest = DragAndDropState(mutableStateOf(null), listState)
+
+    @Test
+    fun isMoving_returnsCorrectValue() {
+        // Asserts no tiles is moving
+        TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() }
+
+        // Start the drag movement
+        val movingTileSpec = TestEditTiles[0].tileSpec
+        underTest.onStarted(movingTileSpec)
+
+        // Assert that the correct tile is marked as moving
+        TestEditTiles.forEach {
+            assertThat(underTest.isMoving(it.tileSpec)).isEqualTo(movingTileSpec == it.tileSpec)
+        }
+    }
+
+    @Test
+    fun onMoved_updatesList() {
+        val movingTileSpec = TestEditTiles[0].tileSpec
+
+        // Start the drag movement
+        underTest.onStarted(movingTileSpec)
+
+        // Move the tile to the end of the list
+        underTest.onMoved(listState.tiles[5].tileSpec)
+        assertThat(underTest.currentPosition()).isEqualTo(5)
+
+        // Move the tile to the middle of the list
+        underTest.onMoved(listState.tiles[2].tileSpec)
+        assertThat(underTest.currentPosition()).isEqualTo(2)
+    }
+
+    @Test
+    fun onDrop_resetsMovingTile() {
+        val movingTileSpec = TestEditTiles[0].tileSpec
+
+        // Start the drag movement
+        underTest.onStarted(movingTileSpec)
+
+        // Move the tile to the end of the list
+        underTest.onMoved(listState.tiles[5].tileSpec)
+
+        // Drop the tile
+        underTest.onDrop()
+
+        // Asserts no tiles is moving
+        TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() }
+    }
+
+    companion object {
+        private fun createEditTile(tileSpec: String): EditTileViewModel {
+            return EditTileViewModel(
+                tileSpec = TileSpec.create(tileSpec),
+                icon = Icon.Resource(0, null),
+                label = Text.Loaded("unused"),
+                appName = null,
+                isCurrent = true,
+                availableEditActions = emptySet(),
+            )
+        }
+
+        private val TestEditTiles =
+            listOf(
+                createEditTile("tileA"),
+                createEditTile("tileB"),
+                createEditTile("tileC"),
+                createEditTile("tileD"),
+                createEditTile("tileE"),
+                createEditTile("tileF"),
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
new file mode 100644
index 0000000..517b601
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class EditTileListStateTest : SysuiTestCase() {
+    val underTest = EditTileListState(TestEditTiles)
+
+    @Test
+    fun movingNonExistentTile_listUnchanged() {
+        underTest.move(TileSpec.create("other_tile"), TestEditTiles[0].tileSpec)
+
+        assertThat(underTest.tiles).containsExactly(*TestEditTiles.toTypedArray())
+    }
+
+    @Test
+    fun movingTileToNonExistentTarget_listUnchanged() {
+        underTest.move(TestEditTiles[0].tileSpec, TileSpec.create("other_tile"))
+
+        assertThat(underTest.tiles).containsExactly(*TestEditTiles.toTypedArray())
+    }
+
+    @Test
+    fun movingTileToItself_listUnchanged() {
+        underTest.move(TestEditTiles[0].tileSpec, TestEditTiles[0].tileSpec)
+
+        assertThat(underTest.tiles).containsExactly(*TestEditTiles.toTypedArray())
+    }
+
+    @Test
+    fun movingTileToSameSection_listUpdates() {
+        // Move tile at index 0 to index 1. Tile 0 should remain current.
+        underTest.move(TestEditTiles[0].tileSpec, TestEditTiles[1].tileSpec)
+
+        // Assert the tiles 0 and 1 have changed places.
+        assertThat(underTest.tiles[0]).isEqualTo(TestEditTiles[1])
+        assertThat(underTest.tiles[1]).isEqualTo(TestEditTiles[0])
+
+        // Assert the rest of the list is unchanged
+        assertThat(underTest.tiles.subList(2, 5))
+            .containsExactly(*TestEditTiles.subList(2, 5).toTypedArray())
+    }
+
+    @Test
+    fun movingTileToDifferentSection_listAndTileUpdates() {
+        // Move tile at index 0 to index 3. Tile 0 should no longer be current.
+        underTest.move(TestEditTiles[0].tileSpec, TestEditTiles[3].tileSpec)
+
+        // Assert tile 0 is now at index 3 and is no longer current.
+        assertThat(underTest.tiles[3]).isEqualTo(TestEditTiles[0].copy(isCurrent = false))
+
+        // Assert previous tiles have shifted places
+        assertThat(underTest.tiles[0]).isEqualTo(TestEditTiles[1])
+        assertThat(underTest.tiles[1]).isEqualTo(TestEditTiles[2])
+        assertThat(underTest.tiles[2]).isEqualTo(TestEditTiles[3])
+
+        // Assert the rest of the list is unchanged
+        assertThat(underTest.tiles.subList(4, 5))
+            .containsExactly(*TestEditTiles.subList(4, 5).toTypedArray())
+    }
+
+    companion object {
+        private fun createEditTile(tileSpec: String, isCurrent: Boolean): EditTileViewModel {
+            return EditTileViewModel(
+                tileSpec = TileSpec.create(tileSpec),
+                icon = Icon.Resource(0, null),
+                label = Text.Loaded("unused"),
+                appName = null,
+                isCurrent = isCurrent,
+                availableEditActions = emptySet(),
+            )
+        }
+
+        private val TestEditTiles =
+            listOf(
+                createEditTile("tileA", true),
+                createEditTile("tileB", true),
+                createEditTile("tileC", true),
+                createEditTile("tileD", false),
+                createEditTile("tileE", false),
+                createEditTile("tileF", false),
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index e01ffa6..9249621 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.ui.viewmodel
 
+import android.testing.TestableLooper.RunWithLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.Back
@@ -65,6 +66,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@RunWithLooper
 @EnableSceneContainer
 class QuickSettingsSceneViewModelTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 412505d..cb4d96f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -48,16 +48,13 @@
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.qs.footerActionsController
-import com.android.systemui.qs.footerActionsViewModelFactory
-import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
+import com.android.systemui.qs.ui.adapter.fakeQSSceneAdapter
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.domain.startable.sceneContainerStartable
@@ -65,16 +62,14 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
-import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModel
+import com.android.systemui.shade.ui.viewmodel.shadeSceneViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
 import com.android.systemui.telephony.data.repository.fakeTelephonyRepository
 import com.android.systemui.testKosmos
-import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -152,8 +147,8 @@
             applicationScope = testScope.backgroundScope,
             deviceEntryInteractor = deviceEntryInteractor,
             communalInteractor = communalInteractor,
-            longPress =
-                KeyguardLongPressViewModel(
+            touchHandling =
+                KeyguardTouchHandlingViewModel(
                     interactor = mock(),
                 ),
             notifications = kosmos.notificationsPlaceholderViewModel,
@@ -167,7 +162,7 @@
 
     private var bouncerSceneJob: Job? = null
 
-    private val qsFlexiglassAdapter = FakeQSSceneAdapter(inflateDelegate = { mock() })
+    private val qsFlexiglassAdapter = kosmos.fakeQSSceneAdapter
 
     private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
     private lateinit var telecomManager: TelecomManager
@@ -200,20 +195,7 @@
         bouncerActionButtonInteractor = kosmos.bouncerActionButtonInteractor
         bouncerViewModel = kosmos.bouncerViewModel
 
-        shadeSceneViewModel =
-            ShadeSceneViewModel(
-                applicationScope = testScope.backgroundScope,
-                shadeHeaderViewModel = kosmos.shadeHeaderViewModel,
-                qsSceneAdapter = qsFlexiglassAdapter,
-                notifications = kosmos.notificationsPlaceholderViewModel,
-                brightnessMirrorViewModel = kosmos.brightnessMirrorViewModel,
-                mediaCarouselInteractor = kosmos.mediaCarouselInteractor,
-                shadeInteractor = kosmos.shadeInteractor,
-                footerActionsController = kosmos.footerActionsController,
-                footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory,
-                sceneInteractor = sceneInteractor,
-                unfoldTransitionInteractor = kosmos.unfoldTransitionInteractor,
-            )
+        shadeSceneViewModel = kosmos.shadeSceneViewModel
 
         val startable = kosmos.sceneContainerStartable
         startable.start()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflaterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflaterTest.kt
index 6de7f40..13b61bc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflaterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflaterTest.kt
@@ -68,4 +68,27 @@
 
         Assert.setTestThread(null)
     }
+
+    @Test
+    fun inflate_frameHasPadding() {
+        Assert.setTestThread(Thread.currentThread())
+
+        val (frame, _) =
+            BrightnessMirrorInflater.inflate(
+                themedContext,
+                kosmos.brightnessSliderControllerFactory,
+            )
+
+        assertThat(frame.visibility).isEqualTo(View.VISIBLE)
+
+        val padding =
+            context.resources.getDimensionPixelSize(R.dimen.rounded_slider_background_padding)
+
+        assertThat(frame.paddingLeft).isEqualTo(padding)
+        assertThat(frame.paddingTop).isEqualTo(padding)
+        assertThat(frame.paddingRight).isEqualTo(padding)
+        assertThat(frame.paddingBottom).isEqualTo(padding)
+
+        Assert.setTestThread(null)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelTest.kt
index 09fc6f9..90c11d4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelTest.kt
@@ -16,10 +16,9 @@
 
 package com.android.systemui.settings.brightness.ui.viewmodel
 
-import android.content.applicationContext
 import android.content.res.mainResources
-import android.view.ContextThemeWrapper
 import android.view.View
+import android.widget.FrameLayout
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -27,12 +26,10 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor
-import com.android.systemui.settings.brightness.ui.binder.BrightnessMirrorInflater
 import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
 import com.android.systemui.settings.brightness.ui.viewModel.LocationAndSize
 import com.android.systemui.settings.brightnessSliderControllerFactory
 import com.android.systemui.testKosmos
-import com.android.systemui.util.Assert
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -47,9 +44,6 @@
 
     private val kosmos = testKosmos()
 
-    private val themedContext =
-        ContextThemeWrapper(kosmos.applicationContext, R.style.Theme_SystemUI_QuickSettings)
-
     private val underTest =
         with(kosmos) {
             BrightnessMirrorViewModel(
@@ -76,7 +70,7 @@
         }
 
     @Test
-    fun setLocationInWindow_correctLocationAndSize() =
+    fun locationInWindowAndContainer_correctLocationAndSize() =
         with(kosmos) {
             testScope.runTest {
                 val locationAndSize by collectLastValue(underTest.locationAndSize)
@@ -101,6 +95,7 @@
                         whenever(measuredHeight).thenReturn(height)
                         whenever(measuredWidth).thenReturn(width)
                     }
+                val yOffsetFromContainer = setContainerViewHierarchy(mockView)
 
                 underTest.setLocationAndSize(mockView)
 
@@ -108,7 +103,8 @@
                     .isEqualTo(
                         // Adjust for padding around the view
                         LocationAndSize(
-                            yOffset = y - padding,
+                            yOffsetFromWindow = y - padding,
+                            yOffsetFromContainer = yOffsetFromContainer - padding,
                             width = width + 2 * padding,
                             height = height + 2 * padding,
                         )
@@ -116,31 +112,20 @@
             }
         }
 
-    @Test
-    fun setLocationInWindow_paddingSetToRootView() =
-        with(kosmos) {
-            Assert.setTestThread(Thread.currentThread())
-            val padding =
-                mainResources.getDimensionPixelSize(R.dimen.rounded_slider_background_padding)
+    private fun setContainerViewHierarchy(mockView: View): Int {
+        val rootView = FrameLayout(context)
+        val containerView = FrameLayout(context).apply { id = R.id.quick_settings_container }
+        val otherView = FrameLayout(context)
 
-            val view = mock<View>()
+        rootView.addView(containerView)
+        containerView.addView(otherView)
+        otherView.addView(mockView)
 
-            val (_, sliderController) =
-                BrightnessMirrorInflater.inflate(
-                    themedContext,
-                    brightnessSliderControllerFactory,
-                )
-            underTest.setToggleSlider(sliderController)
+        containerView.setLeftTopRightBottom(1, /* top= */ 1, 1, 1)
+        otherView.setLeftTopRightBottom(0, /* top= */ 2, 0, 0)
+        whenever(mockView.parent).thenReturn(otherView)
+        whenever(mockView.top).thenReturn(3)
 
-            underTest.setLocationAndSize(view)
-
-            with(sliderController.rootView) {
-                assertThat(paddingBottom).isEqualTo(padding)
-                assertThat(paddingTop).isEqualTo(padding)
-                assertThat(paddingLeft).isEqualTo(padding)
-                assertThat(paddingRight).isEqualTo(padding)
-            }
-
-            Assert.setTestThread(null)
-        }
+        return 2 + 3
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index a0295c9..673d5ef 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -36,28 +36,20 @@
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.data.repository.mediaFilterRepository
-import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
-import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
 import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.qs.footerActionsController
-import com.android.systemui.qs.footerActionsViewModelFactory
-import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
+import com.android.systemui.qs.ui.adapter.fakeQSSceneAdapter
+import com.android.systemui.qs.ui.adapter.qsSceneAdapter
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
-import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
 import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.domain.startable.shadeStartable
 import com.android.systemui.shade.shared.model.ShadeMode
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.testKosmos
-import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
 import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
-import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import java.util.Locale
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -66,11 +58,8 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -83,32 +72,9 @@
     private val testScope = kosmos.testScope
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val shadeRepository by lazy { kosmos.shadeRepository }
+    private val qsSceneAdapter by lazy { kosmos.fakeQSSceneAdapter }
 
-    private val qsSceneAdapter = FakeQSSceneAdapter({ mock() })
-
-    private lateinit var underTest: ShadeSceneViewModel
-
-    @Mock private lateinit var mediaDataManager: MediaDataManager
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest =
-            ShadeSceneViewModel(
-                applicationScope = testScope.backgroundScope,
-                shadeHeaderViewModel = kosmos.shadeHeaderViewModel,
-                qsSceneAdapter = qsSceneAdapter,
-                notifications = kosmos.notificationsPlaceholderViewModel,
-                brightnessMirrorViewModel = kosmos.brightnessMirrorViewModel,
-                mediaCarouselInteractor = kosmos.mediaCarouselInteractor,
-                shadeInteractor = kosmos.shadeInteractor,
-                footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory,
-                footerActionsController = kosmos.footerActionsController,
-                sceneInteractor = kosmos.sceneInteractor,
-                unfoldTransitionInteractor = kosmos.unfoldTransitionInteractor,
-            )
-    }
+    private val underTest: ShadeSceneViewModel by lazy { kosmos.shadeSceneViewModel }
 
     @Test
     fun upTransitionSceneKey_deviceLocked_lockScreen() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index cc3fdc5..23b28e3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.statusbar.notification
 
+import android.testing.TestableLooper.RunWithLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
@@ -48,6 +49,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@RunWithLooper
 @EnableSceneContainer
 class NotificationStackAppearanceIntegrationTest : SysuiTestCase() {
 
@@ -171,6 +173,22 @@
         }
 
     @Test
+    fun shadeExpansion_idleOnQs() =
+        testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(currentScene = Scenes.QuickSettings)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            val expandFraction by collectLastValue(scrollViewModel.expandFraction)
+            assertThat(expandFraction).isEqualTo(1f)
+
+            fakeSceneDataSource.changeScene(toScene = Scenes.QuickSettings)
+            val isScrollable by collectLastValue(scrollViewModel.isScrollable)
+            assertThat(isScrollable).isFalse()
+        }
+
+    @Test
     fun shadeExpansion_shadeToQs() =
         testScope.runTest {
             val transitionState =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
index ee9fd349..4944c8b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
+import android.testing.TestableLooper.RunWithLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -31,9 +32,10 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@RunWithLooper
 class NotificationsPlaceholderViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
-    private val underTest = kosmos.notificationsPlaceholderViewModel
+    private val underTest by lazy { kosmos.notificationsPlaceholderViewModel }
 
     @Test
     fun onBoundsChanged() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
index 200e92e..7346323 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.os.Handler;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper;
 
@@ -85,6 +86,8 @@
     @Mock private DumpManager dumpManager;
     private AvalancheController mAvalancheController;
 
+    @Mock private Handler mBgHandler;
+
     private static final class TestableHeadsUpManagerPhone extends HeadsUpManagerPhone {
         TestableHeadsUpManagerPhone(
                 Context context,
@@ -101,7 +104,8 @@
                 UiEventLogger uiEventLogger,
                 JavaAdapter javaAdapter,
                 ShadeInteractor shadeInteractor,
-                AvalancheController avalancheController
+                AvalancheController avalancheController,
+                Handler bgHandler
         ) {
             super(
                     context,
@@ -119,7 +123,8 @@
                     uiEventLogger,
                     javaAdapter,
                     shadeInteractor,
-                    avalancheController
+                    avalancheController,
+                    bgHandler
             );
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissTime = TEST_AUTO_DISMISS_TIME;
@@ -142,7 +147,8 @@
                 mUiEventLogger,
                 mJavaAdapter,
                 mShadeInteractor,
-                mAvalancheController
+                mAvalancheController,
+                mBgHandler
         );
     }
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2f61b12..ff43c9bc 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1393,7 +1393,7 @@
     <!-- Text which is shown in the expanded notification shade when there are currently no notifications visible that the user hasn't already seen. [CHAR LIMIT=30] -->
     <string name="no_unseen_notif_text">No new notifications</string>
 
-    <!-- Title of heads up notification for adaptive notifications user education. [CHAR LIMIT=30] -->
+    <!-- Title of heads up notification for adaptive notifications user education. [CHAR LIMIT=50] -->
     <string name="adaptive_notification_edu_hun_title">Adaptive notifications is on</string>
 
     <!-- Text of heads up notification for adaptive notifications user education. [CHAR LIMIT=100] -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index d5bc10a..c00007b 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -22,7 +22,7 @@
 import android.view.MotionEvent;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 
-// Next ID: 29
+// Next ID: 34
 oneway interface IOverviewProxy {
 
     void onActiveNavBarRegionChanges(in Region activeRegion) = 11;
@@ -83,6 +83,11 @@
     void onSystemBarAttributesChanged(int displayId, int behavior) = 20;
 
     /**
+     * Sent when {@link TaskbarDelegate#onTransitionModeUpdated} is called.
+     */
+    void onTransitionModeUpdated(int barMode, boolean checkBarModes) = 21;
+
+    /**
      * Sent when the desired dark intensity of the nav buttons has changed
      */
     void onNavButtonsDarkIntensityChanged(float darkIntensity) = 22;
@@ -101,4 +106,30 @@
      * Sent when the task bar stash state is toggled.
      */
     void onTaskbarToggled() = 27;
+
+    /**
+     * Sent when the wallpaper visibility is updated.
+     */
+    void updateWallpaperVisibility(int displayId, boolean visible) = 29;
+
+    /**
+     * Sent when {@link TaskbarDelegate#checkNavBarModes} is called.
+     */
+    void checkNavBarModes() = 30;
+
+    /**
+     * Sent when {@link TaskbarDelegate#finishBarAnimations} is called.
+     */
+    void finishBarAnimations() = 31;
+
+    /**
+     * Sent when {@link TaskbarDelegate#touchAutoDim} is called. {@param reset} is true, when auto
+     * dim is reset after a timeout.
+     */
+    void touchAutoDim(boolean reset) = 32;
+
+    /**
+     * Sent when {@link TaskbarDelegate#transitionTo} is called.
+     */
+    void transitionTo(int barMode, boolean animate) = 33;
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java b/packages/SystemUI/shared/src/com/android/systemui/shared/statusbar/phone/BarTransitions.java
similarity index 87%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java
rename to packages/SystemUI/shared/src/com/android/systemui/shared/statusbar/phone/BarTransitions.java
index f62a79f..c123306 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BarTransitions.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/statusbar/phone/BarTransitions.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 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,11 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone;
+package com.android.systemui.shared.statusbar.phone;
 
+import android.annotation.ColorInt;
 import android.annotation.IntDef;
 import android.content.Context;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
@@ -34,8 +36,6 @@
 import android.view.View;
 
 import com.android.app.animation.Interpolators;
-import com.android.settingslib.Utils;
-import com.android.systemui.res.R;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -44,6 +44,11 @@
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_COLORS = false;
 
+    @ColorInt
+    private static final int SYSTEM_BAR_BACKGROUND_OPAQUE = Color.BLACK;
+    @ColorInt
+    private static final int SYSTEM_BAR_BACKGROUND_TRANSPARENT = Color.TRANSPARENT;
+
     public static final int MODE_TRANSPARENT = 0;
     public static final int MODE_SEMI_TRANSPARENT = 1;
     public static final int MODE_TRANSLUCENT = 2;
@@ -183,11 +188,11 @@
                 mTransparent = 0x2f0000ff;
                 mWarning = 0xffff0000;
             } else {
-                mOpaque = context.getColor(R.color.system_bar_background_opaque);
+                mOpaque = SYSTEM_BAR_BACKGROUND_OPAQUE;
                 mSemiTransparent = context.getColor(
                         com.android.internal.R.color.system_bar_background_semi_transparent);
-                mTransparent = context.getColor(R.color.system_bar_background_transparent);
-                mWarning = Utils.getColorAttrDefaultColor(context, android.R.attr.colorError);
+                mTransparent = SYSTEM_BAR_BACKGROUND_TRANSPARENT;
+                mWarning = getColorAttrDefaultColor(context, android.R.attr.colorError, 0);
             }
             mGradient = context.getDrawable(gradientResourceId);
         }
@@ -226,7 +231,7 @@
         @Override
         public void setTint(int color) {
             PorterDuff.Mode targetMode = mTintFilter == null ? Mode.SRC_IN :
-                mTintFilter.getMode();
+                    mTintFilter.getMode();
             if (mTintFilter == null || mTintFilter.getColor() != color) {
                 mTintFilter = new PorterDuffColorFilter(color, targetMode);
             }
@@ -304,10 +309,13 @@
                             Interpolators.LINEAR.getInterpolation(t), 1));
                     mGradientAlpha = (int)(v * targetGradientAlpha + mGradientAlphaStart * (1 - v));
                     mColor = Color.argb(
-                          (int)(v * Color.alpha(targetColor) + Color.alpha(mColorStart) * (1 - v)),
-                          (int)(v * Color.red(targetColor) + Color.red(mColorStart) * (1 - v)),
-                          (int)(v * Color.green(targetColor) + Color.green(mColorStart) * (1 - v)),
-                          (int)(v * Color.blue(targetColor) + Color.blue(mColorStart) * (1 - v)));
+                            (int) (v * Color.alpha(targetColor) + Color.alpha(mColorStart) * (1
+                                    - v)),
+                            (int) (v * Color.red(targetColor) + Color.red(mColorStart) * (1 - v)),
+                            (int) (v * Color.green(targetColor) + Color.green(mColorStart) * (1
+                                    - v)),
+                            (int) (v * Color.blue(targetColor) + Color.blue(mColorStart) * (1
+                                    - v)));
                 }
             }
             if (mGradientAlpha > 0) {
@@ -332,4 +340,13 @@
             }
         }
     }
+
+    /** Get color styled attribute {@code attr}, default to {@code defValue} if not found. */
+    @ColorInt
+    public static int getColorAttrDefaultColor(Context context, int attr, @ColorInt int defValue) {
+        TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
+        @ColorInt int colorAccent = ta.getColor(0, defValue);
+        ta.recycle();
+        return colorAccent;
+    }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index c0c8b75..a614fc1 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -124,6 +124,8 @@
     public static final long SYSUI_STATE_SHORTCUT_HELPER_SHOWING = 1L << 32;
     // Touchpad gestures are disabled
     public static final long SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED = 1L << 33;
+    // PiP animation is running
+    public static final long SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING = 1L << 34;
 
     // Mask for SystemUiStateFlags to isolate SYSUI_STATE_AWAKE and
     // SYSUI_STATE_WAKEFULNESS_TRANSITION, to match WAKEFULNESS_* constants
@@ -173,6 +175,7 @@
             SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY,
             SYSUI_STATE_SHORTCUT_HELPER_SHOWING,
             SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED,
+            SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING,
     })
     public @interface SystemUiStateFlags {}
 
@@ -277,6 +280,9 @@
         if ((flags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) != 0) {
             str.add("touchpad_gestures_disabled");
         }
+        if ((flags & SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING) != 0) {
+            str.add("disable_gesture_pip_animating");
+        }
 
         return str.toString();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java
index f905241..636bc5b 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java
@@ -38,6 +38,7 @@
 import com.android.systemui.ambient.touch.scrim.ScrimController;
 import com.android.systemui.ambient.touch.scrim.ScrimManager;
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
+import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -103,6 +104,8 @@
 
     private final UiEventLogger mUiEventLogger;
 
+    private final ActivityStarter mActivityStarter;
+
     private final ScrimManager.Callback mScrimManagerCallback = new ScrimManager.Callback() {
         @Override
         public void onScrimControllerChanged(ScrimController controller) {
@@ -149,11 +152,16 @@
                         return true;
                     }
 
-                    // If scrolling up and keyguard is not locked, dismiss the dream since there's
-                    // no bouncer to show.
+                    // If scrolling up and keyguard is not locked, dismiss both keyguard and the
+                    // dream since there's no bouncer to show.
                     if (e1.getY() > e2.getY()
                             && !mLockPatternUtils.isSecure(mUserTracker.getUserId())) {
-                        mCentralSurfaces.get().awakenDreams();
+                        mActivityStarter.executeRunnableDismissingKeyguard(
+                                () -> mCentralSurfaces.get().awakenDreams(),
+                                /* cancelAction= */ null,
+                                /* dismissShade= */ true,
+                                /* afterKeyguardGone= */ true,
+                                /* deferred= */ false);
                         return true;
                     }
 
@@ -162,7 +170,6 @@
                     // bouncer. As that view's expansion shrinks, the bouncer appears. The bouncer
                     // is fully hidden at full expansion (1) and fully visible when fully collapsed
                     // (0).
-                    final float dragDownAmount = e2.getY() - e1.getY();
                     final float screenTravelPercentage = Math.abs(e1.getY() - e2.getY())
                             / mTouchSession.getBounds().height();
                     setPanelExpansion(1 - screenTravelPercentage);
@@ -216,7 +223,8 @@
             FlingAnimationUtils flingAnimationUtilsClosing,
             @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage,
             @Named(BouncerSwipeModule.MIN_BOUNCER_ZONE_SCREEN_PERCENTAGE) float minRegionPercentage,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            ActivityStarter activityStarter) {
         mCentralSurfaces = centralSurfaces;
         mScrimManager = scrimManager;
         mNotificationShadeWindowController = notificationShadeWindowController;
@@ -229,6 +237,7 @@
         mValueAnimatorCreator = valueAnimatorCreator;
         mVelocityTrackerFactory = velocityTrackerFactory;
         mUiEventLogger = uiEventLogger;
+        mActivityStarter = activityStarter;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 01cc33c..e03d160 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -45,7 +45,6 @@
 import androidx.annotation.LayoutRes
 import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.Flags.udfpsViewPerformance
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
@@ -82,67 +81,66 @@
 
 private const val TAG = "UdfpsControllerOverlay"
 
-@VisibleForTesting
-const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"
+@VisibleForTesting const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"
 
 /**
  * Keeps track of the overlay state and UI resources associated with a single FingerprintService
- * request. This state can persist across configuration changes via the [show] and [hide]
- * methods.
+ * request. This state can persist across configuration changes via the [show] and [hide] methods.
  */
 @ExperimentalCoroutinesApi
 @UiThread
-class UdfpsControllerOverlay @JvmOverloads constructor(
-        private val context: Context,
-        private val inflater: LayoutInflater,
-        private val windowManager: WindowManager,
-        private val accessibilityManager: AccessibilityManager,
-        private val statusBarStateController: StatusBarStateController,
-        private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
-        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-        private val dialogManager: SystemUIDialogManager,
-        private val dumpManager: DumpManager,
-        private val transitionController: LockscreenShadeTransitionController,
-        private val configurationController: ConfigurationController,
-        private val keyguardStateController: KeyguardStateController,
-        private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
-        private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
-        val requestId: Long,
-        @RequestReason val requestReason: Int,
-        private val controllerCallback: IUdfpsOverlayControllerCallback,
-        private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
-        private val activityTransitionAnimator: ActivityTransitionAnimator,
-        private val primaryBouncerInteractor: PrimaryBouncerInteractor,
-        private val alternateBouncerInteractor: AlternateBouncerInteractor,
-        private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
-        private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
-        private val transitionInteractor: KeyguardTransitionInteractor,
-        private val selectedUserInteractor: SelectedUserInteractor,
-        private val deviceEntryUdfpsTouchOverlayViewModel:
-            Lazy<DeviceEntryUdfpsTouchOverlayViewModel>,
-        private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>,
-        private val shadeInteractor: ShadeInteractor,
-        private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
-        private val powerInteractor: PowerInteractor,
-        @Application private val scope: CoroutineScope,
+class UdfpsControllerOverlay
+@JvmOverloads
+constructor(
+    private val context: Context,
+    private val inflater: LayoutInflater,
+    private val windowManager: WindowManager,
+    private val accessibilityManager: AccessibilityManager,
+    private val statusBarStateController: StatusBarStateController,
+    private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val dialogManager: SystemUIDialogManager,
+    private val dumpManager: DumpManager,
+    private val transitionController: LockscreenShadeTransitionController,
+    private val configurationController: ConfigurationController,
+    private val keyguardStateController: KeyguardStateController,
+    private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
+    private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
+    val requestId: Long,
+    @RequestReason val requestReason: Int,
+    private val controllerCallback: IUdfpsOverlayControllerCallback,
+    private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
+    private val activityTransitionAnimator: ActivityTransitionAnimator,
+    private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+    private val alternateBouncerInteractor: AlternateBouncerInteractor,
+    private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
+    private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
+    private val transitionInteractor: KeyguardTransitionInteractor,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val deviceEntryUdfpsTouchOverlayViewModel: Lazy<DeviceEntryUdfpsTouchOverlayViewModel>,
+    private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>,
+    private val shadeInteractor: ShadeInteractor,
+    private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
+    private val powerInteractor: PowerInteractor,
+    @Application private val scope: CoroutineScope,
 ) {
     private val currentStateUpdatedToOffAodOrDozing: Flow<Unit> =
         transitionInteractor.currentKeyguardState
             .filter {
-                it == KeyguardState.OFF ||
-                    it == KeyguardState.AOD ||
-                    it == KeyguardState.DOZING
+                it == KeyguardState.OFF || it == KeyguardState.AOD || it == KeyguardState.DOZING
             }
-            .map { } // map to Unit
+            .map {} // map to Unit
     private var listenForCurrentKeyguardState: Job? = null
     private var addViewRunnable: Runnable? = null
     private var overlayViewLegacy: UdfpsView? = null
         private set
+
     private var overlayTouchView: UdfpsTouchOverlay? = null
 
     /**
-     * Get the current UDFPS overlay touch view which is a different View depending on whether
-     * the DeviceEntryUdfpsRefactor flag is enabled or not.
+     * Get the current UDFPS overlay touch view which is a different View depending on whether the
+     * DeviceEntryUdfpsRefactor flag is enabled or not.
+     *
      * @return The view, when [isShowing], else null
      */
     fun getTouchOverlay(): View? {
@@ -158,23 +156,28 @@
 
     private var overlayTouchListener: TouchExplorationStateChangeListener? = null
 
-    private val coreLayoutParams = WindowManager.LayoutParams(
-        WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
-        0 /* flags set in computeLayoutParams() */,
-        PixelFormat.TRANSLUCENT
-    ).apply {
-        title = TAG
-        fitInsetsTypes = 0
-        gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
-        layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-        flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or
-                WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
-        privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY or
-                WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION
-        // Avoid announcing window title.
-        accessibilityTitle = " "
-        inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
-    }
+    private val coreLayoutParams =
+        WindowManager.LayoutParams(
+                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+                0 /* flags set in computeLayoutParams() */,
+                PixelFormat.TRANSLUCENT
+            )
+            .apply {
+                title = TAG
+                fitInsetsTypes = 0
+                gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
+                layoutInDisplayCutoutMode =
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                flags =
+                    (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or
+                        WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
+                privateFlags =
+                    WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY or
+                        WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION
+                // Avoid announcing window title.
+                accessibilityTitle = " "
+                inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
+            }
 
     /** If the overlay is currently showing. */
     val isShowing: Boolean
@@ -209,51 +212,51 @@
             sensorBounds = Rect(params.sensorBounds)
             try {
                 if (DeviceEntryUdfpsRefactor.isEnabled) {
-                    overlayTouchView = (inflater.inflate(
-                            R.layout.udfps_touch_overlay, null, false
-                    ) as UdfpsTouchOverlay).apply {
-                        // This view overlaps the sensor area
-                        // prevent it from being selectable during a11y
-                        if (requestReason.isImportantForAccessibility()) {
-                            importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
-                        }
+                    overlayTouchView =
+                        (inflater.inflate(R.layout.udfps_touch_overlay, null, false)
+                                as UdfpsTouchOverlay)
+                            .apply {
+                                // This view overlaps the sensor area
+                                // prevent it from being selectable during a11y
+                                if (requestReason.isImportantForAccessibility()) {
+                                    importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+                                }
 
-                        addViewNowOrLater(this, null)
-                        when (requestReason) {
-                            REASON_AUTH_KEYGUARD ->
-                                UdfpsTouchOverlayBinder.bind(
-                                    view = this,
-                                    viewModel = deviceEntryUdfpsTouchOverlayViewModel.get(),
-                                    udfpsOverlayInteractor = udfpsOverlayInteractor,
-                                )
-                            else ->
-                                UdfpsTouchOverlayBinder.bind(
-                                    view = this,
-                                    viewModel = defaultUdfpsTouchOverlayViewModel.get(),
-                                    udfpsOverlayInteractor = udfpsOverlayInteractor,
-                                )
-                        }
-                    }
+                                addViewNowOrLater(this, null)
+                                when (requestReason) {
+                                    REASON_AUTH_KEYGUARD ->
+                                        UdfpsTouchOverlayBinder.bind(
+                                            view = this,
+                                            viewModel = deviceEntryUdfpsTouchOverlayViewModel.get(),
+                                            udfpsOverlayInteractor = udfpsOverlayInteractor,
+                                        )
+                                    else ->
+                                        UdfpsTouchOverlayBinder.bind(
+                                            view = this,
+                                            viewModel = defaultUdfpsTouchOverlayViewModel.get(),
+                                            udfpsOverlayInteractor = udfpsOverlayInteractor,
+                                        )
+                                }
+                            }
                 } else {
-                    overlayViewLegacy = (inflater.inflate(
-                            R.layout.udfps_view, null, false
-                    ) as UdfpsView).apply {
-                        overlayParams = params
-                        setUdfpsDisplayModeProvider(udfpsDisplayModeProvider)
-                        val animation = inflateUdfpsAnimation(this, controller)
-                        if (animation != null) {
-                            animation.init()
-                            animationViewController = animation
-                        }
-                        // This view overlaps the sensor area
-                        // prevent it from being selectable during a11y
-                        if (requestReason.isImportantForAccessibility()) {
-                            importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
-                        }
+                    overlayViewLegacy =
+                        (inflater.inflate(R.layout.udfps_view, null, false) as UdfpsView).apply {
+                            overlayParams = params
+                            setUdfpsDisplayModeProvider(udfpsDisplayModeProvider)
+                            val animation = inflateUdfpsAnimation(this, controller)
+                            if (animation != null) {
+                                animation.init()
+                                animationViewController = animation
+                            }
+                            // This view overlaps the sensor area
+                            // prevent it from being selectable during a11y
+                            if (requestReason.isImportantForAccessibility()) {
+                                importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+                            }
 
-                        addViewNowOrLater(this, animation)
-                        sensorRect = sensorBounds
-                    }
+                            addViewNowOrLater(this, animation)
+                            sensorRect = sensorBounds
+                        }
                 }
                 getTouchOverlay()?.apply {
                     touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
@@ -269,7 +272,7 @@
                         }
                     }
                     accessibilityManager.addTouchExplorationStateChangeListener(
-                            overlayTouchListener!!
+                        overlayTouchListener!!
                     )
                     overlayTouchListener?.onTouchExplorationStateChanged(true)
                 }
@@ -284,30 +287,18 @@
     }
 
     private fun addViewNowOrLater(view: View, animation: UdfpsAnimationViewController<*>?) {
-        if (udfpsViewPerformance()) {
-            addViewRunnable = kotlinx.coroutines.Runnable {
+        addViewRunnable =
+            kotlinx.coroutines.Runnable {
                 Trace.setCounter("UdfpsAddView", 1)
-                windowManager.addView(
-                        view,
-                        coreLayoutParams.updateDimensions(animation)
-                )
+                windowManager.addView(view, coreLayoutParams.updateDimensions(animation))
             }
-            if (powerInteractor.detailedWakefulness.value.isAwake()) {
-                // Device is awake, so we add the view immediately.
-                addViewIfPending()
-            } else {
-                listenForCurrentKeyguardState?.cancel()
-                listenForCurrentKeyguardState = scope.launch {
-                    currentStateUpdatedToOffAodOrDozing.collect {
-                        addViewIfPending()
-                    }
-                }
-            }
+        if (powerInteractor.detailedWakefulness.value.isAwake()) {
+            // Device is awake, so we add the view immediately.
+            addViewIfPending()
         } else {
-            windowManager.addView(
-                    view,
-                    coreLayoutParams.updateDimensions(animation)
-            )
+            listenForCurrentKeyguardState?.cancel()
+            listenForCurrentKeyguardState =
+                scope.launch { currentStateUpdatedToOffAodOrDozing.collect { addViewIfPending() } }
         }
     }
 
@@ -340,23 +331,26 @@
     ): UdfpsAnimationViewController<*>? {
         DeviceEntryUdfpsRefactor.assertInLegacyMode()
 
-        val isEnrollment = when (requestReason) {
-            REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
-            else -> false
-        }
+        val isEnrollment =
+            when (requestReason) {
+                REASON_ENROLL_FIND_SENSOR,
+                REASON_ENROLL_ENROLLING -> true
+                else -> false
+            }
 
-        val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) {
-            REASON_AUTH_OTHER
-        } else {
-            requestReason
-        }
+        val filteredRequestReason =
+            if (isEnrollment && shouldRemoveEnrollmentUi()) {
+                REASON_AUTH_OTHER
+            } else {
+                requestReason
+            }
 
         return when (filteredRequestReason) {
             REASON_ENROLL_FIND_SENSOR,
             REASON_ENROLL_ENROLLING -> {
                 // Enroll udfps UI is handled by settings, so use empty view here
                 UdfpsFpmEmptyViewController(
-                    view.addUdfpsView(R.layout.udfps_fpm_empty_view){
+                    view.addUdfpsView(R.layout.udfps_fpm_empty_view) {
                         updateAccessibilityViewLocation(sensorBounds)
                     },
                     statusBarStateController,
@@ -434,14 +428,10 @@
             udfpsDisplayModeProvider.disable(null)
         }
         getTouchOverlay()?.apply {
-            if (udfpsViewPerformance()) {
-                if (this.parent != null) {
-                    windowManager.removeView(this)
-                }
-                Trace.setCounter("UdfpsAddView", 0)
-            } else {
+            if (this.parent != null) {
                 windowManager.removeView(this)
             }
+            Trace.setCounter("UdfpsAddView", 0)
             setOnTouchListener(null)
             setOnHoverListener(null)
             overlayTouchListener?.let {
@@ -475,22 +465,19 @@
         val paddingX = animation?.paddingX ?: 0
         val paddingY = animation?.paddingY ?: 0
 
-        val isEnrollment = when (requestReason) {
-            REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
-            else -> false
-        }
+        val isEnrollment =
+            when (requestReason) {
+                REASON_ENROLL_FIND_SENSOR,
+                REASON_ENROLL_ENROLLING -> true
+                else -> false
+            }
 
         // Use expanded overlay unless touchExploration enabled
         var rotatedBounds =
             if (accessibilityManager.isTouchExplorationEnabled && isEnrollment) {
                 Rect(overlayParams.sensorBounds)
             } else {
-                Rect(
-                    0,
-                    0,
-                    overlayParams.naturalDisplayWidth,
-                    overlayParams.naturalDisplayHeight
-                )
+                Rect(0, 0, overlayParams.naturalDisplayWidth, overlayParams.naturalDisplayHeight)
             }
 
         val rot = overlayParams.rotation
@@ -498,7 +485,8 @@
             if (!shouldRotate(animation)) {
                 Log.v(
                     TAG,
-                    "Skip rotating UDFPS bounds " + Surface.rotationToString(rot) +
+                    "Skip rotating UDFPS bounds " +
+                        Surface.rotationToString(rot) +
                         " animation=$animation" +
                         " isGoingToSleep=${keyguardUpdateMonitor.isGoingToSleep}" +
                         " isOccluded=${keyguardStateController.isOccluded}"
@@ -559,6 +547,4 @@
 
 @RequestReason
 private fun Int.isImportantForAccessibility() =
-    this == REASON_ENROLL_FIND_SENSOR ||
-            this == REASON_ENROLL_ENROLLING ||
-            this == REASON_AUTH_BP
+    this == REASON_ENROLL_FIND_SENSOR || this == REASON_ENROLL_ENROLLING || this == REASON_AUTH_BP
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index 94f465d..eaddc42 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -288,11 +288,13 @@
     private fun startSettingsActivity(intent: Intent, view: View) {
         if (job?.isActive == true) {
             intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
-            activityStarter.postStartActivityDismissingKeyguard(
-                intent,
-                0,
-                dialogTransitionAnimator.createActivityTransitionController(view)
-            )
+            val controller = dialogTransitionAnimator.createActivityTransitionController(view)
+            // The controller will be null when the screen is locked and going to show the
+            // primary bouncer. In this case we dismiss the dialog manually.
+            if (controller == null) {
+                cancelJob()
+            }
+            activityStarter.postStartActivityDismissingKeyguard(intent, 0, controller)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 2043dd1..0466bbc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.communal.shared.model.CommunalBackgroundType
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -55,6 +56,8 @@
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 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.distinctUntilChanged
@@ -64,6 +67,7 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 /** The default view model used for showing the communal hub. */
@@ -74,6 +78,7 @@
 constructor(
     @Main val mainDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
+    @Background private val bgScope: CoroutineScope,
     @Main private val resources: Resources,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     keyguardInteractor: KeyguardInteractor,
@@ -303,8 +308,12 @@
      *
      * This is needed because the notification shade does not block touches in blank areas and these
      * fall through to the glanceable hub, which we don't want.
+     *
+     * Using a StateFlow as the value does not necessarily change when hub becomes available.
      */
-    val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded)
+    val touchesAllowed: StateFlow<Boolean> =
+        not(shadeInteractor.isAnyFullyExpanded)
+            .stateIn(bgScope, SharingStarted.Eagerly, initialValue = false)
 
     // TODO(b/339667383): remove this temporary swipe gesture handle
     /**
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 7b5139a..6cbe29e 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -160,7 +160,7 @@
     }
 
     fun onTileClick(): Boolean {
-        if (state == State.TIMEOUT_WAIT) {
+        if (state == State.TIMEOUT_WAIT || state == State.IDLE) {
             setState(State.IDLE)
             qsTile?.let {
                 it.click(expandable)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
index 04bde26..c00bd6f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
@@ -16,11 +16,23 @@
 
 package com.android.systemui.keyboard.shortcut.data.repository
 
+import android.view.KeyEvent
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+import android.view.WindowManager
+import android.view.WindowManager.KeyboardShortcutsReceiver
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsSource
 import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource
+import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
+import com.android.systemui.keyboard.shortcut.shared.model.shortcutCategory
 import javax.inject.Inject
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.suspendCancellableCoroutine
 
 @SysUISingleton
 class ShortcutHelperCategoriesRepository
@@ -28,11 +40,77 @@
 constructor(
     private val systemShortcutsSource: SystemShortcutsSource,
     private val multitaskingShortcutsSource: MultitaskingShortcutsSource,
+    private val windowManager: WindowManager,
+    shortcutHelperStateRepository: ShortcutHelperStateRepository
 ) {
 
-    fun systemShortcutsCategory(): ShortcutCategory =
-        systemShortcutsSource.systemShortcutsCategory()
+    val systemShortcutsCategory =
+        shortcutHelperStateRepository.state.map {
+            if (it is Active) systemShortcutsSource.systemShortcutsCategory() else null
+        }
 
-    fun multitaskingShortcutsCategory(): ShortcutCategory =
-        multitaskingShortcutsSource.multitaskingShortcutCategory()
+    val multitaskingShortcutsCategory =
+        shortcutHelperStateRepository.state.map {
+            if (it is Active) multitaskingShortcutsSource.multitaskingShortcutCategory() else null
+        }
+
+    val imeShortcutsCategory =
+        shortcutHelperStateRepository.state.map {
+            if (it is Active) retrieveImeShortcuts(it.deviceId) else null
+        }
+
+    private suspend fun retrieveImeShortcuts(deviceId: Int): ShortcutCategory {
+        return suspendCancellableCoroutine { continuation ->
+            val shortcutsReceiver = KeyboardShortcutsReceiver { shortcutGroups ->
+                continuation.resumeWith(Result.success(toShortcutCategory(shortcutGroups)))
+            }
+            windowManager.requestImeKeyboardShortcuts(shortcutsReceiver, deviceId)
+        }
+    }
+
+    private fun toShortcutCategory(shortcutGroups: List<KeyboardShortcutGroup>) =
+        shortcutCategory(ShortcutCategoryType.IME) {
+            shortcutGroups.map { shortcutGroup ->
+                subCategory(shortcutGroup.label.toString(), toShortcuts(shortcutGroup.items))
+            }
+        }
+
+    private fun toShortcuts(infoList: List<KeyboardShortcutInfo>) =
+        infoList.mapNotNull { toShortcut(it) }
+
+    private fun toShortcut(shortcutInfo: KeyboardShortcutInfo): Shortcut? {
+        val shortcutCommand = toShortcutCommand(shortcutInfo)
+        return if (shortcutCommand == null) null
+        else Shortcut(label = shortcutInfo.label!!.toString(), commands = listOf(shortcutCommand))
+    }
+
+    private fun toShortcutCommand(info: KeyboardShortcutInfo): ShortcutCommand? {
+        val keyCodes = mutableListOf<Int>()
+        var remainingModifiers = info.modifiers
+        SUPPORTED_MODIFIERS.forEach { supportedModifier ->
+            if ((supportedModifier and remainingModifiers) != 0) {
+                keyCodes += supportedModifier
+                // "Remove" the modifier from the remaining modifiers
+                remainingModifiers = remainingModifiers and supportedModifier.inv()
+            }
+        }
+        if (remainingModifiers != 0) {
+            // There is a remaining modifier we don't support
+            return null
+        }
+        keyCodes += info.keycode
+        return ShortcutCommand(keyCodes)
+    }
+
+    companion object {
+        private val SUPPORTED_MODIFIERS =
+            listOf(
+                KeyEvent.META_META_ON,
+                KeyEvent.META_CTRL_ON,
+                KeyEvent.META_ALT_ON,
+                KeyEvent.META_SHIFT_ON,
+                KeyEvent.META_SYM_ON,
+                KeyEvent.META_FUNCTION_ON
+            )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
index 883407c..57d4b4a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
@@ -18,30 +18,56 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCategoriesRepository
-import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
+import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 
 @SysUISingleton
 class ShortcutHelperCategoriesInteractor
 @Inject
 constructor(
-    stateRepository: ShortcutHelperStateRepository,
     categoriesRepository: ShortcutHelperCategoriesRepository,
 ) {
 
+    private val systemsShortcutCategory = categoriesRepository.systemShortcutsCategory
+    private val multitaskingShortcutsCategory = categoriesRepository.multitaskingShortcutsCategory
+    private val imeShortcutsCategory =
+        categoriesRepository.imeShortcutsCategory.map { groupSubCategoriesInCategory(it) }
+
     val shortcutCategories: Flow<List<ShortcutCategory>> =
-        stateRepository.state.map { state ->
-            when (state) {
-                is ShortcutHelperState.Active ->
-                    listOf(
-                        categoriesRepository.systemShortcutsCategory(),
-                        categoriesRepository.multitaskingShortcutsCategory()
-                    )
-                is ShortcutHelperState.Inactive -> emptyList()
-            }
+        combine(systemsShortcutCategory, multitaskingShortcutsCategory, imeShortcutsCategory) {
+            shortcutCategories ->
+            shortcutCategories.filterNotNull()
         }
+
+    private fun groupSubCategoriesInCategory(
+        shortcutCategory: ShortcutCategory?
+    ): ShortcutCategory? {
+        if (shortcutCategory == null) {
+            return null
+        }
+        val subCategoriesWithGroupedShortcuts =
+            shortcutCategory.subCategories.map {
+                ShortcutSubCategory(
+                    label = it.label,
+                    shortcuts = groupShortcutsInSubcategory(it.shortcuts)
+                )
+            }
+        return ShortcutCategory(
+            type = shortcutCategory.type,
+            subCategories = subCategoriesWithGroupedShortcuts
+        )
+    }
+
+    private fun groupShortcutsInSubcategory(shortcuts: List<Shortcut>) =
+        shortcuts
+            .groupBy { it.label }
+            .entries
+            .map { (commonLabel, groupedShortcuts) ->
+                Shortcut(label = commonLabel, commands = groupedShortcuts.flatMap { it.commands })
+            }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperStateInteractor.kt
index 3d707f7..299628e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperStateInteractor.kt
@@ -26,6 +26,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -38,7 +39,7 @@
     private val repository: ShortcutHelperStateRepository
 ) {
 
-    val state: Flow<ShortcutHelperState> = repository.state
+    val state: Flow<ShortcutHelperState> = repository.state.asStateFlow()
 
     fun onViewClosed() {
         repository.hide()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
index c5e8d2c..3ac7fa8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
@@ -19,6 +19,7 @@
 enum class ShortcutCategoryType {
     SYSTEM,
     MULTI_TASKING,
+    IME
 }
 
 data class ShortcutCategory(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
similarity index 93%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
index bb6215a..7a06d2f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.res.R
+import com.android.systemui.shade.PulsingGestureListener
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -51,10 +52,10 @@
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
-/** Business logic for use-cases related to the keyguard long-press feature. */
+/** Business logic for use-cases related to top-level touch handling in the lock screen. */
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
-class KeyguardLongPressInteractor
+class KeyguardTouchHandlingInteractor
 @Inject
 constructor(
     @Application private val appContext: Context,
@@ -65,6 +66,7 @@
     private val featureFlags: FeatureFlags,
     broadcastDispatcher: BroadcastDispatcher,
     private val accessibilityManager: AccessibilityManagerWrapper,
+    private val pulsingGestureListener: PulsingGestureListener,
 ) {
     /** Whether the long-press handling feature should be enabled. */
     val isLongPressHandlingEnabled: StateFlow<Boolean> =
@@ -166,6 +168,16 @@
         _shouldOpenSettings.value = false
     }
 
+    /** Notifies that the lockscreen has been clicked at position [x], [y]. */
+    fun onClick(x: Float, y: Float) {
+        pulsingGestureListener.onSingleTapUp(x, y)
+    }
+
+    /** Notifies that the lockscreen has been double clicked. */
+    fun onDoubleClick() {
+        pulsingGestureListener.onDoubleTapEvent()
+    }
+
     private fun showSettings() {
         _shouldOpenSettings.value = true
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
index 09fe067..057b4f9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
@@ -22,7 +22,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launch
 import com.android.systemui.common.ui.view.LongPressHandlingView
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 
@@ -39,7 +39,7 @@
     @JvmStatic
     fun bind(
         view: LongPressHandlingView,
-        viewModel: KeyguardLongPressViewModel,
+        viewModel: KeyguardTouchHandlingViewModel,
         onSingleTap: () -> Unit,
         falsingManager: FalsingManager,
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
index fa57565..4150ceb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
@@ -26,9 +26,9 @@
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.common.ui.binder.TextViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel
 import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils
 import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils.LAUNCH_SOURCE_KEYGUARD
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -44,7 +44,7 @@
     fun bind(
         view: View,
         viewModel: KeyguardSettingsMenuViewModel,
-        longPressViewModel: KeyguardLongPressViewModel,
+        touchHandlingViewModel: KeyguardTouchHandlingViewModel,
         rootViewModel: KeyguardRootViewModel?,
         vibratorHelper: VibratorHelper,
         activityStarter: ActivityStarter
@@ -97,7 +97,7 @@
                                 val hitRect = Rect()
                                 view.getHitRect(hitRect)
                                 if (!hitRect.contains(point.x, point.y)) {
-                                    longPressViewModel.onTouchedOutside()
+                                    touchHandlingViewModel.onTouchedOutside()
                                 }
                             }
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 777c873..4f0ac42 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -197,8 +197,7 @@
                 initiallySelectedSlotId =
                     bundle.getString(
                         KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
-                    )
-                        ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                    ) ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
                 shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
             )
         } else {
@@ -230,8 +229,7 @@
             val previewContext =
                 display?.let {
                     ContextThemeWrapper(context.createDisplayContext(it), context.getTheme())
-                }
-                    ?: context
+                } ?: context
 
             val rootView = FrameLayout(previewContext)
 
@@ -318,8 +316,8 @@
      */
     private fun setUpSmartspace(previewContext: Context, parentView: ViewGroup) {
         if (
-            !lockscreenSmartspaceController.isEnabled() ||
-                !lockscreenSmartspaceController.isDateWeatherDecoupled()
+            !lockscreenSmartspaceController.isEnabled ||
+                !lockscreenSmartspaceController.isDateWeatherDecoupled
         ) {
             return
         }
@@ -654,6 +652,7 @@
             clockController.clock = clock
         }
     }
+
     private fun onClockChanged() {
         if (MigrateClocksToBlueprint.isEnabled) {
             return
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
index 32e76d0..5cd5172 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
@@ -34,9 +34,9 @@
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
@@ -48,7 +48,7 @@
 constructor(
     @Main private val resources: Resources,
     private val keyguardSettingsMenuViewModel: KeyguardSettingsMenuViewModel,
-    private val keyguardLongPressViewModel: KeyguardLongPressViewModel,
+    private val keyguardTouchHandlingViewModel: KeyguardTouchHandlingViewModel,
     private val keyguardRootViewModel: KeyguardRootViewModel,
     private val vibratorHelper: VibratorHelper,
     private val activityStarter: ActivityStarter,
@@ -76,7 +76,7 @@
                 KeyguardSettingsViewBinder.bind(
                     constraintLayout.requireViewById<View>(R.id.keyguard_settings_button),
                     keyguardSettingsMenuViewModel,
-                    keyguardLongPressViewModel,
+                    keyguardTouchHandlingViewModel,
                     keyguardRootViewModel,
                     vibratorHelper,
                     activityStarter,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt
index a17c5e5..b33d552 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt
@@ -35,7 +35,7 @@
 ) : KeyguardSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (!MigrateClocksToBlueprint.isEnabled) return
-        if (smartspaceController.isEnabled()) return
+        if (smartspaceController.isEnabled) return
 
         constraintLayout.findViewById<View?>(R.id.keyguard_slice_view)?.let {
             (it.parent as ViewGroup).removeView(it)
@@ -47,7 +47,7 @@
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!MigrateClocksToBlueprint.isEnabled) return
-        if (smartspaceController.isEnabled()) return
+        if (smartspaceController.isEnabled) return
 
         constraintSet.apply {
             connect(
@@ -82,7 +82,7 @@
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
         if (!MigrateClocksToBlueprint.isEnabled) return
-        if (smartspaceController.isEnabled()) return
+        if (smartspaceController.isEnabled) return
 
         constraintLayout.removeView(R.id.keyguard_slice_view)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 3e6f8e6..6fe51ae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -44,7 +44,7 @@
     private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
     private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
     private val burnInHelperWrapper: BurnInHelperWrapper,
-    private val longPressViewModel: KeyguardLongPressViewModel,
+    private val keyguardTouchHandlingViewModel: KeyguardTouchHandlingViewModel,
     val settingsMenuViewModel: KeyguardSettingsMenuViewModel,
 ) {
     data class PreviewMode(
@@ -162,7 +162,7 @@
      * the lock screen settings menu item pop-up.
      */
     fun onTouchedOutsideLockScreenSettingsMenu() {
-        longPressViewModel.onTouchedOutside()
+        keyguardTouchHandlingViewModel.onTouchedOutside()
     }
 
     private fun button(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt
index 66ceded..36a342b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt
@@ -17,10 +17,10 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.res.R
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
-import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTouchHandlingInteractor
+import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 
@@ -28,7 +28,7 @@
 class KeyguardSettingsMenuViewModel
 @Inject
 constructor(
-    private val interactor: KeyguardLongPressInteractor,
+    private val interactor: KeyguardTouchHandlingInteractor,
 ) {
     val isVisible: Flow<Boolean> = interactor.isMenuVisible
     val shouldOpenSettings: Flow<Boolean> = interactor.shouldOpenSettings
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
index c0b1f95..e30ddc6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
@@ -40,12 +40,12 @@
     smartspaceInteractor: KeyguardSmartspaceInteractor,
 ) {
     /** Whether the smartspace section is available in the build. */
-    val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled()
+    val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled
     /** Whether the weather area is available in the build. */
     private val isWeatherEnabled: StateFlow<Boolean> = smartspaceInteractor.isWeatherEnabled
 
     /** Whether the data and weather areas are decoupled in the build. */
-    val isDateWeatherDecoupled: Boolean = smartspaceController.isDateWeatherDecoupled()
+    val isDateWeatherDecoupled: Boolean = smartspaceController.isDateWeatherDecoupled
 
     /** Whether the date area should be visible. */
     val isDateVisible: StateFlow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
index c73931a..f1cbf25 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
@@ -18,16 +18,16 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTouchHandlingInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 
-/** Models UI state to support the lock screen long-press feature. */
+/** Models UI state to support top-level touch handling in the lock screen. */
 @SysUISingleton
-class KeyguardLongPressViewModel
+class KeyguardTouchHandlingViewModel
 @Inject
 constructor(
-    private val interactor: KeyguardLongPressInteractor,
+    private val interactor: KeyguardTouchHandlingInteractor,
 ) {
 
     /** Whether the long-press handling feature should be enabled. */
@@ -45,4 +45,14 @@
     fun onTouchedOutside() {
         interactor.onTouchedOutside()
     }
+
+    /** Notifies that the lockscreen has been clicked at position [x], [y]. */
+    fun onClick(x: Float, y: Float) {
+        interactor.onClick(x, y)
+    }
+
+    /** Notifies that the lockscreen has been double clicked. */
+    fun onDoubleClick() {
+        interactor.onDoubleClick()
+    }
 }
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 b33eaa2..1de0abe 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
@@ -44,7 +44,7 @@
     clockInteractor: KeyguardClockInteractor,
     private val interactor: KeyguardBlueprintInteractor,
     private val authController: AuthController,
-    val longPress: KeyguardLongPressViewModel,
+    val touchHandling: KeyguardTouchHandlingViewModel,
     val shadeInteractor: ShadeInteractor,
     @Application private val applicationScope: CoroutineScope,
     unfoldTransitionInteractor: UnfoldTransitionInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
index 10cfd6b..630dcca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
@@ -53,7 +53,7 @@
     deviceEntryInteractor: DeviceEntryInteractor,
     communalInteractor: CommunalInteractor,
     shadeInteractor: ShadeInteractor,
-    val longPress: KeyguardLongPressViewModel,
+    val touchHandling: KeyguardTouchHandlingViewModel,
     val notifications: NotificationsPlaceholderViewModel,
 ) {
     val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index c7fde48..52b0b87 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -674,4 +674,11 @@
         return factory.create("DeviceEntryIconLog", 100);
     }
 
+    /** Provides a {@link LogBuffer} for use by the volume loggers. */
+    @Provides
+    @SysUISingleton
+    @VolumeLog
+    public static LogBuffer provideVolumeLogBuffer(LogBufferFactory factory) {
+        return factory.create("VolumeLog", 50);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/VolumeLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/VolumeLog.kt
new file mode 100644
index 0000000..bc3858a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/VolumeLog.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for volume. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class VolumeLog
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index 2e5ff9d..b393155 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -27,11 +27,11 @@
 import static com.android.systemui.accessibility.SystemActions.SYSTEM_ACTION_ID_ACCESSIBILITY_BUTTON_CHOOSER;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
 
 import android.content.ContentResolver;
 import android.content.Context;
@@ -74,10 +74,10 @@
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.rotation.RotationPolicyUtil;
+import com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 69aa450..89dce03 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -48,8 +48,8 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
-import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode;
 import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG_WINDOW_STATE;
 import static com.android.systemui.statusbar.phone.CentralSurfaces.dumpBarTransitions;
 import static com.android.systemui.util.Utils.isGesturalModeOnDefaultDisplay;
@@ -137,6 +137,7 @@
 import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.rotation.RotationButtonController;
 import com.android.systemui.shared.rotation.RotationPolicyUtil;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.shared.system.TaskStackChangeListener;
@@ -149,7 +150,6 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.AutoHideController;
-import com.android.systemui.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index a601d7f..c801662 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -19,7 +19,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.statusbar.RegisterStatusBarResult;
-import com.android.systemui.statusbar.phone.BarTransitions;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
 
 /** A controller to handle navigation bars. */
 public interface NavigationBarController {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerEmptyImpl.kt
index e73b078..06a78c5 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerEmptyImpl.kt
@@ -18,7 +18,7 @@
 
 import com.android.internal.statusbar.RegisterStatusBarResult
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.phone.BarTransitions
+import com.android.systemui.shared.statusbar.phone.BarTransitions
 import javax.inject.Inject
 
 /** A no-op version of [NavigationBarController] for variants like Arc and TV. */
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
index 1c2a087..f0207aa 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
@@ -56,11 +56,11 @@
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.AutoHideController;
-import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.settings.SecureSettings;
@@ -431,6 +431,8 @@
         NavigationBar navBar = mNavigationBars.get(displayId);
         if (navBar != null) {
             navBar.checkNavBarModes();
+        } else {
+            mTaskbarDelegate.checkNavBarModes();
         }
     }
 
@@ -439,6 +441,8 @@
         NavigationBar navBar = mNavigationBars.get(displayId);
         if (navBar != null) {
             navBar.finishBarAnimations();
+        } else {
+            mTaskbarDelegate.finishBarAnimations();
         }
     }
 
@@ -447,6 +451,8 @@
         NavigationBar navBar = mNavigationBars.get(displayId);
         if (navBar != null) {
             navBar.touchAutoDim();
+        } else {
+            mTaskbarDelegate.touchAutoDim();
         }
     }
 
@@ -455,6 +461,8 @@
         NavigationBar navBar = mNavigationBars.get(displayId);
         if (navBar != null) {
             navBar.transitionTo(barMode, animate);
+        } else {
+            mTaskbarDelegate.transitionTo(barMode, animate);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java
index 201e586..5442598 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java
@@ -28,7 +28,7 @@
 import com.android.systemui.navigationbar.buttons.ButtonDispatcher;
 import com.android.systemui.res.R;
 import com.android.systemui.settings.DisplayTracker;
-import com.android.systemui.statusbar.phone.BarTransitions;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.phone.LightBarTransitionsController;
 
 import java.io.PrintWriter;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index b360af0..d022c1c 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -24,6 +24,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
 import static com.android.systemui.navigationbar.NavBarHelper.transitionMode;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
@@ -34,7 +35,6 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
-import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
 
 import android.app.StatusBarManager;
 import android.app.StatusBarManager.WindowVisibleState;
@@ -63,13 +63,16 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.shared.recents.utilities.Utilities;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.AutoHideUiElement;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LightBarTransitionsController;
@@ -117,6 +120,11 @@
                         boolean longPressHomeEnabled) {
                     updateAssistantAvailability(available, longPressHomeEnabled);
                 }
+
+                @Override
+                public void updateWallpaperVisibility(boolean visible, int displayId) {
+                    updateWallpaperVisible(displayId, visible);
+                }
             };
     private int mDisabledFlags;
     private @WindowVisibleState int mTaskBarWindowState = WINDOW_STATE_SHOWING;
@@ -150,6 +158,7 @@
     private final AutoHideUiElement mAutoHideUiElement = new AutoHideUiElement() {
         @Override
         public void synchronizeState() {
+            checkNavBarModes();
         }
 
         @Override
@@ -165,11 +174,13 @@
 
     private BackAnimation mBackAnimation;
 
-    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private final StatusBarStateController mStatusBarStateController;
     @Inject
     public TaskbarDelegate(Context context,
             LightBarTransitionsController.Factory lightBarTransitionsControllerFactory,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            StatusBarStateController statusBarStateController) {
         mLightBarTransitionsControllerFactory = lightBarTransitionsControllerFactory;
 
         mContext = context;
@@ -179,6 +190,7 @@
         };
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mStatusBarKeyguardViewManager.setTaskbarDelegate(this);
+        mStatusBarStateController = statusBarStateController;
     }
 
     public void setDependencies(CommandQueue commandQueue,
@@ -324,6 +336,68 @@
         return (mSysUiState.getFlags() & View.STATUS_BAR_DISABLE_RECENT) == 0;
     }
 
+    void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
+        if (mOverviewProxyService.getProxy() == null) {
+            return;
+        }
+
+        try {
+            mOverviewProxyService.getProxy().onTransitionModeUpdated(barMode, checkBarModes);
+        } catch (RemoteException e) {
+            Log.e(TAG, "onTransitionModeUpdated() failed, barMode: " + barMode, e);
+        }
+    }
+
+    void checkNavBarModes() {
+        if (mOverviewProxyService.getProxy() == null) {
+            return;
+        }
+
+        try {
+            mOverviewProxyService.getProxy().checkNavBarModes();
+        } catch (RemoteException e) {
+            Log.e(TAG, "checkNavBarModes() failed", e);
+        }
+    }
+
+    void finishBarAnimations() {
+        if (mOverviewProxyService.getProxy() == null) {
+            return;
+        }
+
+        try {
+            mOverviewProxyService.getProxy().finishBarAnimations();
+        } catch (RemoteException e) {
+            Log.e(TAG, "finishBarAnimations() failed", e);
+        }
+    }
+
+    void touchAutoDim() {
+        if (mOverviewProxyService.getProxy() == null) {
+            return;
+        }
+
+        try {
+            int state = mStatusBarStateController.getState();
+            boolean shouldReset =
+                    state != StatusBarState.KEYGUARD && state != StatusBarState.SHADE_LOCKED;
+            mOverviewProxyService.getProxy().touchAutoDim(shouldReset);
+        } catch (RemoteException e) {
+            Log.e(TAG, "touchAutoDim() failed", e);
+        }
+    }
+
+    void transitionTo(@BarTransitions.TransitionMode int barMode, boolean animate) {
+        if (mOverviewProxyService.getProxy() == null) {
+            return;
+        }
+
+        try {
+            mOverviewProxyService.getProxy().transitionTo(barMode, animate);
+        } catch (RemoteException e) {
+            Log.e(TAG, "transitionTo() failed, barMode: " + barMode, e);
+        }
+    }
     private void updateAssistantAvailability(boolean assistantAvailable,
             boolean longPressHomeEnabled) {
         if (mOverviewProxyService.getProxy() == null) {
@@ -338,6 +412,18 @@
         }
     }
 
+    private void updateWallpaperVisible(int displayId, boolean visible) {
+        if (mOverviewProxyService.getProxy() == null) {
+            return;
+        }
+
+        try {
+            mOverviewProxyService.getProxy().updateWallpaperVisibility(displayId, visible);
+        } catch (RemoteException e) {
+            Log.e(TAG, "updateWallpaperVisibility() failed, visible: " + visible, e);
+        }
+    }
+
     @Override
     public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
             boolean showImeSwitcher) {
@@ -465,9 +551,11 @@
     private boolean updateTransitionMode(int barMode) {
         if (mTransitionMode != barMode) {
             mTransitionMode = barMode;
+            onTransitionModeUpdated(barMode, true);
             if (mAutoHideController != null) {
                 mAutoHideController.touchAutoHide();
             }
+
             return true;
         }
         return false;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
new file mode 100644
index 0000000..007ec3a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
@@ -0,0 +1,180 @@
+/*
+ * 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(ExperimentalFoundationApi::class)
+
+package com.android.systemui.qs.panels.ui.compose
+
+import android.content.ClipData
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.draganddrop.dragAndDropSource
+import androidx.compose.foundation.draganddrop.dragAndDropTarget
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draganddrop.DragAndDropEvent
+import androidx.compose.ui.draganddrop.DragAndDropTarget
+import androidx.compose.ui.draganddrop.DragAndDropTransferData
+import androidx.compose.ui.draganddrop.mimeTypes
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+@Composable
+fun rememberDragAndDropState(listState: EditTileListState): DragAndDropState {
+    val sourceSpec: MutableState<TileSpec?> = remember { mutableStateOf(null) }
+    return remember(listState) { DragAndDropState(sourceSpec, listState) }
+}
+
+/**
+ * Holds the [TileSpec] of the tile being moved and modify the [EditTileListState] based on drag and
+ * drop events.
+ */
+class DragAndDropState(
+    val sourceSpec: MutableState<TileSpec?>,
+    private val listState: EditTileListState
+) {
+    /** Returns index of the dragged tile if it's present in the list. Returns -1 if not. */
+    fun currentPosition(): Int {
+        return sourceSpec.value?.let { listState.indexOf(it) } ?: -1
+    }
+
+    fun isMoving(tileSpec: TileSpec): Boolean {
+        return sourceSpec.value?.let { it == tileSpec } ?: false
+    }
+
+    fun onStarted(spec: TileSpec) {
+        sourceSpec.value = spec
+    }
+
+    fun onMoved(targetSpec: TileSpec) {
+        sourceSpec.value?.let { listState.move(it, targetSpec) }
+    }
+
+    fun onDrop() {
+        sourceSpec.value = null
+    }
+}
+
+/**
+ * Registers a tile as a [DragAndDropTarget] to receive drag events and update the
+ * [DragAndDropState] with the tile's position, which can be used to insert a temporary placeholder.
+ *
+ * @param dragAndDropState The [DragAndDropState] using the tiles list
+ * @param tileSpec The [TileSpec] of the tile
+ * @param acceptDrops Whether the tile should accept a drop based on a given [TileSpec]
+ * @param onDrop Action to be executed when a [TileSpec] is dropped on the tile
+ */
+@Composable
+fun Modifier.dragAndDropTile(
+    dragAndDropState: DragAndDropState,
+    tileSpec: TileSpec,
+    acceptDrops: (TileSpec) -> Boolean,
+    onDrop: (TileSpec, Int) -> Unit,
+): Modifier {
+    val target =
+        remember(dragAndDropState) {
+            object : DragAndDropTarget {
+                override fun onDrop(event: DragAndDropEvent): Boolean {
+                    return dragAndDropState.sourceSpec.value?.let {
+                        onDrop(it, dragAndDropState.currentPosition())
+                        dragAndDropState.onDrop()
+                        true
+                    } ?: false
+                }
+
+                override fun onEntered(event: DragAndDropEvent) {
+                    dragAndDropState.onMoved(tileSpec)
+                }
+            }
+        }
+    return dragAndDropTarget(
+        shouldStartDragAndDrop = { event ->
+            event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE) &&
+                dragAndDropState.sourceSpec.value?.let { acceptDrops(it) } ?: false
+        },
+        target = target,
+    )
+}
+
+/**
+ * Registers a tile list as a [DragAndDropTarget] to receive drop events. Use this on list
+ * containers to catch drops outside of tiles.
+ *
+ * @param dragAndDropState The [DragAndDropState] using the tiles list
+ * @param acceptDrops Whether the tile should accept a drop based on a given [TileSpec]
+ * @param onDrop Action to be executed when a [TileSpec] is dropped on the tile
+ */
+@Composable
+fun Modifier.dragAndDropTileList(
+    dragAndDropState: DragAndDropState,
+    acceptDrops: (TileSpec) -> Boolean,
+    onDrop: (TileSpec, Int) -> Unit,
+): Modifier {
+    val target =
+        remember(dragAndDropState) {
+            object : DragAndDropTarget {
+                override fun onDrop(event: DragAndDropEvent): Boolean {
+                    return dragAndDropState.sourceSpec.value?.let {
+                        onDrop(it, dragAndDropState.currentPosition())
+                        dragAndDropState.onDrop()
+                        true
+                    } ?: false
+                }
+            }
+        }
+    return dragAndDropTarget(
+        target = target,
+        shouldStartDragAndDrop = { event ->
+            event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE) &&
+                dragAndDropState.sourceSpec.value?.let { acceptDrops(it) } ?: false
+        },
+    )
+}
+
+fun Modifier.dragAndDropTileSource(
+    tileSpec: TileSpec,
+    onTap: (TileSpec) -> Unit,
+    dragAndDropState: DragAndDropState
+): Modifier {
+    return dragAndDropSource {
+        detectTapGestures(
+            onTap = { onTap(tileSpec) },
+            onLongPress = {
+                dragAndDropState.onStarted(tileSpec)
+
+                // The tilespec from the ClipData transferred isn't actually needed as we're moving
+                // a tile within the same application. We're using a custom MIME type to limit the
+                // drag event to QS.
+                startTransfer(
+                    DragAndDropTransferData(
+                        ClipData(
+                            QsDragAndDrop.CLIPDATA_LABEL,
+                            arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE),
+                            ClipData.Item(tileSpec.spec)
+                        )
+                    )
+                )
+            }
+        )
+    }
+}
+
+private object QsDragAndDrop {
+    const val CLIPDATA_LABEL = "tilespec"
+    const val TILESPEC_MIME_TYPE = "qstile/tilespec"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
new file mode 100644
index 0000000..482c498
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.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.qs.panels.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.runtime.toMutableStateList
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+@Composable
+fun rememberEditListState(
+    tiles: List<EditTileViewModel>,
+): EditTileListState {
+    return remember(tiles) { EditTileListState(tiles) }
+}
+
+/** Holds the temporary state of the tile list during a drag movement where we move tiles around. */
+class EditTileListState(tiles: List<EditTileViewModel>) {
+    val tiles: SnapshotStateList<EditTileViewModel> = tiles.toMutableStateList()
+
+    fun move(tileSpec: TileSpec, target: TileSpec) {
+        val fromIndex = indexOf(tileSpec)
+        val toIndex = indexOf(target)
+
+        if (fromIndex == -1 || toIndex == -1 || fromIndex == toIndex) {
+            return
+        }
+
+        val isMovingToCurrent = tiles[toIndex].isCurrent
+        tiles.apply { add(toIndex, removeAt(fromIndex).copy(isCurrent = isMovingToCurrent)) }
+    }
+
+    fun indexOf(tileSpec: TileSpec): Int {
+        return tiles.indexOfFirst { it.tileSpec == tileSpec }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
index 7f5e474..092ad44 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
@@ -110,12 +110,15 @@
         tiles: List<EditTileViewModel>,
         modifier: Modifier,
         onAddTile: (TileSpec, Int) -> Unit,
-        onRemoveTile: (TileSpec) -> Unit
+        onRemoveTile: (TileSpec) -> Unit,
     ) {
         val columns by viewModel.columns.collectAsStateWithLifecycle()
         val showLabels by viewModel.showLabels.collectAsStateWithLifecycle()
 
-        val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
+        val listState = rememberEditListState(tiles)
+        val dragAndDropState = rememberDragAndDropState(listState)
+
+        val (currentTiles, otherTiles) = listState.tiles.partition { it.isCurrent }
         val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
             onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
         }
@@ -156,20 +159,24 @@
                 largeTileHeight = largeTileHeight,
                 iconTileHeight = iconTileHeight,
                 tilePadding = tilePadding,
-                onRemoveTile = onRemoveTile,
+                onAdd = onAddTile,
+                onRemove = onRemoveTile,
                 isIconOnly = viewModel::isIconTile,
                 columns = columns,
                 showLabels = showLabels,
+                dragAndDropState = dragAndDropState,
             )
             AvailableTiles(
-                tiles = otherTiles,
+                tiles = otherTiles.filter { !dragAndDropState.isMoving(it.tileSpec) },
                 largeTileHeight = largeTileHeight,
                 iconTileHeight = iconTileHeight,
                 tilePadding = tilePadding,
                 addTileToEnd = addTileToEnd,
+                onRemove = onRemoveTile,
                 isIconOnly = viewModel::isIconTile,
                 showLabels = showLabels,
                 columns = columns,
+                dragAndDropState = dragAndDropState,
             )
         }
     }
@@ -194,10 +201,12 @@
         largeTileHeight: Dp,
         iconTileHeight: Dp,
         tilePadding: Dp,
-        onRemoveTile: (TileSpec) -> Unit,
+        onAdd: (TileSpec, Int) -> Unit,
+        onRemove: (TileSpec) -> Unit,
         isIconOnly: (TileSpec) -> Boolean,
         showLabels: Boolean,
         columns: Int,
+        dragAndDropState: DragAndDropState,
     ) {
         val (smallTiles, largeTiles) = tiles.partition { isIconOnly(it.tileSpec) }
 
@@ -207,29 +216,40 @@
         CurrentTilesContainer {
             TileLazyGrid(
                 columns = GridCells.Fixed(columns),
-                modifier = Modifier.height(largeGridHeight),
+                modifier =
+                    Modifier.height(largeGridHeight)
+                        .dragAndDropTileList(dragAndDropState, { !isIconOnly(it) }, onAdd)
             ) {
                 editTiles(
-                    largeTiles,
-                    ClickAction.REMOVE,
-                    onRemoveTile,
-                    { false },
-                    indicatePosition = true
+                    tiles = largeTiles,
+                    clickAction = ClickAction.REMOVE,
+                    onClick = onRemove,
+                    isIconOnly = { false },
+                    dragAndDropState = dragAndDropState,
+                    acceptDrops = { !isIconOnly(it) },
+                    onDrop = onAdd,
+                    indicatePosition = true,
                 )
             }
         }
+
         CurrentTilesContainer {
             TileLazyGrid(
                 columns = GridCells.Fixed(columns),
-                modifier = Modifier.height(smallGridHeight),
+                modifier =
+                    Modifier.height(smallGridHeight)
+                        .dragAndDropTileList(dragAndDropState, { isIconOnly(it) }, onAdd)
             ) {
                 editTiles(
-                    smallTiles,
-                    ClickAction.REMOVE,
-                    onRemoveTile,
-                    { true },
+                    tiles = smallTiles,
+                    clickAction = ClickAction.REMOVE,
+                    onClick = onRemove,
+                    isIconOnly = { true },
                     showLabels = showLabels,
-                    indicatePosition = true
+                    dragAndDropState = dragAndDropState,
+                    acceptDrops = { isIconOnly(it) },
+                    onDrop = onAdd,
+                    indicatePosition = true,
                 )
             }
         }
@@ -242,9 +262,11 @@
         iconTileHeight: Dp,
         tilePadding: Dp,
         addTileToEnd: (TileSpec) -> Unit,
+        onRemove: (TileSpec) -> Unit,
         isIconOnly: (TileSpec) -> Boolean,
         showLabels: Boolean,
         columns: Int,
+        dragAndDropState: DragAndDropState,
     ) {
         val (tilesStock, tilesCustom) = tiles.partition { it.appName == null }
         val (smallTiles, largeTiles) = tilesStock.partition { isIconOnly(it.tileSpec) }
@@ -258,13 +280,27 @@
         val gridHeight =
             largeGridHeight + smallGridHeight + largeGridHeightCustom + (tilePadding * 2)
 
+        val onDrop: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, _ ->
+            onRemove(tileSpec)
+        }
+
         AvailableTilesContainer {
             TileLazyGrid(
                 columns = GridCells.Fixed(columns),
-                modifier = Modifier.height(gridHeight),
+                modifier =
+                    Modifier.height(gridHeight)
+                        .dragAndDropTileList(dragAndDropState, { true }, onDrop)
             ) {
                 // Large tiles
-                editTiles(largeTiles, ClickAction.ADD, addTileToEnd, isIconOnly)
+                editTiles(
+                    largeTiles,
+                    ClickAction.ADD,
+                    addTileToEnd,
+                    isIconOnly,
+                    dragAndDropState,
+                    acceptDrops = { true },
+                    onDrop = onDrop,
+                )
                 fillUpRow(nTiles = largeTiles.size, columns = columns / 2)
 
                 // Small tiles
@@ -273,7 +309,10 @@
                     ClickAction.ADD,
                     addTileToEnd,
                     isIconOnly,
-                    showLabels = showLabels
+                    showLabels = showLabels,
+                    dragAndDropState = dragAndDropState,
+                    acceptDrops = { true },
+                    onDrop = onDrop,
                 )
                 fillUpRow(nTiles = smallTiles.size, columns = columns)
 
@@ -283,7 +322,10 @@
                     ClickAction.ADD,
                     addTileToEnd,
                     isIconOnly,
-                    showLabels = showLabels
+                    showLabels = showLabels,
+                    dragAndDropState = dragAndDropState,
+                    acceptDrops = { true },
+                    onDrop = onDrop,
                 )
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index bbb98d3..0bb4cfa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -74,12 +74,12 @@
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.Expandable
+import com.android.compose.modifiers.thenIf
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.common.ui.compose.load
 import com.android.systemui.plugins.qs.QSTile
-import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
@@ -114,6 +114,7 @@
         showLabels = showLabels,
         label = state.label.toString(),
         iconOnly = iconOnly,
+        clickEnabled = true,
         onClick = tile::onClick,
         onLongClick = tile::onLongClick,
         modifier = modifier,
@@ -127,6 +128,7 @@
                 secondaryLabel = state.secondaryLabel.toString(),
                 icon = icon,
                 colors = colors,
+                clickEnabled = true,
                 onClick = tile::onSecondaryClick,
                 onLongClick = tile::onLongClick,
             )
@@ -140,7 +142,7 @@
     showLabels: Boolean,
     label: String,
     iconOnly: Boolean,
-    clickEnabled: Boolean = true,
+    clickEnabled: Boolean = false,
     onClick: (Expandable) -> Unit = {},
     onLongClick: (Expandable) -> Unit = {},
     modifier: Modifier = Modifier,
@@ -168,11 +170,12 @@
             Box(
                 modifier =
                     Modifier.fillMaxSize()
-                        .combinedClickable(
-                            enabled = clickEnabled,
-                            onClick = { onClick(it) },
-                            onLongClick = { onLongClick(it) }
-                        )
+                        .thenIf(clickEnabled) {
+                            Modifier.combinedClickable(
+                                onClick = { onClick(it) },
+                                onLongClick = { onLongClick(it) }
+                            )
+                        }
                         .tilePadding(),
             ) {
                 content()
@@ -197,7 +200,7 @@
     secondaryLabel: String?,
     icon: Icon,
     colors: TileColors,
-    clickEnabled: Boolean = true,
+    clickEnabled: Boolean = false,
     onClick: (Expandable) -> Unit = {},
     onLongClick: (Expandable) -> Unit = {},
 ) {
@@ -212,13 +215,12 @@
         ) {
             Box(
                 modifier =
-                    Modifier.fillMaxSize()
-                        .clip(TileDefaults.TileShape)
-                        .combinedClickable(
-                            enabled = clickEnabled,
+                    Modifier.fillMaxSize().clip(TileDefaults.TileShape).thenIf(clickEnabled) {
+                        Modifier.combinedClickable(
                             onClick = { onClick(it) },
                             onLongClick = { onLongClick(it) }
                         )
+                    }
             ) {
                 TileIcon(
                     icon = icon,
@@ -269,13 +271,29 @@
     onAddTile: (TileSpec, Int) -> Unit,
     onRemoveTile: (TileSpec) -> Unit,
 ) {
-    val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
-    val (otherTilesStock, otherTilesCustom) = otherTiles.partition { it.appName == null }
+    val currentListState = rememberEditListState(tiles)
+    val dragAndDropState = rememberDragAndDropState(currentListState)
+
+    val (currentTiles, otherTiles) = currentListState.tiles.partition { it.isCurrent }
+    val (otherTilesStock, otherTilesCustom) =
+        otherTiles
+            .filter { !dragAndDropState.isMoving(it.tileSpec) }
+            .partition { it.appName == null }
     val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
         onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
     }
 
-    TileLazyGrid(modifier = modifier, columns = columns) {
+    val onDropAdd: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, position ->
+        onAddTile(tileSpec, position)
+    }
+    val onDropRemove: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, _ ->
+        onRemoveTile(tileSpec)
+    }
+
+    TileLazyGrid(
+        modifier = modifier.dragAndDropTileList(dragAndDropState, { true }, onDropAdd),
+        columns = columns
+    ) {
         // These Text are just placeholders to see the different sections. Not final UI.
         item(span = { GridItemSpan(maxLineSpan) }) { Text("Current tiles", color = Color.White) }
 
@@ -285,6 +303,9 @@
             onRemoveTile,
             isIconOnly,
             indicatePosition = true,
+            dragAndDropState = dragAndDropState,
+            acceptDrops = { true },
+            onDrop = onDropAdd,
         )
 
         item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) }
@@ -294,6 +315,9 @@
             ClickAction.ADD,
             addTileToEnd,
             isIconOnly,
+            dragAndDropState = dragAndDropState,
+            acceptDrops = { true },
+            onDrop = onDropRemove,
         )
 
         item(span = { GridItemSpan(maxLineSpan) }) {
@@ -305,6 +329,9 @@
             ClickAction.ADD,
             addTileToEnd,
             isIconOnly,
+            dragAndDropState = dragAndDropState,
+            acceptDrops = { true },
+            onDrop = onDropRemove,
         )
     }
 }
@@ -314,6 +341,9 @@
     clickAction: ClickAction,
     onClick: (TileSpec) -> Unit,
     isIconOnly: (TileSpec) -> Boolean,
+    dragAndDropState: DragAndDropState,
+    acceptDrops: (TileSpec) -> Boolean,
+    onDrop: (TileSpec, Int) -> Unit,
     showLabels: Boolean = false,
     indicatePosition: Boolean = false,
 ) {
@@ -322,41 +352,44 @@
         key = { tiles[it].tileSpec.spec },
         span = { GridItemSpan(if (isIconOnly(tiles[it].tileSpec)) 1 else 2) },
         contentType = { TileType }
-    ) {
-        val viewModel = tiles[it]
-        val canClick =
-            when (clickAction) {
-                ClickAction.ADD -> AvailableEditActions.ADD in viewModel.availableEditActions
-                ClickAction.REMOVE -> AvailableEditActions.REMOVE in viewModel.availableEditActions
-            }
-        val onClickActionName =
-            when (clickAction) {
-                ClickAction.ADD ->
-                    stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
-                ClickAction.REMOVE ->
-                    stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
-            }
-        val stateDescription =
-            if (indicatePosition) {
-                stringResource(id = R.string.accessibility_qs_edit_position, it + 1)
-            } else {
-                ""
-            }
-
+    ) { index ->
+        val viewModel = tiles[index]
         val iconOnly = isIconOnly(viewModel.tileSpec)
         val tileHeight = tileHeight(iconOnly && showLabels)
-        EditTile(
-            tileViewModel = viewModel,
-            iconOnly = iconOnly,
-            showLabels = showLabels,
-            clickEnabled = canClick,
-            onClick = { onClick.invoke(viewModel.tileSpec) },
-            modifier =
-                Modifier.height(tileHeight).animateItem().semantics {
-                    onClick(onClickActionName) { false }
-                    this.stateDescription = stateDescription
+
+        if (!dragAndDropState.isMoving(viewModel.tileSpec)) {
+            val onClickActionName =
+                when (clickAction) {
+                    ClickAction.ADD ->
+                        stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
+                    ClickAction.REMOVE ->
+                        stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
                 }
-        )
+            val stateDescription =
+                if (indicatePosition) {
+                    stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+                } else {
+                    ""
+                }
+            EditTile(
+                tileViewModel = viewModel,
+                iconOnly = iconOnly,
+                showLabels = showLabels,
+                modifier =
+                    Modifier.height(tileHeight)
+                        .animateItem()
+                        .semantics {
+                            onClick(onClickActionName) { false }
+                            this.stateDescription = stateDescription
+                        }
+                        .dragAndDropTile(dragAndDropState, viewModel.tileSpec, acceptDrops, onDrop)
+                        .dragAndDropTileSource(
+                            viewModel.tileSpec,
+                            onClick,
+                            dragAndDropState,
+                        )
+            )
+        }
     }
 }
 
@@ -365,8 +398,6 @@
     tileViewModel: EditTileViewModel,
     iconOnly: Boolean,
     showLabels: Boolean,
-    clickEnabled: Boolean,
-    onClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
@@ -377,9 +408,6 @@
         showLabels = showLabels,
         label = label,
         iconOnly = iconOnly,
-        clickEnabled = clickEnabled,
-        onClick = { onClick() },
-        onLongClick = { onClick() },
         modifier = modifier,
     ) {
         if (iconOnly) {
@@ -394,9 +422,6 @@
                 secondaryLabel = tileViewModel.appName?.load(),
                 icon = tileViewModel.icon,
                 colors = colors,
-                clickEnabled = clickEnabled,
-                onClick = { onClick() },
-                onLongClick = { onClick() },
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
index ba9a044..a4c8638 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
@@ -26,7 +26,7 @@
  * [isCurrent] indicates whether this tile is part of the current set of tiles that the user sees in
  * Quick Settings.
  */
-class EditTileViewModel(
+data class EditTileViewModel(
     val tileSpec: TileSpec,
     val icon: Icon,
     val label: Text,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
index ab4480d..c4f6cd9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
@@ -23,12 +23,14 @@
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_NAME;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_TASK_ID;
+import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CLIP_DATA;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.PERMISSION_SELF;
 
 import android.app.Activity;
 import android.content.BroadcastReceiver;
+import android.content.ClipData;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -299,6 +301,14 @@
         data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
                 Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
         data.putParcelable(EXTRA_SCREENSHOT_URI, uri);
+
+        if (mBacklinksIncludeDataCheckBox.getVisibility() == View.VISIBLE
+                && mBacklinksIncludeDataCheckBox.isChecked()
+                && mViewModel.getBacklinksLiveData().getValue() != null) {
+            ClipData backlinksData = mViewModel.getBacklinksLiveData().getValue().getClipData();
+            data.putParcelable(EXTRA_CLIP_DATA, backlinksData);
+        }
+
         try {
             mResultReceiver.send(Activity.RESULT_OK, data);
             logUiEvent(SCREENSHOT_FOR_NOTE_ACCEPTED);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
index 3c4469d..0161f78 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
@@ -26,6 +26,7 @@
 
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
+import android.content.ClipData;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -82,6 +83,7 @@
     private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName();
     static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
     static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI";
+    static final String EXTRA_CLIP_DATA = TAG + "CLIP_DATA";
     static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE";
     static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER";
     static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME";
@@ -265,6 +267,11 @@
                 convertedData.setData(uri);
             }
 
+            if (resultData.containsKey(EXTRA_CLIP_DATA)) {
+                ClipData backlinksData = resultData.getParcelable(EXTRA_CLIP_DATA, ClipData.class);
+                convertedData.setClipData(backlinksData);
+            }
+
             // Broadcast no longer required, setting it to null.
             mKillAppClipsBroadcastIntent = null;
 
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflater.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflater.kt
index 468a873..cebc59b 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflater.kt
@@ -21,6 +21,7 @@
 import android.view.View
 import android.view.ViewGroup
 import androidx.core.view.isVisible
+import androidx.core.view.setPadding
 import com.android.systemui.res.R
 import com.android.systemui.settings.brightness.BrightnessSliderController
 
@@ -33,7 +34,15 @@
         val frame =
             (LayoutInflater.from(context).inflate(R.layout.brightness_mirror_container, null)
                     as ViewGroup)
-                .apply { isVisible = true }
+                .apply {
+                    isVisible = true
+                    // Match BrightnessMirrorController padding
+                    setPadding(
+                        context.resources.getDimensionPixelSize(
+                            R.dimen.rounded_slider_background_padding
+                        )
+                    )
+                }
         val sliderController = sliderControllerFactory.create(context, frame)
         sliderController.init()
         frame.addView(
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt
index 2651a994..79e8b87 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.settings.brightness.ui.viewModel
 
 import android.content.res.Resources
+import android.util.Log
 import android.view.View
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
@@ -67,25 +68,45 @@
     override fun setLocationAndSize(view: View) {
         view.getLocationInWindow(tempPosition)
         val padding = resources.getDimensionPixelSize(R.dimen.rounded_slider_background_padding)
-        _toggleSlider?.rootView?.setPadding(padding, padding, padding, padding)
         // Account for desired padding
         _locationAndSize.value =
             LocationAndSize(
-                yOffset = tempPosition[1] - padding,
+                yOffsetFromContainer = view.findTopFromContainer() - padding,
+                yOffsetFromWindow = tempPosition[1] - padding,
                 width = view.measuredWidth + 2 * padding,
                 height = view.measuredHeight + 2 * padding,
             )
     }
 
+    private fun View.findTopFromContainer(): Int {
+        var out = 0
+        var view = this
+        while (view.id != R.id.quick_settings_container) {
+            out += view.top
+            val parent = view.parent as? View
+            if (parent == null) {
+                Log.wtf(TAG, "Couldn't find container in parents of $this")
+                break
+            }
+            view = parent
+        }
+        return out
+    }
+
     // Callbacks are used for indicating reinflation when the config changes in some ways (like
     // density). However, we don't need that as we recompose the view anyway
     override fun addCallback(listener: MirrorController.BrightnessMirrorListener) {}
 
     override fun removeCallback(listener: MirrorController.BrightnessMirrorListener) {}
+
+    companion object {
+        private const val TAG = "BrightnessMirrorViewModel"
+    }
 }
 
 data class LocationAndSize(
-    val yOffset: Int = 0,
+    val yOffsetFromContainer: Int = 0,
+    val yOffsetFromWindow: Int = 0,
     val width: Int = 0,
     val height: Int = 0,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 8265cee..16d10ab 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -148,7 +148,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingLockscreenHostedTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
@@ -775,7 +775,7 @@
             @Main CoroutineDispatcher mainDispatcher,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
             DumpManager dumpManager,
-            KeyguardLongPressViewModel keyguardLongPressViewModel,
+            KeyguardTouchHandlingViewModel keyguardTouchHandlingViewModel,
             KeyguardInteractor keyguardInteractor,
             ActivityStarter activityStarter,
             SharedNotificationContainerInteractor sharedNotificationContainerInteractor,
@@ -970,7 +970,7 @@
         mKeyguardClockInteractor = keyguardClockInteractor;
         KeyguardLongPressViewBinder.bind(
                 mView.requireViewById(R.id.keyguard_long_press),
-                keyguardLongPressViewModel,
+                keyguardTouchHandlingViewModel,
                 () -> {
                     onEmptySpaceClick();
                     return Unit.INSTANCE;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 47fd494..1c223db 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -369,7 +369,9 @@
                 }
 
                 mFalsingCollector.onTouchEvent(ev);
-                mPulsingWakeupGestureHandler.onTouchEvent(ev);
+                if (!SceneContainerFlag.isEnabled()) {
+                    mPulsingWakeupGestureHandler.onTouchEvent(ev);
+                }
 
                 if (!SceneContainerFlag.isEnabled()
                         && mGlanceableHubContainerController.onTouchEvent(ev)) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index fe4832f0..062327d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -47,17 +47,19 @@
  * display state, wake-ups are handled by [com.android.systemui.doze.DozeSensors].
  */
 @SysUISingleton
-class PulsingGestureListener @Inject constructor(
-        private val falsingManager: FalsingManager,
-        private val dockManager: DockManager,
-        private val powerInteractor: PowerInteractor,
-        private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
-        private val statusBarStateController: StatusBarStateController,
-        private val shadeLogger: ShadeLogger,
-        private val dozeInteractor: DozeInteractor,
-        userTracker: UserTracker,
-        tunerService: TunerService,
-        dumpManager: DumpManager
+class PulsingGestureListener
+@Inject
+constructor(
+    private val falsingManager: FalsingManager,
+    private val dockManager: DockManager,
+    private val powerInteractor: PowerInteractor,
+    private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
+    private val statusBarStateController: StatusBarStateController,
+    private val shadeLogger: ShadeLogger,
+    private val dozeInteractor: DozeInteractor,
+    userTracker: UserTracker,
+    tunerService: TunerService,
+    dumpManager: DumpManager
 ) : GestureDetector.SimpleOnGestureListener(), Dumpable {
     private var doubleTapEnabled = false
     private var singleTapEnabled = false
@@ -66,21 +68,27 @@
         val tunable = Tunable { key: String?, _: String? ->
             when (key) {
                 Settings.Secure.DOZE_DOUBLE_TAP_GESTURE ->
-                    doubleTapEnabled = ambientDisplayConfiguration.doubleTapGestureEnabled(
-                            userTracker.userId)
+                    doubleTapEnabled =
+                        ambientDisplayConfiguration.doubleTapGestureEnabled(userTracker.userId)
                 Settings.Secure.DOZE_TAP_SCREEN_GESTURE ->
-                    singleTapEnabled = ambientDisplayConfiguration.tapGestureEnabled(
-                            userTracker.userId)
+                    singleTapEnabled =
+                        ambientDisplayConfiguration.tapGestureEnabled(userTracker.userId)
             }
         }
-        tunerService.addTunable(tunable,
-                Settings.Secure.DOZE_DOUBLE_TAP_GESTURE,
-                Settings.Secure.DOZE_TAP_SCREEN_GESTURE)
+        tunerService.addTunable(
+            tunable,
+            Settings.Secure.DOZE_DOUBLE_TAP_GESTURE,
+            Settings.Secure.DOZE_TAP_SCREEN_GESTURE
+        )
 
         dumpManager.registerDumpable(this)
     }
 
     override fun onSingleTapUp(e: MotionEvent): Boolean {
+        return onSingleTapUp(e.x, e.y)
+    }
+
+    fun onSingleTapUp(x: Float, y: Float): Boolean {
         val isNotDocked = !dockManager.isDocked
         shadeLogger.logSingleTapUp(statusBarStateController.isDozing, singleTapEnabled, isNotDocked)
         if (statusBarStateController.isDozing && singleTapEnabled && isNotDocked) {
@@ -89,11 +97,13 @@
             shadeLogger.logSingleTapUpFalsingState(proximityIsNotNear, isNotAFalseTap)
             if (proximityIsNotNear && isNotAFalseTap) {
                 shadeLogger.d("Single tap handled, requesting centralSurfaces.wakeUpIfDozing")
-                dozeInteractor.setLastTapToWakePosition(Point(e.x.toInt(), e.y.toInt()))
+                dozeInteractor.setLastTapToWakePosition(Point(x.toInt(), y.toInt()))
                 powerInteractor.wakeUpIfDozing("PULSING_SINGLE_TAP", PowerManager.WAKE_REASON_TAP)
             }
+
             return true
         }
+
         shadeLogger.d("onSingleTapUp event ignored")
         return false
     }
@@ -103,10 +113,18 @@
      * motion events for a double tap.
      */
     override fun onDoubleTapEvent(e: MotionEvent): Boolean {
+        if (e.actionMasked != MotionEvent.ACTION_UP) {
+            return false
+        }
+
+        return onDoubleTapEvent()
+    }
+
+    fun onDoubleTapEvent(): Boolean {
         // React to the [MotionEvent.ACTION_UP] event after double tap is detected. Falsing
         // checks MUST be on the ACTION_UP event.
-        if (e.actionMasked == MotionEvent.ACTION_UP &&
-                statusBarStateController.isDozing &&
+        if (
+            statusBarStateController.isDozing &&
                 (doubleTapEnabled || singleTapEnabled) &&
                 !falsingManager.isProximityNear &&
                 !falsingManager.isFalseDoubleTap
@@ -114,6 +132,7 @@
             powerInteractor.wakeUpIfDozing("PULSING_DOUBLE_TAP", PowerManager.WAKE_REASON_TAP)
             return true
         }
+
         return false
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index b0100b9..2b2aac64 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -37,7 +37,6 @@
 import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import java.util.concurrent.atomic.AtomicBoolean
@@ -60,7 +59,6 @@
     @Application private val applicationScope: CoroutineScope,
     val qsSceneAdapter: QSSceneAdapter,
     val shadeHeaderViewModel: ShadeHeaderViewModel,
-    val notifications: NotificationsPlaceholderViewModel,
     val brightnessMirrorViewModel: BrightnessMirrorViewModel,
     val mediaCarouselInteractor: MediaCarouselInteractor,
     shadeInteractor: ShadeInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarMode.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarMode.kt
index 933d0ab..b467032 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarMode.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarMode.kt
@@ -16,13 +16,13 @@
 
 package com.android.systemui.statusbar.data.model
 
-import com.android.systemui.statusbar.phone.BarTransitions
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT
-import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode
+import com.android.systemui.shared.statusbar.phone.BarTransitions
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSPARENT
+import com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode
 
 /**
  * The possible status bar modes.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index af8a89d..ef4dffad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -305,26 +305,20 @@
         dumpManager.registerDumpable(this)
     }
 
-    fun isEnabled(): Boolean {
-        execution.assertIsMainThread()
+    val isEnabled: Boolean = plugin != null
 
-        return plugin != null
-    }
+    val isDateWeatherDecoupled: Boolean = datePlugin != null && weatherPlugin != null
 
-    fun isDateWeatherDecoupled(): Boolean {
-        execution.assertIsMainThread()
-
-        return datePlugin != null && weatherPlugin != null
-    }
-
-    fun isWeatherEnabled(): Boolean {
-       execution.assertIsMainThread()
-       val showWeather = secureSettings.getIntForUser(
-           LOCK_SCREEN_WEATHER_ENABLED,
-           1,
-           userTracker.userId) == 1
-       return showWeather
-    }
+    val isWeatherEnabled: Boolean
+        get() {
+            val showWeather =
+                secureSettings.getIntForUser(
+                    LOCK_SCREEN_WEATHER_ENABLED,
+                    1,
+                    userTracker.userId,
+                ) == 1
+            return showWeather
+        }
 
     private fun updateBypassEnabled() {
         val bypassEnabled = bypassController.bypassEnabled
@@ -337,10 +331,10 @@
     fun buildAndConnectDateView(parent: ViewGroup): View? {
         execution.assertIsMainThread()
 
-        if (!isEnabled()) {
+        if (!isEnabled) {
             throw RuntimeException("Cannot build view when not enabled")
         }
-        if (!isDateWeatherDecoupled()) {
+        if (!isDateWeatherDecoupled) {
             throw RuntimeException("Cannot build date view when not decoupled")
         }
 
@@ -361,10 +355,10 @@
     fun buildAndConnectWeatherView(parent: ViewGroup): View? {
         execution.assertIsMainThread()
 
-        if (!isEnabled()) {
+        if (!isEnabled) {
             throw RuntimeException("Cannot build view when not enabled")
         }
-        if (!isDateWeatherDecoupled()) {
+        if (!isDateWeatherDecoupled) {
             throw RuntimeException("Cannot build weather view when not decoupled")
         }
 
@@ -385,7 +379,7 @@
     fun buildAndConnectView(parent: ViewGroup): View? {
         execution.assertIsMainThread()
 
-        if (!isEnabled()) {
+        if (!isEnabled) {
             throw RuntimeException("Cannot build view when not enabled")
         }
 
@@ -577,7 +571,7 @@
     }
 
     private fun filterSmartspaceTarget(t: SmartspaceTarget): Boolean {
-        if (isDateWeatherDecoupled() && t.featureType == SmartspaceTarget.FEATURE_WEATHER) {
+        if (isDateWeatherDecoupled && t.featureType == SmartspaceTarget.FEATURE_WEATHER) {
             return false
         }
         if (!showNotifications) {
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 f98f77e..aff57bd 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
@@ -40,6 +40,7 @@
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider
 import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix
 import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
@@ -726,6 +727,12 @@
      */
     private fun isAttemptingToShowHun(entry: ListEntry) =
         mHeadsUpManager.isHeadsUpEntry(entry.key) || isEntryBinding(entry)
+                || isHeadsUpAnimatingAway(entry)
+
+    private fun isHeadsUpAnimatingAway(entry: ListEntry): Boolean {
+        if (!GroupHunAnimationFix.isEnabled) return false
+        return entry.representativeEntry?.row?.isHeadsUpAnimatingAway ?: false
+    }
 
     /**
      * Whether the notification is already heads up/binding per [isAttemptingToShowHun] OR if it
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 367aaad..48c89f8 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
@@ -22,19 +22,25 @@
 import android.app.Notification.CATEGORY_EVENT
 import android.app.Notification.CATEGORY_REMINDER
 import android.app.Notification.VISIBILITY_PRIVATE
+import android.app.NotificationManager
 import android.app.NotificationManager.IMPORTANCE_DEFAULT
 import android.app.NotificationManager.IMPORTANCE_HIGH
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.database.ContentObserver
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.Handler
 import android.os.PowerManager
+import android.os.SystemProperties
 import android.provider.Settings
 import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED
 import android.provider.Settings.Global.HEADS_UP_OFF
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
@@ -47,6 +53,7 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
 import com.android.systemui.statusbar.policy.BatteryController
 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
@@ -244,12 +251,22 @@
         keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
 }
 
+/**
+ * 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"
+
+private const val PREF_HAS_SEEN_AVALANCHE_EDU = "has_seen_avalanche_edu"
+
 class AvalancheSuppressor(
     private val avalancheProvider: AvalancheProvider,
     private val systemClock: SystemClock,
     private val systemSettings: SystemSettings,
     private val packageManager: PackageManager,
     private val uiEventLogger: UiEventLogger,
+    private val context: Context,
+    private val notificationManager: NotificationManager
 ) :
     VisualInterruptionFilter(
         types = setOf(PEEK, PULSE),
@@ -257,6 +274,24 @@
     ) {
     val TAG = "AvalancheSuppressor"
 
+    private val prefs = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
+
+    // SharedPreferences are persisted across reboots
+    var hasSeenEdu: Boolean
+        get() = prefs.getBoolean(PREF_HAS_SEEN_AVALANCHE_EDU, false)
+        set(value) = prefs.edit().putBoolean(PREF_HAS_SEEN_AVALANCHE_EDU, value).apply()
+
+    // Reset on reboot.
+    // The pipeline runs these suppressors many times very fast, so we must use a separate bool
+    // to force show for debug so that phone does not get stuck sending out infinite number of
+    // education HUNs.
+    private var hasShownOnceForDebug = false
+
+    private fun shouldShowEdu() : Boolean {
+        val forceShowOnce = SystemProperties.get(FORCE_SHOW_AVALANCHE_EDU_ONCE, "").equals("1")
+        return !hasSeenEdu || (forceShowOnce && !hasShownOnceForDebug)
+    }
+
     enum class State {
         ALLOW_CONVERSATION_AFTER_AVALANCHE,
         ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME,
@@ -309,9 +344,46 @@
         if (state != State.SUPPRESS) {
             return false
         }
+        if (shouldShowEdu()) {
+            showEdu()
+        }
         return true
     }
 
+    /**
+     * 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 intent = Intent(Settings.ACTION_MANAGE_ADAPTIVE_NOTIFICATIONS)
+        val pendingIntent = PendingIntent.getActivity(
+            context, 0, intent,
+            PendingIntent.FLAG_IMMUTABLE
+        )
+
+        val builder =
+            Notification.Builder(context, NotificationChannels.ALERTS)
+                .setTicker(titleStr)
+                .setContentTitle(titleStr)
+                .setContentText(textStr)
+                .setSmallIcon(com.android.systemui.res.R.drawable.ic_settings)
+                .setCategory(Notification.CATEGORY_SYSTEM)
+                .setAutoCancel(true)
+                .addAction(android.R.drawable.button_onoff_indicator_off, actionStr, pendingIntent)
+                .setContentIntent(pendingIntent)
+
+        notificationManager.notify(SystemMessage.NOTE_ADAPTIVE_NOTIFICATIONS, builder.build())
+        hasSeenEdu = true
+        hasShownOnceForDebug = true;
+    }
+
     private fun calculateState(entry: NotificationEntry): State {
         if (
             entry.ranking.isConversation &&
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 84f8662..96f94ca 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
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.interruption
 
+import android.app.NotificationManager
+import android.content.Context
 import android.content.pm.PackageManager
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.Handler
@@ -68,7 +70,9 @@
     private val avalancheProvider: AvalancheProvider,
     private val systemSettings: SystemSettings,
     private val packageManager: PackageManager,
-    private val bubbles: Optional<Bubbles>
+    private val bubbles: Optional<Bubbles>,
+    private val context: Context,
+    private val notificationManager: NotificationManager
 ) : VisualInterruptionDecisionProvider {
 
     init {
@@ -179,7 +183,7 @@
         if (NotificationAvalancheSuppression.isEnabled) {
             addFilter(
                 AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                        uiEventLogger)
+                        uiEventLogger, context, notificationManager)
             )
             avalancheProvider.register()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 96b1cf2..646d0b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1477,7 +1477,7 @@
             }
             if (hasRemoteInput) {
                 result.mView.setWrapper(wrapper);
-                result.mView.addOnVisibilityChangedListener(this::setRemoteInputVisible);
+                result.mView.setOnVisibilityChangedListener(this::setRemoteInputVisible);
 
                 if (existingPendingIntent != null || result.mView.isActive()) {
                     // The current action could be gone, or the pending intent no longer valid.
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
new file mode 100644
index 0000000..5867612
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/GroupHunAnimationFix.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.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for com.android.systemui.Flags.FLAG_NOTIFICATION_GROUP_HUN_REMOVAL_ANIMATION_FIX */
+@Suppress("NOTHING_TO_INLINE")
+object GroupHunAnimationFix {
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_GROUP_HUN_REMOVAL_ANIMATION_FIX
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Are sections sorted by time? */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationGroupHunRemovalAnimationFix()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index ddfa86d..715c6e6 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
@@ -1500,6 +1500,7 @@
      * needed.
      */
     void setOnStackYChanged(Consumer<Boolean> onStackYChanged) {
+        SceneContainerFlag.assertInLegacyMode();
         mOnStackYChanged = onStackYChanged;
     }
 
@@ -2270,6 +2271,7 @@
 
     public void setOverscrollTopChangedListener(
             OnOverscrollTopChangedListener overscrollTopChangedListener) {
+        SceneContainerFlag.assertInLegacyMode();
         mOverscrollTopChangedListener = overscrollTopChangedListener;
     }
 
@@ -5705,6 +5707,7 @@
      * Set a listener to when scrolling changes.
      */
     public void setOnScrollListener(Consumer<Integer> listener) {
+        SceneContainerFlag.assertInLegacyMode();
         mScrollListener = listener;
     }
 
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 bf53ee2..12f8f69 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
@@ -1052,6 +1052,7 @@
 
     public void setOverscrollTopChangedListener(
             OnOverscrollTopChangedListener listener) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setOverscrollTopChangedListener(listener);
     }
 
@@ -1248,6 +1249,7 @@
     }
 
     public void setOnStackYChanged(Consumer<Boolean> onStackYChanged) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setOnStackYChanged(onStackYChanged);
     }
 
@@ -1750,6 +1752,7 @@
      * Set a listener to when scrolling changes.
      */
     public void setOnScrollListener(Consumer<Integer> listener) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setOnScrollListener(listener);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index ebb0d7d..57e52b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -70,10 +70,10 @@
             ) { shadeExpansion, shadeMode, qsExpansion, transitionState, quickSettingsScene ->
                 when (transitionState) {
                     is ObservableTransitionState.Idle -> {
-                        if (transitionState.currentScene == Scenes.Lockscreen) {
-                            1f
-                        } else {
-                            shadeExpansion
+                        when (transitionState.currentScene) {
+                            Scenes.Lockscreen,
+                            Scenes.QuickSettings -> 1f
+                            else -> shadeExpansion
                         }
                     }
                     is ObservableTransitionState.Transition -> {
@@ -162,9 +162,13 @@
         stackAppearanceInteractor::setCurrentGestureOverscroll
 
     /** Whether the notification stack is scrollable or not. */
-    val isScrollable: Flow<Boolean> = sceneInteractor.currentScene.map {
-        sceneInteractor.isSceneInFamily(it, SceneFamilies.NotifShade) || it == Scenes.Lockscreen
-    }.dumpWhileCollecting("isScrollable")
+    val isScrollable: Flow<Boolean> =
+        sceneInteractor.currentScene
+            .map {
+                sceneInteractor.isSceneInFamily(it, SceneFamilies.NotifShade) ||
+                    it == Scenes.Lockscreen
+            }
+            .dumpWhileCollecting("isScrollable")
 
     /** Whether the notification stack is displayed in doze mode. */
     val isDozing: Flow<Boolean> by lazy {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 634bd7e..08e81d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -20,15 +20,16 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 
 /**
  * ViewModel used by the Notification placeholders inside the scene container to update the
@@ -41,8 +42,8 @@
     dumpManager: DumpManager,
     private val interactor: NotificationStackAppearanceInteractor,
     shadeInteractor: ShadeInteractor,
+    private val shadeSceneViewModel: ShadeSceneViewModel,
     featureFlags: FeatureFlagsClassic,
-    private val keyguardInteractor: KeyguardInteractor,
 ) : FlowDumperImpl(dumpManager) {
     /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */
     val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES)
@@ -60,11 +61,19 @@
         interactor.setConstrainedAvailableSpace(height)
     }
 
+    /** Notifies that empty space on the notification scrim has been clicked. */
+    fun onEmptySpaceClicked() {
+        shadeSceneViewModel.onContentClicked()
+    }
+
     /** Sets the content alpha for the current state of the brightness mirror */
     fun setAlphaForBrightnessMirror(alpha: Float) {
         interactor.setAlphaForBrightnessMirror(alpha)
     }
 
+    /** Whether or not the notification scrim should be clickable. */
+    val isClickable: StateFlow<Boolean> = shadeSceneViewModel.isClickable
+
     /** Corner rounding of the stack */
     val shadeScrimRounding: Flow<ShadeScrimRounding> =
         interactor.shadeScrimRounding.dumpWhileCollecting("shadeScrimRounding")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index d75a738..0a02381 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -44,6 +44,7 @@
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.qs.QSPanelController;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.util.Compile;
 
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 5c262f3..28117e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -181,6 +181,7 @@
 import com.android.systemui.shade.ShadeSurface;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.shared.recents.utilities.Utilities;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.AutoHideUiElement;
 import com.android.systemui.statusbar.CircleReveal;
 import com.android.systemui.statusbar.CommandQueue;
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 c5dcb09..4ce9010 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -31,6 +31,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
@@ -105,6 +106,8 @@
     private boolean mIsExpanded;
     private int mStatusBarState;
     private AnimationStateHandler mAnimationStateHandler;
+
+    private Handler mBgHandler;
     private int mHeadsUpInset;
 
     // Used for determining the region for touch interaction
@@ -149,7 +152,8 @@
             UiEventLogger uiEventLogger,
             JavaAdapter javaAdapter,
             ShadeInteractor shadeInteractor,
-            AvalancheController avalancheController) {
+            AvalancheController avalancheController,
+            @Background Handler bgHandler) {
         super(context, logger, handler, globalSettings, systemClock, executor,
                 accessibilityManagerWrapper, uiEventLogger, avalancheController);
         Resources resources = mContext.getResources();
@@ -159,7 +163,7 @@
         mGroupMembershipManager = groupMembershipManager;
         mVisualStabilityProvider = visualStabilityProvider;
         mAvalancheController = avalancheController;
-
+        mBgHandler = bgHandler;
         updateResources();
         configurationController.addCallback(new ConfigurationController.ConfigurationListener() {
             @Override
@@ -401,7 +405,11 @@
             // Waiting HUNs in AvalancheController are still promoted to the HUN section and thus
             // seen in open shade; clear them so we don't show them again when the shade closes and
             // reordering is allowed again.
-            mAvalancheController.logDroppedHuns(mAvalancheController.getWaitingKeys().size());
+            int waitingKeysSize = mAvalancheController.getWaitingKeys().size();
+            mBgHandler.post(() -> {
+                // Do this in the background to avoid missing frames when closing the shade
+                mAvalancheController.logDroppedHuns(waitingKeysSize);
+            });
             mAvalancheController.clearNext();
 
             // In open shade the first HUN is pinned, and visual stability logic prevents us from
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt
index 8a45ec1..4aece3d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt
@@ -42,7 +42,7 @@
     }
 
     override fun onViewAttached() {
-        if (!smartspaceRelocateToBottom() || !smartspaceController.isEnabled()) {
+        if (!smartspaceRelocateToBottom() || !smartspaceController.isEnabled) {
             return
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index eec617b..a33996b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -19,8 +19,8 @@
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
 
 import android.content.Context;
 import android.graphics.Rect;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
index ae3f923..6676a7f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
@@ -23,6 +23,7 @@
 import android.view.View;
 
 import com.android.systemui.res.R;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
 
 public final class PhoneStatusBarTransitions extends BarTransitions {
     private static final float ICON_ALPHA_WHEN_NOT_OPAQUE = 1;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java
index 29c1372..25b8bfe0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarDemoMode.java
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_WARNING;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSLUCENT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_WARNING;
 import static com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentModule.OPERATOR_NAME_VIEW;
 
 import android.annotation.NonNull;
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 fad5df8..220e729 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -834,6 +834,7 @@
          * @return true if the notification is sticky
          */
         public boolean isSticky() {
+            if (mEntry == null) return false;
             return (mEntry.isRowPinned() && mExpanded)
                     || mRemoteInputActive
                     || hasFullScreenIntent(mEntry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 1fc7bf4..31776cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -115,7 +115,7 @@
     private final SendButtonTextWatcher mTextWatcher;
     private final TextView.OnEditorActionListener mEditorActionHandler;
     private final ArrayList<Runnable> mOnSendListeners = new ArrayList<>();
-    private final ArrayList<Consumer<Boolean>> mOnVisibilityChangedListeners = new ArrayList<>();
+    private Consumer<Boolean> mOnVisibilityChangedListener = null;
     private final ArrayList<OnFocusChangeListener> mEditTextFocusChangeListeners =
             new ArrayList<>();
 
@@ -733,24 +733,17 @@
      * {@link #getVisibility()} would return {@link View#VISIBLE}, and {@code false} it would return
      * any other value.
      */
-    public void addOnVisibilityChangedListener(Consumer<Boolean> listener) {
-        mOnVisibilityChangedListeners.add(listener);
-    }
-
-    /**
-     * Unregister a listener previously registered via
-     * {@link #addOnVisibilityChangedListener(Consumer)}.
-     */
-    public void removeOnVisibilityChangedListener(Consumer<Boolean> listener) {
-        mOnVisibilityChangedListeners.remove(listener);
+    public void setOnVisibilityChangedListener(Consumer<Boolean> listener) {
+        mOnVisibilityChangedListener = listener;
     }
 
     @Override
     protected void onVisibilityChanged(View changedView, int visibility) {
         super.onVisibilityChanged(changedView, visibility);
         if (changedView == this) {
-            for (Consumer<Boolean> listener : new ArrayList<>(mOnVisibilityChangedListeners)) {
-                listener.accept(visibility == VISIBLE);
+            final Consumer<Boolean> visibilityChangedListener = mOnVisibilityChangedListener;
+            if (visibilityChangedListener != null) {
+                visibilityChangedListener.accept(visibility == VISIBLE);
             }
             // Hide soft-keyboard when the input view became invisible
             // (i.e. The notification shade collapsed by pressing the home key)
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 1ae5614..2e7b05a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.volume.shared.VolumeLogger
 import dagger.Module
 import dagger.Provides
 import kotlin.coroutines.CoroutineContext
@@ -58,6 +59,7 @@
             contentResolver: ContentResolver,
             @Background coroutineContext: CoroutineContext,
             @Application coroutineScope: CoroutineScope,
+            volumeLogger: VolumeLogger,
         ): AudioRepository =
             AudioRepositoryImpl(
                 intentsReceiver,
@@ -65,6 +67,7 @@
                 contentResolver,
                 coroutineContext,
                 coroutineScope,
+                volumeLogger,
             )
 
         @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index c18573e..521f608 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -26,6 +26,7 @@
 import com.android.settingslib.volume.shared.model.RingerMode
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.res.R
+import com.android.systemui.volume.panel.shared.VolumePanelLogger
 import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -51,6 +52,7 @@
     private val context: Context,
     private val audioVolumeInteractor: AudioVolumeInteractor,
     private val uiEventLogger: UiEventLogger,
+    private val volumePanelLogger: VolumePanelLogger,
 ) : SliderViewModel {
 
     private val volumeChanges = MutableStateFlow<Int?>(null)
@@ -105,6 +107,7 @@
                 audioVolumeInteractor.canChangeVolume(audioStream),
                 audioVolumeInteractor.ringerMode,
             ) { model, isEnabled, ringerMode ->
+                volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
                 model.toState(isEnabled, ringerMode)
             }
             .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
@@ -112,7 +115,10 @@
     init {
         volumeChanges
             .filterNotNull()
-            .onEach { audioVolumeInteractor.setVolume(audioStream, it) }
+            .onEach {
+                volumePanelLogger.onSetVolumeRequested(audioStream, it)
+                audioVolumeInteractor.setVolume(audioStream, it)
+            }
             .launchIn(coroutineScope)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
new file mode 100644
index 0000000..cc513b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.shared
+
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.VolumeLog
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import javax.inject.Inject
+
+private const val TAG = "SysUI_VolumePanel"
+
+/** Logs events related to the Volume Panel. */
+@VolumePanelScope
+class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) {
+
+    fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = audioStream.toString()
+                int1 = volume
+            },
+            { "Set volume: stream=$str1 volume=$int1" }
+        )
+    }
+
+    fun onVolumeUpdateReceived(audioStream: AudioStream, volume: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = audioStream.toString()
+                int1 = volume
+            },
+            { "Volume update received: stream=$str1 volume=$int1" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt
new file mode 100644
index 0000000..869a82a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.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.volume.shared
+
+import com.android.settingslib.volume.data.repository.AudioRepositoryImpl
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.AudioStreamModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.VolumeLog
+import javax.inject.Inject
+
+private const val TAG = "SysUI_Volume"
+
+/** Logs general System UI volume events. */
+@SysUISingleton
+class VolumeLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) :
+    AudioRepositoryImpl.Logger {
+
+    override fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = audioStream.toString()
+                int1 = volume
+            },
+            { "Set volume: stream=$str1 volume=$int1" }
+        )
+    }
+
+    override fun onVolumeUpdateReceived(audioStream: AudioStream, model: AudioStreamModel) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = audioStream.toString()
+                int1 = model.volume
+            },
+            { "Volume update received: stream=$str1 volume=$int1" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index ec9b5cf..3f1ec85 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -20,8 +20,9 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
@@ -66,6 +67,7 @@
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
 import com.android.wm.shell.onehanded.OneHandedUiEventLogger;
 import com.android.wm.shell.pip.Pip;
+import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.recents.RecentTasks;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.sysui.ShellInterface;
@@ -249,7 +251,25 @@
                 pip.showPictureInPictureMenu();
             }
         });
+        pip.registerPipTransitionCallback(
+                new PipTransitionController.PipTransitionCallback() {
+                    @Override
+                    public void onPipTransitionStarted(int direction, Rect pipBounds) {
+                        mSysUiState.setFlag(SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, true)
+                                .commitUpdate(mDisplayTracker.getDefaultDisplayId());
+                    }
 
+                    @Override
+                    public void onPipTransitionFinished(int direction) {
+                        mSysUiState.setFlag(SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, false)
+                                .commitUpdate(mDisplayTracker.getDefaultDisplayId());
+                    }
+
+                    @Override
+                    public void onPipTransitionCanceled(int direction) {
+                        // No op.
+                    }
+                }, mSysUiMainExecutor);
         mSysUiState.addCallback(sysUiStateFlag -> {
             mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0;
             pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag);
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 0ed40e9..97f5efc 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
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.biometrics.ui.kosmos.promptViewmodel
+package com.android.systemui.biometrics.ui.viewmodel
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.ComponentName
@@ -66,12 +66,6 @@
 import com.android.systemui.biometrics.shared.model.toSensorStrength
 import com.android.systemui.biometrics.shared.model.toSensorType
 import com.android.systemui.biometrics.udfpsUtils
-import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode
-import com.android.systemui.biometrics.ui.viewmodel.PromptMessage
-import com.android.systemui.biometrics.ui.viewmodel.PromptPosition
-import com.android.systemui.biometrics.ui.viewmodel.PromptSize
-import com.android.systemui.biometrics.ui.viewmodel.iconProvider
-import com.android.systemui.biometrics.ui.viewmodel.promptViewModel
 import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
new file mode 100644
index 0000000..6985439
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.data.repository
+
+import android.view.KeyEvent
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+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.keyboard.shortcut.shared.model.Shortcut
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
+import com.android.systemui.keyboard.shortcut.shared.model.shortcutCategory
+import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesRepository
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.kosmos.testDispatcher
+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.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() {
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val kosmos = testKosmos().also { it.testDispatcher = UnconfinedTestDispatcher() }
+    private val repo = kosmos.shortcutHelperCategoriesRepository
+    private val helper = kosmos.shortcutHelperTestHelper
+    private val testScope = kosmos.testScope
+
+    @Test
+    fun stateActive_imeShortcuts_shortcutInfoCorrectlyConverted() =
+        testScope.runTest {
+            helper.setImeShortcuts(imeShortcutsGroupWithPreviousLanguageSwitchShortcut)
+            val imeShortcutCategory by collectLastValue(repo.imeShortcutsCategory)
+
+            helper.showFromActivity()
+
+            assertThat(imeShortcutCategory)
+                .isEqualTo(expectedImeShortcutCategoryWithPreviousLanguageSwitchShortcut)
+        }
+
+    @Test
+    fun stateActive_imeShortcuts_discardUnsupportedShortcutInfoModifiers() =
+        testScope.runTest {
+            helper.setImeShortcuts(imeShortcutsGroupWithUnsupportedShortcutModifiers)
+            val imeShortcutCategory by collectLastValue(repo.imeShortcutsCategory)
+
+            helper.showFromActivity()
+
+            assertThat(imeShortcutCategory)
+                .isEqualTo(expectedImeShortcutCategoryWithDiscardedUnsupportedShortcuts)
+        }
+
+    private val switchToPreviousLanguageCommand =
+        ShortcutCommand(
+            listOf(KeyEvent.META_CTRL_ON, KeyEvent.META_SHIFT_ON, KeyEvent.KEYCODE_SPACE)
+        )
+
+    private val expectedImeShortcutCategoryWithDiscardedUnsupportedShortcuts =
+        shortcutCategory(ShortcutCategoryType.IME) { subCategory("input", emptyList()) }
+
+    private val switchToPreviousLanguageKeyboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ "switch to previous language",
+            /* keycode = */ switchToPreviousLanguageCommand.keyCodes[2],
+            /* modifiers = */ switchToPreviousLanguageCommand.keyCodes[0] or
+                switchToPreviousLanguageCommand.keyCodes[1],
+        )
+
+    private val expectedImeShortcutCategoryWithPreviousLanguageSwitchShortcut =
+        shortcutCategory(ShortcutCategoryType.IME) {
+            subCategory(
+                "input",
+                listOf(
+                    Shortcut(
+                        switchToPreviousLanguageKeyboardShortcutInfo.label!!.toString(),
+                        listOf(switchToPreviousLanguageCommand)
+                    )
+                )
+            )
+        }
+
+    private val imeShortcutsGroupWithPreviousLanguageSwitchShortcut =
+        listOf(
+            KeyboardShortcutGroup(
+                "input",
+                listOf(
+                    switchToPreviousLanguageKeyboardShortcutInfo,
+                )
+            )
+        )
+
+    private val shortcutInfoWithUnsupportedModifier =
+        KeyboardShortcutInfo(
+            /* label = */ "unsupported shortcut",
+            /* keycode = */ KeyEvent.KEYCODE_SPACE,
+            /* modifiers = */ 32
+        )
+
+    private val imeShortcutsGroupWithUnsupportedShortcutModifiers =
+        listOf(
+            KeyboardShortcutGroup(
+                "input",
+                listOf(
+                    shortcutInfoWithUnsupportedModifier,
+                )
+            )
+        )
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
index 9c9e48e..5c7ce3e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
@@ -16,10 +16,17 @@
 
 package com.android.systemui.keyboard.shortcut.domain.interactor
 
+import android.view.KeyEvent
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
 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.keyboard.shortcut.shared.model.ShortcutCategory
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
+import com.android.systemui.keyboard.shortcut.shared.model.shortcut
 import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesInteractor
 import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
@@ -57,6 +64,7 @@
     @Test
     fun categories_stateActive_emitsAllCategoriesInOrder() =
         testScope.runTest {
+            helper.setImeShortcuts(imeShortcutGroups)
             val categories by collectLastValue(interactor.shortcutCategories)
 
             helper.showFromActivity()
@@ -64,7 +72,8 @@
             assertThat(categories)
                 .containsExactly(
                     systemShortcutsSource.systemShortcutsCategory(),
-                    multitaskingShortcutsSource.multitaskingShortcutCategory()
+                    multitaskingShortcutsSource.multitaskingShortcutCategory(),
+                    imeShortcutCategory
                 )
                 .inOrder()
         }
@@ -78,4 +87,165 @@
 
             assertThat(categories).isEmpty()
         }
+
+    fun categories_stateActive_emitsGroupedShortcuts() =
+        testScope.runTest {
+            helper.setImeShortcuts(imeShortcutsGroupsWithDuplicateLabels)
+            val categories by collectLastValue(interactor.shortcutCategories)
+
+            helper.showFromActivity()
+
+            assertThat(categories)
+                .containsExactly(
+                    systemShortcutsSource.systemShortcutsCategory(),
+                    multitaskingShortcutsSource.multitaskingShortcutCategory(),
+                    expectedGroupedShortcutCategories
+                )
+        }
+
+    private val switchToNextLanguageShortcut =
+        shortcut(label = "switch to next language") {
+            command(KeyEvent.META_CTRL_ON, KeyEvent.KEYCODE_SPACE)
+        }
+
+    private val switchToNextLanguageKeyboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ switchToNextLanguageShortcut.label,
+            /* keycode = */ switchToNextLanguageShortcut.commands[0].keyCodes[1],
+            /* modifiers = */ switchToNextLanguageShortcut.commands[0].keyCodes[0],
+        )
+
+    private val switchToNextLanguageShortcutAlternative =
+        shortcut("switch to next language") {
+            command(KeyEvent.META_CTRL_ON, KeyEvent.KEYCODE_SPACE)
+        }
+
+    private val switchToNextLanguageKeyboardShortcutInfoAlternative =
+        KeyboardShortcutInfo(
+            /* label = */ switchToNextLanguageShortcutAlternative.label,
+            /* keycode = */ switchToNextLanguageShortcutAlternative.commands[0].keyCodes[1],
+            /* modifiers = */ switchToNextLanguageShortcutAlternative.commands[0].keyCodes[0],
+        )
+
+    private val switchToPreviousLanguageShortcut =
+        shortcut("switch to previous language") {
+            command(
+                KeyEvent.META_SHIFT_ON,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        }
+
+    private val switchToPreviousLanguageKeyboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ switchToPreviousLanguageShortcut.label,
+            /* keycode = */ switchToPreviousLanguageShortcut.commands[0].keyCodes[1],
+            /* modifiers = */ switchToPreviousLanguageShortcut.commands[0].keyCodes[0],
+        )
+
+    private val switchToPreviousLanguageShortcutAlternative =
+        shortcut("switch to previous language") {
+            command(
+                KeyEvent.META_SHIFT_ON,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        }
+
+    private val switchToPreviousLanguageKeyboardShortcutInfoAlternative =
+        KeyboardShortcutInfo(
+            /* label = */ switchToPreviousLanguageShortcutAlternative.label,
+            /* keycode = */ switchToPreviousLanguageShortcutAlternative.commands[0].keyCodes[1],
+            /* modifiers = */ switchToPreviousLanguageShortcutAlternative.commands[0].keyCodes[0],
+        )
+
+    private val showOnscreenKeyboardShortcut =
+        shortcut(label = "Show on-screen keyboard") {
+            command(KeyEvent.META_ALT_ON, KeyEvent.KEYCODE_K)
+        }
+
+    private val showOnScreenKeyboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ showOnscreenKeyboardShortcut.label,
+            /* keycode = */ showOnscreenKeyboardShortcut.commands[0].keyCodes[1],
+            /* modifiers = */ showOnscreenKeyboardShortcut.commands[0].keyCodes[0],
+        )
+
+    private val accessClipboardShortcut =
+        shortcut(label = "Access clipboard") { command(KeyEvent.META_ALT_ON, KeyEvent.KEYCODE_V) }
+
+    private val accessClipboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ accessClipboardShortcut.label,
+            /* keycode = */ accessClipboardShortcut.commands[0].keyCodes[1],
+            /* modifiers = */ accessClipboardShortcut.commands[0].keyCodes[0],
+        )
+
+    private val imeShortcutGroups =
+        listOf(
+            KeyboardShortcutGroup(
+                /* label = */ "input",
+                /* shortcutInfoList = */ listOf(
+                    switchToNextLanguageKeyboardShortcutInfo,
+                    switchToPreviousLanguageKeyboardShortcutInfo
+                )
+            )
+        )
+
+    private val imeShortcutCategory =
+        ShortcutCategory(
+            type = ShortcutCategoryType.IME,
+            subCategories =
+                listOf(
+                    ShortcutSubCategory(
+                        imeShortcutGroups[0].label.toString(),
+                        listOf(switchToNextLanguageShortcut, switchToPreviousLanguageShortcut)
+                    )
+                )
+        )
+
+    private val imeShortcutsGroupsWithDuplicateLabels =
+        listOf(
+            KeyboardShortcutGroup(
+                "input",
+                listOf(
+                    switchToNextLanguageKeyboardShortcutInfo,
+                    switchToNextLanguageKeyboardShortcutInfoAlternative,
+                    switchToPreviousLanguageKeyboardShortcutInfo,
+                    switchToPreviousLanguageKeyboardShortcutInfoAlternative
+                )
+            ),
+            KeyboardShortcutGroup(
+                "Gboard",
+                listOf(
+                    showOnScreenKeyboardShortcutInfo,
+                    accessClipboardShortcutInfo,
+                )
+            )
+        )
+
+    private val expectedGroupedShortcutCategories =
+        ShortcutCategory(
+            type = ShortcutCategoryType.IME,
+            subCategories =
+                listOf(
+                    ShortcutSubCategory(
+                        imeShortcutsGroupsWithDuplicateLabels[0].label.toString(),
+                        listOf(
+                            switchToNextLanguageShortcut.copy(
+                                commands =
+                                    switchToNextLanguageShortcut.commands +
+                                        switchToNextLanguageShortcutAlternative.commands
+                            ),
+                            switchToPreviousLanguageShortcut.copy(
+                                commands =
+                                    switchToPreviousLanguageShortcut.commands +
+                                        switchToPreviousLanguageShortcutAlternative.commands
+                            )
+                        ),
+                    ),
+                    ShortcutSubCategory(
+                        imeShortcutsGroupsWithDuplicateLabels[1].label.toString(),
+                        listOf(showOnscreenKeyboardShortcut, accessClipboardShortcut),
+                    )
+                )
+        )
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index bdc5fc3..49a72e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -46,8 +46,8 @@
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
-import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTouchHandlingInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
@@ -58,6 +58,7 @@
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.pulsingGestureListener
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -211,8 +212,8 @@
                 dumpManager = mock(),
                 userHandle = UserHandle.SYSTEM,
             )
-        val keyguardLongPressInteractor =
-            KeyguardLongPressInteractor(
+        val keyguardTouchHandlingInteractor =
+            KeyguardTouchHandlingInteractor(
                 appContext = mContext,
                 scope = testScope.backgroundScope,
                 transitionInteractor = kosmos.keyguardTransitionInteractor,
@@ -221,6 +222,7 @@
                 featureFlags = featureFlags,
                 broadcastDispatcher = broadcastDispatcher,
                 accessibilityManager = accessibilityManager,
+                pulsingGestureListener = kosmos.pulsingGestureListener,
             )
         underTest =
             KeyguardBottomAreaViewModel(
@@ -246,13 +248,13 @@
                     ),
                 bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
                 burnInHelperWrapper = burnInHelperWrapper,
-                longPressViewModel =
-                    KeyguardLongPressViewModel(
-                        interactor = keyguardLongPressInteractor,
+                keyguardTouchHandlingViewModel =
+                    KeyguardTouchHandlingViewModel(
+                        interactor = keyguardTouchHandlingInteractor,
                     ),
                 settingsMenuViewModel =
                     KeyguardSettingsMenuViewModel(
-                        interactor = keyguardLongPressInteractor,
+                        interactor = keyguardTouchHandlingInteractor,
                     ),
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java
index fbfd35f..c7a92d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java
@@ -36,7 +36,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.settings.FakeDisplayTracker;
-import com.android.systemui.statusbar.phone.BarTransitions;
+import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.statusbar.phone.LightBarTransitionsController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
index 5ed8a11..eae6cdb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
@@ -7,6 +7,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.model.SysUiState
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler
+import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.recents.OverviewProxyService
 import com.android.systemui.shared.system.QuickStepContract
 import com.android.systemui.shared.system.TaskStackChangeListeners
@@ -70,6 +71,8 @@
     lateinit var mCurrentSysUiState: NavBarHelper.CurrentSysuiState
     @Mock
     lateinit var mStatusBarKeyguardViewManager: StatusBarKeyguardViewManager
+    @Mock
+    lateinit var mStatusBarStateController: StatusBarStateController
 
     @Before
     fun setup() {
@@ -80,7 +83,7 @@
         `when`(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState)
         mTaskStackChangeListeners = TaskStackChangeListeners.getTestInstance()
         mTaskbarDelegate = TaskbarDelegate(context, mLightBarControllerFactory,
-            mStatusBarKeyguardViewManager)
+            mStatusBarKeyguardViewManager, mStatusBarStateController)
         mTaskbarDelegate.setDependencies(mCommandQueue, mOverviewProxyService, mNavBarHelper,
         mNavigationModeController, mSysUiState, mDumpManager, mAutoHideController,
                 mLightBarController, mOptionalPip, mBackAnimation, mTaskStackChangeListeners)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
index 6733ead..809fb3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
@@ -26,6 +26,7 @@
 
 import static com.android.internal.infra.AndroidFuture.completedFuture;
 import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED;
+import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CLIP_DATA;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -37,6 +38,7 @@
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
+import android.content.ClipData;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -81,6 +83,7 @@
 
     private static final String TEST_URI_STRING = "www.test-uri.com";
     private static final Uri TEST_URI = Uri.parse(TEST_URI_STRING);
+    private static final ClipData TEST_CLIP_DATA = ClipData.newRawUri("Test backlinks", TEST_URI);
     private static final int TEST_UID = 42;
     private static final String TEST_CALLING_PACKAGE = "test-calling-package";
 
@@ -238,6 +241,7 @@
         Bundle bundle = new Bundle();
         bundle.putParcelable(EXTRA_SCREENSHOT_URI, TEST_URI);
         bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
+        bundle.putParcelable(EXTRA_CLIP_DATA, TEST_CLIP_DATA);
         activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle);
         waitForIdleSync();
 
@@ -245,7 +249,10 @@
         assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
         assertThat(getStatusCodeExtra(actualResult.getResultData()))
                 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
-        assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI);
+
+        Intent resultData = actualResult.getResultData();
+        assertThat(resultData.getData()).isEqualTo(TEST_URI);
+        assertThat(resultData.getClipData()).isEqualTo(TEST_CLIP_DATA);
         assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 15c4bfc..e7ca091 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -111,7 +111,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingLockscreenHostedTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
@@ -334,7 +334,7 @@
     @Mock protected PrimaryBouncerToGoneTransitionViewModel
             mPrimaryBouncerToGoneTransitionViewModel;
     @Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor;
-    @Mock protected KeyguardLongPressViewModel mKeyuardLongPressViewModel;
+    @Mock protected KeyguardTouchHandlingViewModel mKeyuardTouchHandlingViewModel;
     @Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor;
     @Mock protected MotionEvent mDownMotionEvent;
     @Mock protected CoroutineDispatcher mMainDispatcher;
@@ -755,7 +755,7 @@
                 mMainDispatcher,
                 mKeyguardTransitionInteractor,
                 mDumpManager,
-                mKeyuardLongPressViewModel,
+                mKeyuardTouchHandlingViewModel,
                 mKeyguardInteractor,
                 mActivityStarter,
                 mSharedNotificationContainerInteractor,
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 e984200..a7f36c3 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
@@ -20,6 +20,7 @@
 import android.app.Notification.CATEGORY_EVENT
 import android.app.Notification.CATEGORY_REMINDER
 import android.app.NotificationManager
+import android.content.pm.PackageManager.PERMISSION_DENIED
 import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -28,11 +29,16 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
-import java.util.Optional
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mockito.anyString
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
+import org.mockito.kotlin.whenever
+import java.util.Optional
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -58,7 +64,9 @@
             avalancheProvider,
             systemSettings,
             packageManager,
-            Optional.of(bubbles)
+            Optional.of(bubbles),
+            context,
+            notificationManager
         )
     }
 
@@ -87,12 +95,60 @@
     // because avalanche code is based on the suppression refactor.
 
     @Test
+    fun testAvalancheFilter_suppress_hasNotSeenEdu_showEduHun() {
+        setAllowedEmergencyPkg(false)
+        whenever(avalancheProvider.timeoutMs).thenReturn(20)
+        whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
+
+        val avalancheSuppressor = AvalancheSuppressor(
+            avalancheProvider, systemClock, systemSettings, packageManager,
+            uiEventLogger, context, notificationManager
+        )
+        avalancheSuppressor.hasSeenEdu = false
+
+        withFilter(avalancheSuppressor) {
+            ensurePeekState()
+            assertShouldNotHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    whenMs = whenAgo(5)
+                }
+            )
+        }
+        verify(notificationManager, times(1)).notify(anyInt(), any())
+    }
+
+    @Test
+    fun testAvalancheFilter_suppress_hasSeenEduHun_doNotShowEduHun() {
+        setAllowedEmergencyPkg(false)
+        whenever(avalancheProvider.timeoutMs).thenReturn(20)
+        whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
+
+        val avalancheSuppressor = AvalancheSuppressor(
+            avalancheProvider, systemClock, systemSettings, packageManager,
+            uiEventLogger, context, notificationManager
+        )
+        avalancheSuppressor.hasSeenEdu = true
+
+        withFilter(avalancheSuppressor) {
+            ensurePeekState()
+            assertShouldNotHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    whenMs = whenAgo(5)
+                }
+            )
+        }
+        verify(notificationManager, times(0)).notify(anyInt(), any())
+    }
+
+    @Test
     fun testAvalancheFilter_duringAvalanche_allowConversationFromAfterEvent() {
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -112,7 +168,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldNotHeadsUp(
@@ -132,7 +188,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -150,7 +206,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -168,7 +224,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -186,7 +242,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -204,7 +260,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             assertFsiNotSuppressed()
         }
@@ -216,7 +272,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -228,20 +284,24 @@
         }
     }
 
-    @Test
-    fun testAvalancheFilter_duringAvalanche_allowEmergency() {
-        avalancheProvider.startTime = whenAgo(10)
-
+    private fun setAllowedEmergencyPkg(allow: Boolean) {
         `when`(
             packageManager.checkPermission(
                 org.mockito.Mockito.eq(permission.RECEIVE_EMERGENCY_BROADCAST),
                 anyString()
             )
-        ).thenReturn(PERMISSION_GRANTED)
+        ).thenReturn(if (allow) PERMISSION_GRANTED else PERMISSION_DENIED)
+    }
+
+    @Test
+    fun testAvalancheFilter_duringAvalanche_allowEmergency() {
+        avalancheProvider.startTime = whenAgo(10)
+
+        setAllowedEmergencyPkg(true)
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
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 a457405..378705a 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
@@ -31,6 +31,7 @@
 import android.app.Notification.GROUP_ALERT_SUMMARY
 import android.app.Notification.VISIBILITY_PRIVATE
 import android.app.NotificationChannel
+import android.app.NotificationManager
 import android.app.NotificationManager.IMPORTANCE_DEFAULT
 import android.app.NotificationManager.IMPORTANCE_HIGH
 import android.app.NotificationManager.IMPORTANCE_LOW
@@ -133,7 +134,7 @@
     protected val bubbles: Bubbles = mock()
     lateinit var systemSettings: SystemSettings
     protected val packageManager: PackageManager = mock()
-
+    protected val notificationManager: NotificationManager = mock()
     protected abstract val provider: VisualInterruptionDecisionProvider
 
     private val neverSuppresses = object : NotificationInterruptSuppressor {}
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 01e638b..f4cebd7 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
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.interruption
 
+import android.app.NotificationManager
+import android.content.Context
 import android.content.pm.PackageManager
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.Handler
@@ -58,6 +60,8 @@
         systemSettings: SystemSettings,
         packageManager: PackageManager,
         bubbles: Optional<Bubbles>,
+        context: Context,
+        notificationManager: NotificationManager
     ): VisualInterruptionDecisionProvider {
         return if (VisualInterruptionRefactor.isEnabled) {
             VisualInterruptionDecisionProviderImpl(
@@ -79,7 +83,9 @@
                 avalancheProvider,
                 systemSettings,
                 packageManager,
-                bubbles
+                bubbles,
+                context,
+                notificationManager
             )
         } else {
             NotificationInterruptStateProviderWrapper(
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 1eb33ce..d2540a6 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
@@ -53,6 +53,7 @@
 
 import android.app.ActivityManager;
 import android.app.IWallpaperManager;
+import android.app.NotificationManager;
 import android.app.WallpaperManager;
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
@@ -339,6 +340,7 @@
     @Mock private KeyboardShortcuts mKeyboardShortcuts;
     @Mock private KeyboardShortcutListSearch mKeyboardShortcutListSearch;
     @Mock private PackageManager mPackageManager;
+    @Mock private NotificationManager mNotificationManager;
     @Mock private GlanceableHubContainerController mGlanceableHubContainerController;
     @Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
 
@@ -399,7 +401,9 @@
                         mAvalancheProvider,
                         mSystemSettings,
                         mPackageManager,
-                        Optional.of(mBubbles));
+                        Optional.of(mBubbles),
+                        mContext,
+                        mNotificationManager);
         mVisualInterruptionDecisionProvider.start();
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index a27073c..88ec18d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -18,7 +18,7 @@
 
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
-import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
 
 import static junit.framework.Assert.assertTrue;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitionsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitionsTest.kt
index c4568a9..318656b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitionsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitionsTest.kt
@@ -21,12 +21,12 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSLUCENT
-import com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSLUCENT
+import com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSPARENT
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 70afbd8..ffe7750 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -247,7 +247,7 @@
         ExpandableNotificationRow row = helper.createRow();
         RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
 
-        view.addOnVisibilityChangedListener(null);
+        view.setOnVisibilityChangedListener(null);
         view.setVisibility(View.INVISIBLE);
         view.setVisibility(View.VISIBLE);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
index 97688d5..ef2d4ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt
@@ -29,10 +29,16 @@
 import com.android.systemui.unfold.util.TestFoldStateProvider
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
+/**
+ * This test class tests [PhysicsBasedUnfoldTransitionProgressProvider] in a more E2E
+ * fashion, it uses real handler thread and timings, so it might be perceptible to more flakiness
+ * compared to the other unit tests that do not perform real multithreaded interactions.
+ */
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
@@ -44,8 +50,8 @@
         mock<UnfoldFrameCallbackScheduler.Factory>().apply {
             whenever(create()).then { UnfoldFrameCallbackScheduler() }
         }
-    private val mockBgHandler = mock<Handler>()
-    private val fakeHandler = Handler(HandlerThread("UnfoldBg").apply { start() }.looper)
+    private val handlerThread = HandlerThread("UnfoldBg").apply { start() }
+    private val bgHandler = Handler(handlerThread.looper)
 
     @Before
     fun setUp() {
@@ -54,20 +60,26 @@
                 context,
                 schedulerFactory,
                 foldStateProvider = foldStateProvider,
-                progressHandler = fakeHandler
+                progressHandler = bgHandler
             )
         progressProvider.addCallback(listener)
     }
 
+    @After
+    fun after() {
+        handlerThread.quit()
+    }
+
     @Test
     fun testUnfold_emitsIncreasingTransitionEvents() {
         runOnProgressThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
             { foldStateProvider.sendHingeAngleUpdate(10f) },
-            { foldStateProvider.sendUnfoldedScreenAvailable() },
-            { foldStateProvider.sendHingeAngleUpdate(90f) },
-            { foldStateProvider.sendHingeAngleUpdate(180f) },
-            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) },
+            { foldStateProvider.sendUnfoldedScreenAvailable() }
+        )
+        sendHingeAngleAndEnsureAnimationUpdate(90f, 120f, 180f)
+        runOnProgressThreadWithInterval(
+            { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) }
         )
 
         with(listener.ensureTransitionFinished()) {
@@ -91,7 +103,7 @@
     }
 
     @Test
-    fun testUnfold_screenAvailableOnlyAfterFullUnfold_emitsIncreasingTransitionEvents() {
+    fun testUnfold_screenAvailableOnlyAfterFullUnfold_finishesWithUnfoldEvent() {
         runOnProgressThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
             { foldStateProvider.sendHingeAngleUpdate(10f) },
@@ -102,7 +114,6 @@
         )
 
         with(listener.ensureTransitionFinished()) {
-            assertIncreasingProgress()
             assertFinishedWithUnfold()
         }
     }
@@ -111,9 +122,9 @@
     fun testFold_emitsDecreasingTransitionEvents() {
         runOnProgressThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_CLOSING) },
-            { foldStateProvider.sendHingeAngleUpdate(170f) },
-            { foldStateProvider.sendHingeAngleUpdate(90f) },
-            { foldStateProvider.sendHingeAngleUpdate(10f) },
+        )
+        sendHingeAngleAndEnsureAnimationUpdate(170f, 90f, 10f)
+        runOnProgressThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) },
         )
 
@@ -127,9 +138,9 @@
     fun testUnfoldAndStopUnfolding_finishesTheUnfoldTransition() {
         runOnProgressThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
-            { foldStateProvider.sendUnfoldedScreenAvailable() },
-            { foldStateProvider.sendHingeAngleUpdate(10f) },
-            { foldStateProvider.sendHingeAngleUpdate(90f) },
+            { foldStateProvider.sendUnfoldedScreenAvailable() })
+        sendHingeAngleAndEnsureAnimationUpdate(10f, 50f, 90f)
+        runOnProgressThreadWithInterval(
             { foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN) },
         )
 
@@ -159,12 +170,22 @@
         with(listener.ensureTransitionFinished()) { assertHasFoldAnimationAtTheEnd() }
     }
 
+    private fun sendHingeAngleAndEnsureAnimationUpdate(vararg angles: Float) {
+        angles.forEach { angle ->
+            listener.waitForProgressChangeAfter {
+                bgHandler.post {
+                    foldStateProvider.sendHingeAngleUpdate(angle)
+                }
+            }
+        }
+    }
+
     private fun runOnProgressThreadWithInterval(
         vararg blocks: () -> Unit,
         intervalMillis: Long = 60,
     ) {
         blocks.forEach {
-            fakeHandler.post(it)
+            bgHandler.post(it)
             Thread.sleep(intervalMillis)
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
index bbc96f70..6e8bf85 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt
@@ -68,6 +68,24 @@
         return recordings.first()
     }
 
+    /**
+     * Number of progress event for the currently running transition
+     * Returns null if there is no currently running transition
+     */
+    val currentTransitionProgressEventCount: Int?
+        get() = currentRecording?.progressHistory?.size
+
+    /**
+     * Runs [block] and ensures that there was at least once onTransitionProgress event after that
+     */
+    fun waitForProgressChangeAfter(block: () -> Unit) {
+        val eventCount = currentTransitionProgressEventCount
+        block()
+        waitForCondition {
+            currentTransitionProgressEventCount != eventCount
+        }
+    }
+
     fun assertStarted() {
         assertWithMessage("Transition didn't start").that(currentRecording).isNotNull()
     }
@@ -86,7 +104,7 @@
     }
 
     class UnfoldTransitionRecording {
-        private val progressHistory: MutableList<Float> = arrayListOf()
+        val progressHistory: MutableList<Float> = arrayListOf()
         private var finishingInvocations: Int = 0
 
         fun addProgress(progress: Float) {
@@ -142,6 +160,6 @@
     }
 
     private companion object {
-        private const val MIN_ANIMATION_EVENTS = 5
+        private const val MIN_ANIMATION_EVENTS = 3
     }
 }
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 c5fbc39..dc7a2c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -59,6 +59,7 @@
 import android.app.IActivityManager;
 import android.app.INotificationManager;
 import android.app.Notification;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -473,7 +474,9 @@
                         mock(AvalancheProvider.class),
                         mock(SystemSettings.class),
                         mock(PackageManager.class),
-                        Optional.of(mock(Bubbles.class))
+                        Optional.of(mock(Bubbles.class)),
+                        mContext,
+                        mock(NotificationManager.class)
                         );
         interruptionDecisionProvider.start();
 
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/display/AmbientDisplayConfigurationKosmos.kt b/packages/SystemUI/tests/utils/src/android/hardware/display/AmbientDisplayConfigurationKosmos.kt
new file mode 100644
index 0000000..3f3c30f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/hardware/display/AmbientDisplayConfigurationKosmos.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 android.hardware.display
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.ambientDisplayConfiguration by Fixture {
+    FakeAmbientDisplayConfiguration(applicationContext)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index f51036f..e00f980 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -19,6 +19,7 @@
 import android.content.applicationContext
 import android.content.res.mainResources
 import android.hardware.input.fakeInputManager
+import android.view.windowManager
 import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCategoriesRepository
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
@@ -59,6 +60,8 @@
         ShortcutHelperCategoriesRepository(
             shortcutHelperSystemShortcutsSource,
             shortcutHelperMultiTaskingShortcutsSource,
+            windowManager,
+            shortcutHelperStateRepository
         )
     }
 
@@ -68,7 +71,8 @@
             shortcutHelperStateRepository,
             applicationContext,
             broadcastDispatcher,
-            fakeCommandQueue
+            fakeCommandQueue,
+            windowManager
         )
     }
 
@@ -83,12 +87,7 @@
     }
 
 val Kosmos.shortcutHelperCategoriesInteractor by
-    Kosmos.Fixture {
-        ShortcutHelperCategoriesInteractor(
-            shortcutHelperStateRepository,
-            shortcutHelperCategoriesRepository
-        )
-    }
+    Kosmos.Fixture { ShortcutHelperCategoriesInteractor(shortcutHelperCategoriesRepository) }
 
 val Kosmos.shortcutHelperViewModel by
     Kosmos.Fixture { ShortcutHelperViewModel(testDispatcher, shortcutHelperStateInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
index 36608ff..40510db 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
@@ -18,20 +18,45 @@
 
 import android.content.Context
 import android.content.Intent
+import android.view.KeyboardShortcutGroup
+import android.view.WindowManager
+import android.view.WindowManager.KeyboardShortcutsReceiver
 import com.android.systemui.broadcast.FakeBroadcastDispatcher
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
 
 class ShortcutHelperTestHelper(
     repo: ShortcutHelperStateRepository,
     private val context: Context,
     private val fakeBroadcastDispatcher: FakeBroadcastDispatcher,
     private val fakeCommandQueue: FakeCommandQueue,
+    windowManager: WindowManager
 ) {
 
+    companion object {
+        const val DEFAULT_DEVICE_ID = 123
+    }
+
+    private var imeShortcuts: List<KeyboardShortcutGroup> = emptyList()
+
     init {
+        whenever(windowManager.requestImeKeyboardShortcuts(any(), any())).thenAnswer {
+            val keyboardShortcutReceiver = it.getArgument<KeyboardShortcutsReceiver>(0)
+            keyboardShortcutReceiver.onKeyboardShortcutsReceived(imeShortcuts)
+            return@thenAnswer Unit
+        }
         repo.start()
     }
 
+    /**
+     * Use this method to set what ime shortcuts should be returned from windowManager in tests. By
+     * default windowManager.requestImeKeyboardShortcuts will return emptyList. See init block.
+     */
+    fun setImeShortcuts(imeShortcuts: List<KeyboardShortcutGroup>) {
+        this.imeShortcuts = imeShortcuts
+    }
+
     fun hideThroughCloseSystemDialogs() {
         fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
             context,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
index c06f833..73799b6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
@@ -24,10 +24,11 @@
 import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.shade.pulsingGestureListener
 
-val Kosmos.keyguardLongPressInteractor by
+val Kosmos.keyguardTouchHandlingInteractor by
     Kosmos.Fixture {
-        KeyguardLongPressInteractor(
+        KeyguardTouchHandlingInteractor(
             appContext = applicationContext,
             scope = applicationCoroutineScope,
             transitionInteractor = keyguardTransitionInteractor,
@@ -36,5 +37,6 @@
             featureFlags = featureFlagsClassic,
             broadcastDispatcher = broadcastDispatcher,
             accessibilityManager = accessibilityManagerWrapper,
+            pulsingGestureListener = pulsingGestureListener,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
index 3c9846a..281d7b0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
@@ -16,12 +16,12 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.domain.interactor.keyguardLongPressInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTouchHandlingInteractor
 import com.android.systemui.kosmos.Kosmos
 
-val Kosmos.keyguardLongPressViewModel by
+val Kosmos.keyguardTouchHandlingViewModel by
     Kosmos.Fixture {
-        KeyguardLongPressViewModel(
-            interactor = keyguardLongPressInteractor,
+        KeyguardTouchHandlingViewModel(
+            interactor = keyguardTouchHandlingInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
index 30a4f21..24e47b0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -30,7 +30,7 @@
             clockInteractor = keyguardClockInteractor,
             interactor = keyguardBlueprintInteractor,
             authController = authController,
-            longPress = keyguardLongPressViewModel,
+            touchHandling = keyguardTouchHandlingViewModel,
             shadeInteractor = shadeInteractor,
             applicationScope = applicationCoroutineScope,
             unfoldTransitionInteractor = unfoldTransitionInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/PulsingGestureListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/PulsingGestureListenerKosmos.kt
new file mode 100644
index 0000000..4fc2228
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/PulsingGestureListenerKosmos.kt
@@ -0,0 +1,44 @@
+/*
+ * 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
+
+import android.hardware.display.ambientDisplayConfiguration
+import com.android.systemui.classifier.falsingManager
+import com.android.systemui.dock.dockManager
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.keyguard.domain.interactor.dozeInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.settings.userTracker
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.pulsingGestureListener by Fixture {
+    PulsingGestureListener(
+        falsingManager = falsingManager,
+        dockManager = dockManager,
+        powerInteractor = powerInteractor,
+        ambientDisplayConfiguration = ambientDisplayConfiguration,
+        statusBarStateController = statusBarStateController,
+        shadeLogger = mock(),
+        dozeInteractor = dozeInteractor,
+        userTracker = userTracker,
+        tunerService = mock(),
+        dumpManager = dumpManager,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelKosmos.kt
new file mode 100644
index 0000000..989c3a5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelKosmos.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.systemui.shade.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
+import com.android.systemui.qs.footerActionsController
+import com.android.systemui.qs.footerActionsViewModelFactory
+import com.android.systemui.qs.ui.adapter.qsSceneAdapter
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
+
+val Kosmos.shadeSceneViewModel: ShadeSceneViewModel by
+    Kosmos.Fixture {
+        ShadeSceneViewModel(
+            applicationScope = applicationCoroutineScope,
+            shadeHeaderViewModel = shadeHeaderViewModel,
+            qsSceneAdapter = qsSceneAdapter,
+            brightnessMirrorViewModel = brightnessMirrorViewModel,
+            mediaCarouselInteractor = mediaCarouselInteractor,
+            shadeInteractor = shadeInteractor,
+            footerActionsViewModelFactory = footerActionsViewModelFactory,
+            footerActionsController = footerActionsController,
+            sceneInteractor = sceneInteractor,
+            unfoldTransitionInteractor = unfoldTransitionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
index f0eea38..a0e9303 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
@@ -18,10 +18,10 @@
 
 import com.android.systemui.dump.dumpManager
 import com.android.systemui.flags.featureFlagsClassic
-import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.ui.viewmodel.shadeSceneViewModel
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
 
 val Kosmos.notificationsPlaceholderViewModel by Fixture {
@@ -29,7 +29,7 @@
         dumpManager = dumpManager,
         interactor = notificationStackAppearanceInteractor,
         shadeInteractor = shadeInteractor,
+        shadeSceneViewModel = shadeSceneViewModel,
         featureFlags = featureFlagsClassic,
-        keyguardInteractor = keyguardInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt
index b2b19de..e6b52f0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.internal.logging.uiEventLogger
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.volume.domain.interactor.audioVolumeInteractor
+import com.android.systemui.volume.shared.volumePanelLogger
 import kotlinx.coroutines.CoroutineScope
 
 val Kosmos.audioStreamSliderViewModelFactory by
@@ -36,6 +37,7 @@
                     applicationContext,
                     audioVolumeInteractor,
                     uiEventLogger,
+                    volumePanelLogger,
                 )
             }
         }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/shared/VolumePanelLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/shared/VolumePanelLoggerKosmos.kt
new file mode 100644
index 0000000..3a7574d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/shared/VolumePanelLoggerKosmos.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.volume.shared
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.volume.panel.shared.VolumePanelLogger
+
+val Kosmos.volumePanelLogger by Kosmos.Fixture { VolumePanelLogger(logcatLogBuffer()) }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b23f5f2..f6b3b39 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5481,8 +5481,13 @@
     private boolean isHomeLaunchDelayable() {
         // This feature is disabled on Auto since it seems to add an unacceptably long boot delay
         // without even solving the underlying issue (it merely hits the timeout).
-        return enableHomeDelay() &&
-                !mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+        // This feature is disabled on TV since the ThemeOverlayController is currently not present
+        // and therefore we do not want to wait unnecessarily.
+        // This feature is currently disabled in WearOS to avoid extreme boot regressions
+        return enableHomeDelay()
+                && !mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+                && !mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)
+                && !mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
     }
 
     final void ensureBootCompleted() {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 4512284..cc4f7d9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -2865,12 +2865,13 @@
         return mDeviceInventory.getImmutableDeviceInventory();
     }
 
-    void addOrUpdateDeviceSAStateInInventory(AdiDeviceState deviceState) {
-        mDeviceInventory.addOrUpdateDeviceSAStateInInventory(deviceState);
+    void addOrUpdateDeviceSAStateInInventory(AdiDeviceState deviceState, boolean syncInventory) {
+        mDeviceInventory.addOrUpdateDeviceSAStateInInventory(deviceState, syncInventory);
     }
 
-    void addOrUpdateBtAudioDeviceCategoryInInventory(AdiDeviceState deviceState) {
-        mDeviceInventory.addOrUpdateAudioDeviceCategoryInInventory(deviceState);
+    void addOrUpdateBtAudioDeviceCategoryInInventory(
+            AdiDeviceState deviceState, boolean syncInventory) {
+        mDeviceInventory.addOrUpdateAudioDeviceCategoryInInventory(deviceState, syncInventory);
     }
 
     @Nullable
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index ba7aee0..6ff4a61 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -135,9 +135,10 @@
      * AdiDeviceState in the {@link AudioDeviceInventory#mDeviceInventory} list.
      * @param deviceState the device to update
      */
-    void addOrUpdateDeviceSAStateInInventory(AdiDeviceState deviceState) {
+    void addOrUpdateDeviceSAStateInInventory(AdiDeviceState deviceState, boolean syncInventory) {
         synchronized (mDeviceInventoryLock) {
-            mDeviceInventory.merge(deviceState.getDeviceId(), deviceState, (oldState, newState) -> {
+            mDeviceInventory.merge(deviceState.getDeviceId(), deviceState,
+                    (oldState, newState) -> {
                 oldState.setHasHeadTracker(newState.hasHeadTracker());
                 oldState.setHeadTrackerEnabled(newState.isHeadTrackerEnabled());
                 oldState.setSAEnabled(newState.isSAEnabled());
@@ -145,7 +146,9 @@
             });
             checkDeviceInventorySize_l();
         }
-        mDeviceBroker.postSynchronizeAdiDevicesInInventory(deviceState);
+        if (syncInventory) {
+            mDeviceBroker.postSynchronizeAdiDevicesInInventory(deviceState);
+        }
     }
 
     /**
@@ -196,7 +199,8 @@
      * AdiDeviceState in the {@link AudioDeviceInventory#mDeviceInventory} list.
      * @param deviceState the device to update
      */
-    void addOrUpdateAudioDeviceCategoryInInventory(AdiDeviceState deviceState) {
+    void addOrUpdateAudioDeviceCategoryInInventory(
+            AdiDeviceState deviceState, boolean syncInventory) {
         AtomicBoolean updatedCategory = new AtomicBoolean(false);
         synchronized (mDeviceInventoryLock) {
             if (automaticBtDeviceType()) {
@@ -218,7 +222,9 @@
         if (updatedCategory.get()) {
             mDeviceBroker.postUpdatedAdiDeviceState(deviceState, false /*initSA*/);
         }
-        mDeviceBroker.postSynchronizeAdiDevicesInInventory(deviceState);
+        if (syncInventory) {
+            mDeviceBroker.postSynchronizeAdiDevicesInInventory(deviceState);
+        }
     }
 
     void addAudioDeviceWithCategoryInInventoryIfNeeded(@NonNull String address,
@@ -235,14 +241,14 @@
         boolean bleCategoryFound = false;
         AdiDeviceState deviceState = findBtDeviceStateForAddress(address, DEVICE_OUT_BLE_HEADSET);
         if (deviceState != null) {
-            addOrUpdateAudioDeviceCategoryInInventory(deviceState);
+            addOrUpdateAudioDeviceCategoryInInventory(deviceState, true /*syncInventory*/);
             btCategory = deviceState.getAudioDeviceCategory();
             bleCategoryFound = true;
         }
 
         deviceState = findBtDeviceStateForAddress(address, DEVICE_OUT_BLUETOOTH_A2DP);
         if (deviceState != null) {
-            addOrUpdateAudioDeviceCategoryInInventory(deviceState);
+            addOrUpdateAudioDeviceCategoryInInventory(deviceState, true /*syncInventory*/);
             int a2dpCategory = deviceState.getAudioDeviceCategory();
             if (bleCategoryFound && a2dpCategory != btCategory) {
                 Log.w(TAG, "Found different audio device category for A2DP and BLE profiles with "
@@ -269,23 +275,43 @@
     }
 
     /**
-     * synchronize AdiDeviceState for LE devices in the same group
+     * Synchronize AdiDeviceState for LE devices in the same group
+     * or BT classic devices with the same address.
+     * @param updatedDevice the device state to synchronize or null.
+     * Called with null once after the device inventory and spatializer helper
+     * have been initialized to resync all devices.
      */
     void onSynchronizeAdiDevicesInInventory(AdiDeviceState updatedDevice) {
         synchronized (mDevicesLock) {
             synchronized (mDeviceInventoryLock) {
-                boolean found = false;
-                found |= synchronizeBleDeviceInInventory(updatedDevice);
-                if (automaticBtDeviceType()) {
-                    found |= synchronizeDeviceProfilesInInventory(updatedDevice);
-                }
-                if (found) {
-                    mDeviceBroker.postPersistAudioDeviceSettings();
+                if (updatedDevice != null) {
+                    onSynchronizeAdiDeviceInInventory_l(updatedDevice);
+                } else {
+                    for (AdiDeviceState ads : mDeviceInventory.values()) {
+                        onSynchronizeAdiDeviceInInventory_l(ads);
+                    }
                 }
             }
         }
     }
 
+    /**
+     * Synchronize AdiDeviceState for LE devices in the same group
+     * or BT classic devices with the same address.
+     * @param updatedDevice the device state to synchronize.
+     */
+    @GuardedBy({"mDevicesLock", "mDeviceInventoryLock"})
+    void onSynchronizeAdiDeviceInInventory_l(AdiDeviceState updatedDevice) {
+        boolean found = false;
+        found |= synchronizeBleDeviceInInventory(updatedDevice);
+        if (automaticBtDeviceType()) {
+            found |= synchronizeDeviceProfilesInInventory(updatedDevice);
+        }
+        if (found) {
+            mDeviceBroker.postPersistAudioDeviceSettings();
+        }
+    }
+
     @GuardedBy("mDeviceInventoryLock")
     private void checkDeviceInventorySize_l() {
         if (mDeviceInventory.size() > MAX_DEVICE_INVENTORY_ENTRIES) {
@@ -595,6 +621,9 @@
             mDeviceName = TextUtils.emptyIfNull(deviceName);
             mDeviceAddress = TextUtils.emptyIfNull(address);
             mDeviceIdentityAddress = TextUtils.emptyIfNull(identityAddress);
+            if (mDeviceIdentityAddress.isEmpty()) {
+                mDeviceIdentityAddress = mDeviceAddress;
+            }
             mDeviceCodecFormat = codecFormat;
             mGroupId = groupId;
             mPeerDeviceAddress = TextUtils.emptyIfNull(peerAddress);
@@ -2951,8 +2980,8 @@
             // Note if the device is not compatible with spatialization mode or the device
             // type is not canonical, it will be ignored in {@link SpatializerHelper}.
             if (devState != null) {
-                addOrUpdateDeviceSAStateInInventory(devState);
-                addOrUpdateAudioDeviceCategoryInInventory(devState);
+                addOrUpdateDeviceSAStateInInventory(devState, false /*syncInventory*/);
+                addOrUpdateAudioDeviceCategoryInInventory(devState, false /*syncInventory*/);
             }
         }
     }
diff --git a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
index 14eae8d..c5180af 100644
--- a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
+++ b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
@@ -36,6 +36,7 @@
 import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.IntArray;
+import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.media.permission.INativePermissionController;
@@ -61,6 +62,9 @@
 
     static final String[] MONITORED_PERMS = new String[PermissionEnum.ENUM_SIZE];
 
+    static final byte[] HDS_PERMS = new byte[] {PermissionEnum.CAPTURE_AUDIO_HOTWORD,
+            PermissionEnum.CAPTURE_AUDIO_OUTPUT, PermissionEnum.RECORD_AUDIO};
+
     static {
         MONITORED_PERMS[PermissionEnum.RECORD_AUDIO] = RECORD_AUDIO;
         MONITORED_PERMS[PermissionEnum.MODIFY_AUDIO_ROUTING] = MODIFY_AUDIO_ROUTING;
@@ -88,6 +92,7 @@
 
     @GuardedBy("mLock")
     private final Map<Integer, Set<String>> mPackageMap;
+
     // Values are sorted
     @GuardedBy("mLock")
     private final int[][] mPermMap = new int[PermissionEnum.ENUM_SIZE][];
@@ -95,6 +100,9 @@
     @GuardedBy("mLock")
     private boolean mIsUpdateDeferred = true;
 
+    @GuardedBy("mLock")
+    private int mHdsUid = -1;
+
     /**
      * @param appInfos - PackageState for all apps on the device, used to populate init state
      */
@@ -124,7 +132,7 @@
             try {
                 for (byte i = 0; i < PermissionEnum.ENUM_SIZE; i++) {
                     if (mIsUpdateDeferred) {
-                        mPermMap[i] = getUidsHoldingPerm(MONITORED_PERMS[i]);
+                        mPermMap[i] = getUidsHoldingPerm(i);
                     }
                     mDest.populatePermissionState(i, mPermMap[i]);
                 }
@@ -184,7 +192,7 @@
             }
             try {
                 for (byte i = 0; i < PermissionEnum.ENUM_SIZE; i++) {
-                    var newPerms = getUidsHoldingPerm(MONITORED_PERMS[i]);
+                    var newPerms = getUidsHoldingPerm(i);
                     if (!Arrays.equals(newPerms, mPermMap[i])) {
                         mPermMap[i] = newPerms;
                         mDest.populatePermissionState(i, newPerms);
@@ -199,6 +207,77 @@
         }
     }
 
+    public void setIsolatedServiceUid(int uid, int owningUid) {
+        synchronized (mLock) {
+            if (mHdsUid == uid) return;
+            var packageNameSet = mPackageMap.get(owningUid);
+            if (packageNameSet == null) return;
+            var packageName = packageNameSet.iterator().next();
+            onModifyPackageState(uid, packageName, /* isRemove= */ false);
+            // permissions
+            mHdsUid = uid;
+            if (mDest == null) {
+                mIsUpdateDeferred = true;
+                return;
+            }
+            try {
+                for (byte perm : HDS_PERMS) {
+                    int[] newPerms = new int[mPermMap[perm].length + 1];
+                    System.arraycopy(mPermMap[perm], 0, newPerms, 0, mPermMap[perm].length);
+                    newPerms[newPerms.length - 1] = mHdsUid;
+                    Arrays.sort(newPerms);
+                    mPermMap[perm] = newPerms;
+                    mDest.populatePermissionState(perm, newPerms);
+                }
+            } catch (RemoteException e) {
+                // We will re-init the state when the service comes back up
+                mDest = null;
+                // We didn't necessarily finish
+                mIsUpdateDeferred = true;
+            }
+        }
+    }
+
+    public void clearIsolatedServiceUid(int uid) {
+        synchronized (mLock) {
+            if (mHdsUid != uid) return;
+            var packageNameSet = mPackageMap.get(uid);
+            if (packageNameSet == null) return;
+            var packageName = packageNameSet.iterator().next();
+            onModifyPackageState(uid, packageName, /* isRemove= */ true);
+            // permissions
+            if (mDest == null) {
+                mIsUpdateDeferred = true;
+                return;
+            }
+            try {
+                for (byte perm : HDS_PERMS) {
+                    int[] newPerms = new int[mPermMap[perm].length - 1];
+                    int ind = Arrays.binarySearch(mPermMap[perm], uid);
+                    if (ind < 0) continue;
+                    System.arraycopy(mPermMap[perm], 0, newPerms, 0, ind);
+                    System.arraycopy(mPermMap[perm], ind + 1, newPerms, ind,
+                            mPermMap[perm].length - ind - 1);
+                    mPermMap[perm] = newPerms;
+                    mDest.populatePermissionState(perm, newPerms);
+                }
+            } catch (RemoteException e) {
+                // We will re-init the state when the service comes back up
+                mDest = null;
+                // We didn't necessarily finish
+                mIsUpdateDeferred = true;
+            }
+            mHdsUid = -1;
+        }
+    }
+
+    private boolean isSpecialHdsPermission(int perm) {
+        for (var hdsPerm : HDS_PERMS) {
+            if (perm == hdsPerm) return true;
+        }
+        return false;
+    }
+
     /** Called when full syncing package state to audioserver. */
     @GuardedBy("mLock")
     private void resetNativePackageState() {
@@ -223,16 +302,19 @@
 
     @GuardedBy("mLock")
     /** Return all uids (not app-ids) which currently hold a given permission. Not app-op aware */
-    private int[] getUidsHoldingPerm(String perm) {
+    private int[] getUidsHoldingPerm(int perm) {
         IntArray acc = new IntArray();
         for (int userId : mUserIdSupplier.get()) {
             for (int appId : mPackageMap.keySet()) {
                 int uid = UserHandle.getUid(userId, appId);
-                if (mPermissionPredicate.test(uid, perm)) {
+                if (mPermissionPredicate.test(uid, MONITORED_PERMS[perm])) {
                     acc.add(uid);
                 }
             }
         }
+        if (isSpecialHdsPermission(perm) && mHdsUid != -1) {
+            acc.add(mHdsUid);
+        }
         var unwrapped = acc.toArray();
         Arrays.sort(unwrapped);
         return unwrapped;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index c89992d..39cb5a9 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -9639,6 +9639,9 @@
 
                 case MSG_INIT_SPATIALIZER:
                     onInitSpatializer();
+                    // the device inventory can only be synchronized after the
+                    // spatializer has been initialized
+                    mDeviceBroker.postSynchronizeAdiDevicesInInventory(null);
                     mAudioEventWakeLock.release();
                     break;
 
@@ -11394,7 +11397,8 @@
 
         deviceState.setAudioDeviceCategory(btAudioDeviceCategory);
 
-        mDeviceBroker.addOrUpdateBtAudioDeviceCategoryInInventory(deviceState);
+        mDeviceBroker.addOrUpdateBtAudioDeviceCategoryInInventory(
+                deviceState, true /*syncInventory*/);
         mDeviceBroker.postPersistAudioDeviceSettings();
 
         mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes(),
@@ -11963,8 +11967,9 @@
         var umi = LocalServices.getService(UserManagerInternal.class);
         var pmsi = LocalServices.getService(PermissionManagerServiceInternal.class);
         var provider = new AudioServerPermissionProvider(packageStates,
-                (Integer uid, String perm) -> (pmsi.checkUidPermission(uid, perm,
-                        Context.DEVICE_ID_DEFAULT) == PackageManager.PERMISSION_GRANTED),
+                (Integer uid, String perm) -> ActivityManager.checkComponentPermission(perm, uid,
+                        /* owningUid = */ -1, /* exported */true)
+                    == PackageManager.PERMISSION_GRANTED,
                 () -> umi.getUserIds()
                 );
         audioPolicy.registerOnStartTask(() -> {
@@ -12326,13 +12331,19 @@
         }
 
         @Override
-        public void addAssistantServiceUid(int uid) {
+        public void addAssistantServiceUid(int uid, int owningUid) {
+            if (audioserverPermissions()) {
+                mPermissionProvider.setIsolatedServiceUid(uid, owningUid);
+            }
             sendMsg(mAudioHandler, MSG_ADD_ASSISTANT_SERVICE_UID, SENDMSG_QUEUE,
                     uid, 0, null, 0);
         }
 
         @Override
         public void removeAssistantServiceUid(int uid) {
+            if (audioserverPermissions()) {
+                mPermissionProvider.clearIsolatedServiceUid(uid);
+            }
             sendMsg(mAudioHandler, MSG_REMOVE_ASSISTANT_SERVICE_UID, SENDMSG_QUEUE,
                     uid, 0, null, 0);
         }
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 9ed1044..0de3428 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -142,7 +142,6 @@
     private static final int SCO_MODE_MAX = 2;
 
     private static final int BT_HEARING_AID_GAIN_MIN = -128;
-    private static final int BT_LE_AUDIO_MIN_VOL = 0;
     private static final int BT_LE_AUDIO_MAX_VOL = 255;
 
     // BtDevice constants currently rolling out under flag protection. Use own
@@ -211,8 +210,7 @@
     //----------------------------------------------------------------------
     // Interface for AudioDeviceBroker
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
     /*package*/ synchronized void onSystemReady() {
         mScoConnectionState = android.media.AudioManager.SCO_AUDIO_STATE_ERROR;
         resetBluetoothSco();
@@ -373,8 +371,7 @@
         return codecAndChanged;
     }
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
     /*package*/ synchronized void onReceiveBtEvent(Intent intent) {
         final String action = intent.getAction();
 
@@ -396,11 +393,11 @@
     }
 
     /**
-     * Exclusively called from AudioDeviceBroker when handling MSG_L_RECEIVED_BT_EVENT
+     * Exclusively called from AudioDeviceBroker (with mSetModeLock held)
+     * when handling MSG_L_RECEIVED_BT_EVENT in {@link #onReceiveBtEvent(Intent)}
      * as part of the serialization of the communication route selection
      */
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    @GuardedBy("BtHelper.this")
     private void onScoAudioStateChanged(int state) {
         boolean broadcast = false;
         int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
@@ -482,16 +479,14 @@
               || mScoAudioState == SCO_STATE_ACTIVATE_REQ;
     }
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
     /*package*/ synchronized boolean startBluetoothSco(int scoAudioMode,
                 @NonNull String eventSource) {
         AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource));
         return requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode);
     }
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
     /*package*/ synchronized boolean stopBluetoothSco(@NonNull String eventSource) {
         AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource));
         return requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, SCO_MODE_VIRTUAL_CALL);
@@ -563,8 +558,7 @@
         mScoConnectionState = state;
     }
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
     /*package*/ synchronized void resetBluetoothSco() {
         mScoAudioState = SCO_STATE_INACTIVE;
         broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
@@ -573,8 +567,7 @@
         mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco");
     }
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
     /*package*/ synchronized void onBtProfileDisconnected(int profile) {
         AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                 "BT profile " + BluetoothProfile.getProfileName(profile)
@@ -638,8 +631,7 @@
 
     MyLeAudioCallback mLeAudioCallback = null;
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
     /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) {
         AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                 "BT profile " + BluetoothProfile.getProfileName(profile) + " connected to proxy "
@@ -776,8 +768,7 @@
         }
     }
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
     private void onHeadsetProfileConnected(@NonNull BluetoothHeadset headset) {
         // Discard timeout message
         mDeviceBroker.handleCancelFailureToConnectToBtHeadsetService();
@@ -920,8 +911,7 @@
         return btDevice == null ? "(null)" : btDevice.getAnonymizedAddress();
     }
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
+    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
     /*package */ synchronized void onSetBtScoActiveDevice(BluetoothDevice btDevice) {
         Log.i(TAG, "onSetBtScoActiveDevice: " + getAnonymizedAddress(mBluetoothHeadsetDevice)
                 + " -> " + getAnonymizedAddress(btDevice));
@@ -1122,6 +1112,9 @@
 
     //-----------------------------------------------------
     // Utilities
+
+    // suppress warning due to generic Intent passed as param
+    @SuppressWarnings("AndroidFrameworkRequiresPermission")
     private void sendStickyBroadcastToAll(Intent intent) {
         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         final long ident = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 5c74304..ded93e6 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -643,9 +643,9 @@
             if (index > safeIndex) {
                 streamState.setIndex(safeIndex, deviceType, caller,
                         true /*hasModifyAudioSettings*/);
-                mAudioHandler.sendMessageAtTime(
+                mAudioHandler.sendMessage(
                         mAudioHandler.obtainMessage(MSG_SET_DEVICE_VOLUME, deviceType,
-                                /*arg2=*/0, streamState), /*delay=*/0);
+                                /*arg2=*/0, streamState));
             }
         }
     }
@@ -686,8 +686,11 @@
     /*package*/ void disableSafeMediaVolume(String callingPackage) {
         synchronized (mSafeMediaVolumeStateLock) {
             final long identity = Binder.clearCallingIdentity();
-            setSafeMediaVolumeEnabled(false, callingPackage);
-            Binder.restoreCallingIdentity(identity);
+            try {
+                setSafeMediaVolumeEnabled(false, callingPackage);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
 
             if (mPendingVolumeCommand != null) {
                 mAudioService.onSetStreamVolume(mPendingVolumeCommand.mStreamType,
@@ -701,6 +704,7 @@
         }
     }
 
+    @SuppressWarnings("AndroidFrameworkRequiresPermission")
     /*package*/ void scheduleMusicActiveCheck() {
         synchronized (mSafeMediaVolumeStateLock) {
             cancelMusicActiveCheck();
@@ -1035,10 +1039,9 @@
             mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
         }
 
-        mAudioHandler.sendMessageAtTime(
+        mAudioHandler.sendMessage(
                 mAudioHandler.obtainMessage(MSG_PERSIST_SAFE_VOLUME_STATE,
-                        persistedState, /*arg2=*/0,
-                        /*obj=*/null), /*delay=*/0);
+                        persistedState, /*arg2=*/0, /*obj=*/null));
     }
 
     private void updateCsdEnabled(String caller) {
@@ -1199,8 +1202,8 @@
 
         sanitizeDoseRecords_l();
 
-        mAudioHandler.sendMessageAtTime(mAudioHandler.obtainMessage(MSG_PERSIST_CSD_VALUES,
-                /* arg1= */0, /* arg2= */0, /* obj= */null), /* delay= */0);
+        mAudioHandler.sendMessage(mAudioHandler.obtainMessage(MSG_PERSIST_CSD_VALUES,
+                /* arg1= */0, /* arg2= */0, /* obj= */null));
 
         mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration));
     }
@@ -1316,6 +1319,7 @@
     }
 
     /** Called when handling MSG_LOWER_VOLUME_TO_RS1 */
+    @SuppressWarnings("AndroidFrameworkRequiresPermission")
     private void onLowerVolumeToRs1() {
         final ArrayList<AudioDeviceAttributes> devices = mAudioService.getDevicesForAttributesInt(
                 new AudioAttributes.Builder().setUsage(
@@ -1360,9 +1364,9 @@
 
         @Override
         public String toString() {
-            return new StringBuilder().append("{streamType=").append(mStreamType).append(",index=")
-                    .append(mIndex).append(",flags=").append(mFlags).append(",device=")
-                    .append(mDevice).append('}').toString();
+            return "{streamType=" + mStreamType
+                    + ",index=" + mIndex + ",flags=" + mFlags
+                    + ",device=" + mDevice + "}";
         }
     }
 }
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index cae1695..9265ff2 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -568,7 +568,8 @@
             updatedDevice = new AdiDeviceState(canonicalDeviceType, ada.getInternalType(),
                     ada.getAddress());
             initSAState(updatedDevice);
-            mDeviceBroker.addOrUpdateDeviceSAStateInInventory(updatedDevice);
+            mDeviceBroker.addOrUpdateDeviceSAStateInInventory(
+                    updatedDevice, true /*syncInventory*/);
         }
         if (updatedDevice != null) {
             onRoutingUpdated();
@@ -723,7 +724,7 @@
                     new AdiDeviceState(canonicalDeviceType, ada.getInternalType(),
                             ada.getAddress());
             initSAState(deviceState);
-            mDeviceBroker.addOrUpdateDeviceSAStateInInventory(deviceState);
+            mDeviceBroker.addOrUpdateDeviceSAStateInInventory(deviceState, true /*syncInventory*/);
             mDeviceBroker.postPersistAudioDeviceSettings();
             logDeviceState(deviceState, "addWirelessDeviceIfNew"); // may be updated later.
         }
diff --git a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
new file mode 100644
index 0000000..4a66bac
--- /dev/null
+++ b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "postsubmit": [
+    {
+      "name": "FrameworksMockingServicesTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.RescuePartyTest"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index e686779..43c1f24f 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -598,10 +598,11 @@
         FoldSettingProvider foldSettingProvider = new FoldSettingProvider(context,
                 new SettingsWrapper(),
                 new FoldLockSettingAvailabilityProvider(context.getResources()));
+        Looper displayThreadLooper = DisplayThread.get().getLooper();
         mInjector = injector;
         mContext = context;
         mFlags = injector.getFlags();
-        mHandler = new DisplayManagerHandler(DisplayThread.get().getLooper());
+        mHandler = new DisplayManagerHandler(displayThreadLooper);
         mUiHandler = UiThread.getHandler();
         mDisplayDeviceRepo = new DisplayDeviceRepository(mSyncRoot, mPersistentDataStore);
         mLogicalDisplayMapper = new LogicalDisplayMapper(mContext,
@@ -609,7 +610,7 @@
                 mDisplayDeviceRepo, new LogicalDisplayListener(), mSyncRoot, mHandler, mFlags);
         mDisplayModeDirector = new DisplayModeDirector(
                 context, mHandler, mFlags, mDisplayDeviceConfigProvider);
-        mBrightnessSynchronizer = new BrightnessSynchronizer(mContext,
+        mBrightnessSynchronizer = new BrightnessSynchronizer(mContext, displayThreadLooper,
                 mFlags.isBrightnessIntRangeUserPerceptionEnabled());
         Resources resources = mContext.getResources();
         mDefaultDisplayDefaultColorMode = mContext.getResources().getInteger(
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index a089331..0c46c5b 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -3933,7 +3933,7 @@
             }
             final var statsToken = createStatsTokenForFocusedClient(isShow, imeVisRes.getReason());
             mVisibilityApplier.applyImeVisibility(mImeBindingState.mFocusedWindow, statsToken,
-                    imeVisRes.getState(), imeVisRes.getReason());
+                    imeVisRes.getState(), imeVisRes.getReason(), bindingController.mUserId);
             if (imeVisRes.getReason() == SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW) {
                 // If focused display changed, we should unbind current method
                 // to make app window in previous display relayout after Ime
@@ -4821,7 +4821,7 @@
             @NonNull ImeVisibilityResult result) {
         synchronized (ImfLock.class) {
             mVisibilityApplier.applyImeVisibility(windowToken, statsToken, result.getState(),
-                    result.getReason());
+                    result.getReason(), mCurrentUserId);
         }
     }
 
@@ -5105,7 +5105,8 @@
                 if (imeVisRes != null) {
                     // Pass in a null statsToken as the IME snapshot is not tracked by ImeTracker.
                     mVisibilityApplier.applyImeVisibility(mImeBindingState.mFocusedWindow,
-                            null /* statsToken */, imeVisRes.getState(), imeVisRes.getReason());
+                            null /* statsToken */, imeVisRes.getState(), imeVisRes.getReason(),
+                            mCurrentUserId);
                 }
                 // Eligible IME processes use new "setInteractive" protocol.
                 mCurClient.mClient.setInteractive(mIsInteractive, mInFullscreenMode);
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 7a722bc..381b667 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -81,7 +81,6 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.PriorityQueue;
-import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -162,8 +161,8 @@
             new PriorityQueue<>(
                     Comparator.comparingLong(ReliableMessageRecord::getTimestamp));
 
-    // The test mode manager that manages behaviors during test mode.
-    private final TestModeManager mTestModeManager = new TestModeManager();
+    // The test mode manager that manages behaviors during test mode
+    private final ContextHubTestModeManager mTestModeManager = new ContextHubTestModeManager();
 
     // The period of the recurring time
     private static final int PERIOD_METRIC_QUERY_DAYS = 1;
@@ -229,9 +228,11 @@
             if (Flags.reliableMessageImplementation()
                     && Flags.reliableMessageTestModeBehavior()
                     && mIsTestModeEnabled.get()
-                    && mTestModeManager.handleNanoappMessage(mContextHubId, hostEndpointId,
-                            message, nanoappPermissions, messagePermissions)) {
-                // The TestModeManager handled the nanoapp message, so return here.
+                    && mTestModeManager.handleNanoappMessage(() -> {
+                        handleClientMessageCallback(mContextHubId, hostEndpointId, message,
+                                nanoappPermissions, messagePermissions);
+                    }, message)) {
+                // The ContextHubTestModeManager handled the nanoapp message, so return here.
                 return;
             }
 
@@ -261,8 +262,6 @@
      * Records a reliable message from a nanoapp for duplicate detection.
      */
     private static class ReliableMessageRecord {
-        public static final int TIMEOUT_NS = 1000000000;
-
         public int mContextHubId;
         public long mTimestamp;
         public int mMessageSequenceNumber;
@@ -297,56 +296,8 @@
         }
 
         public boolean isExpired() {
-            return mTimestamp + TIMEOUT_NS < SystemClock.elapsedRealtimeNanos();
-        }
-    }
-
-    /**
-     * A class to manage behaviors during test mode. This is used for testing.
-     */
-    private class TestModeManager {
-        /**
-         * Probability (in percent) of duplicating a message.
-         */
-        private static final int MESSAGE_DUPLICATION_PROBABILITY_PERCENT = 50;
-
-        /**
-         * The number of total messages to send when the duplicate event happens.
-         */
-        private static final int NUM_MESSAGES_TO_DUPLICATE = 3;
-
-        /**
-         * A probability percent for a certain event.
-         */
-        private static final int MAX_PROBABILITY_PERCENT = 100;
-
-        private final Random mRandom = new Random();
-
-        /**
-         * @return whether the message was handled
-         * @see ContextHubServiceCallback#handleNanoappMessage
-         */
-        public boolean handleNanoappMessage(int contextHubId,
-                short hostEndpointId, NanoAppMessage message,
-                List<String> nanoappPermissions, List<String> messagePermissions) {
-            if (!message.isReliable()) {
-                return false;
-            }
-
-            if (Flags.reliableMessageDuplicateDetectionService()
-                    && mRandom.nextInt(MAX_PROBABILITY_PERCENT)
-                    < MESSAGE_DUPLICATION_PROBABILITY_PERCENT) {
-                Log.i(TAG, "[TEST MODE] Duplicating message ("
-                        + NUM_MESSAGES_TO_DUPLICATE
-                        + " sends) with message sequence number: "
-                        + message.getMessageSequenceNumber());
-                for (int i = 0; i < NUM_MESSAGES_TO_DUPLICATE; ++i) {
-                    handleClientMessageCallback(contextHubId, hostEndpointId,
-                            message, nanoappPermissions, messagePermissions);
-                }
-                return true;
-            }
-            return false;
+            return mTimestamp + ContextHubTransactionManager.RELIABLE_MESSAGE_TIMEOUT.toNanos()
+                    < SystemClock.elapsedRealtimeNanos();
         }
     }
 
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java b/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java
index 6da7a65..2ec9bdb 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java
@@ -27,53 +27,65 @@
  *
  * @hide
  */
-/* package */ abstract class ContextHubServiceTransaction {
+abstract class ContextHubServiceTransaction {
     private final int mTransactionId;
+
     @ContextHubTransaction.Type
     private final int mTransactionType;
 
-    /** The ID of the nanoapp this transaction is targeted for, null if not applicable. */
     private final Long mNanoAppId;
 
-    /**
-     * The host package associated with this transaction.
-     */
     private final String mPackage;
 
-    /**
-     * The message sequence number associated with this transaction, null if not applicable.
-     */
     private final Integer mMessageSequenceNumber;
 
-    /**
-     * true if the transaction has already completed, false otherwise
-     */
+    private long mNextRetryTime;
+
+    private long mTimeoutTime;
+
+    /** The number of times the transaction has been started (start function called). */
+    private int mNumCompletedStartCalls;
+
+    private final short mHostEndpointId;
+
     private boolean mIsComplete = false;
 
-    /* package */ ContextHubServiceTransaction(int id, int type, String packageName) {
+    ContextHubServiceTransaction(int id, int type, String packageName) {
         mTransactionId = id;
         mTransactionType = type;
         mNanoAppId = null;
         mPackage = packageName;
         mMessageSequenceNumber = null;
+        mNextRetryTime = Long.MAX_VALUE;
+        mTimeoutTime = Long.MAX_VALUE;
+        mNumCompletedStartCalls = 0;
+        mHostEndpointId = Short.MAX_VALUE;
     }
 
-    /* package */ ContextHubServiceTransaction(int id, int type, long nanoAppId,
+    ContextHubServiceTransaction(int id, int type, long nanoAppId,
             String packageName) {
         mTransactionId = id;
         mTransactionType = type;
         mNanoAppId = nanoAppId;
         mPackage = packageName;
         mMessageSequenceNumber = null;
+        mNextRetryTime = Long.MAX_VALUE;
+        mTimeoutTime = Long.MAX_VALUE;
+        mNumCompletedStartCalls = 0;
+        mHostEndpointId = Short.MAX_VALUE;
     }
 
-    /* package */ ContextHubServiceTransaction(int id, int type, String packageName,
-            int messageSequenceNumber) {
+    ContextHubServiceTransaction(int id, int type, String packageName,
+            int messageSequenceNumber, short hostEndpointId) {
         mTransactionId = id;
         mTransactionType = type;
         mNanoAppId = null;
         mPackage = packageName;
         mMessageSequenceNumber = messageSequenceNumber;
+        mNextRetryTime = Long.MAX_VALUE;
+        mTimeoutTime = Long.MAX_VALUE;
+        mNumCompletedStartCalls = 0;
+        mHostEndpointId = hostEndpointId;
     }
 
     /**
@@ -95,7 +107,7 @@
      *
      * @param result the result of the transaction
      */
-    /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+    void onTransactionComplete(@ContextHubTransaction.Result int result) {
     }
 
     /**
@@ -106,44 +118,51 @@
      * @param result           the result of the query
      * @param nanoAppStateList the list of nanoapps given by the query response
      */
-    /* package */ void onQueryResponse(
+    void onQueryResponse(
             @ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) {
     }
 
-    /**
-     * @return the ID of this transaction
-     */
-    /* package */ int getTransactionId() {
+    int getTransactionId() {
         return mTransactionId;
     }
 
-    /**
-     * @return the type of this transaction
-     * @see ContextHubTransaction.Type
-     */
     @ContextHubTransaction.Type
-    /* package */ int getTransactionType() {
+    int getTransactionType() {
         return mTransactionType;
     }
 
-    /**
-     * @return the message sequence number of this transaction
-     */
     Integer getMessageSequenceNumber() {
         return mMessageSequenceNumber;
     }
 
+    long getNextRetryTime() {
+        return mNextRetryTime;
+    }
+
+    long getTimeoutTime() {
+        return mTimeoutTime;
+    }
+
+    int getNumCompletedStartCalls() {
+        return mNumCompletedStartCalls;
+    }
+
+    short getHostEndpointId() {
+        return mHostEndpointId;
+    }
+
     /**
      * Gets the timeout period as defined in IContexthub.hal
      *
      * @return the timeout of this transaction in the specified time unit
      */
-    /* package */ long getTimeout(TimeUnit unit) {
+    long getTimeout(TimeUnit unit) {
         switch (mTransactionType) {
             case ContextHubTransaction.TYPE_LOAD_NANOAPP:
                 return unit.convert(30L, TimeUnit.SECONDS);
             case ContextHubTransaction.TYPE_RELIABLE_MESSAGE:
-                return unit.convert(1000L, TimeUnit.MILLISECONDS);
+                return unit.convert(ContextHubTransactionManager.RELIABLE_MESSAGE_TIMEOUT.toNanos(),
+                        TimeUnit.NANOSECONDS);
             case ContextHubTransaction.TYPE_UNLOAD_NANOAPP:
             case ContextHubTransaction.TYPE_ENABLE_NANOAPP:
             case ContextHubTransaction.TYPE_DISABLE_NANOAPP:
@@ -159,14 +178,23 @@
      *
      * Should only be called as a result of a response from a Context Hub callback
      */
-    /* package */ void setComplete() {
+    void setComplete() {
         mIsComplete = true;
     }
 
-    /**
-     * @return true if the transaction has already completed, false otherwise
-     */
-    /* package */ boolean isComplete() {
+    void setNextRetryTime(long nextRetryTime) {
+        mNextRetryTime = nextRetryTime;
+    }
+
+    void setTimeoutTime(long timeoutTime) {
+        mTimeoutTime = timeoutTime;
+    }
+
+    void setNumCompletedStartCalls(int numCompletedStartCalls) {
+        mNumCompletedStartCalls = numCompletedStartCalls;
+    }
+
+    boolean isComplete() {
         return mIsComplete;
     }
 
@@ -187,7 +215,18 @@
             out.append(", messageSequenceNumber = ");
             out.append(mMessageSequenceNumber);
         }
+        if (mTransactionType == ContextHubTransaction.TYPE_RELIABLE_MESSAGE) {
+            out.append(", nextRetryTime = ");
+            out.append(mNextRetryTime);
+            out.append(", timeoutTime = ");
+            out.append(mTimeoutTime);
+            out.append(", numCompletedStartCalls = ");
+            out.append(mNumCompletedStartCalls);
+            out.append(", hostEndpointId = ");
+            out.append(mHostEndpointId);
+        }
         out.append(")");
+
         return out.toString();
     }
 }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java
new file mode 100644
index 0000000..e50324e
--- /dev/null
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java
@@ -0,0 +1,82 @@
+/*
+ * 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.location.contexthub;
+
+import android.chre.flags.Flags;
+import android.hardware.location.NanoAppMessage;
+import android.util.Log;
+
+import java.util.Random;
+
+/**
+ * A class to manage behaviors during test mode. This is used for testing.
+ * @hide
+ */
+public class ContextHubTestModeManager {
+    private static final String TAG = "ContextHubTestModeManager";
+
+    /** Probability (in percent) of duplicating a message. */
+    private static final int MESSAGE_DROP_PROBABILITY_PERCENT = 20;
+
+    /** Probability (in percent) of duplicating a message. */
+    private static final int MESSAGE_DUPLICATION_PROBABILITY_PERCENT = 20;
+
+    /** The number of total messages to send when the duplicate event happens. */
+    private static final int NUM_MESSAGES_TO_DUPLICATE = 3;
+
+    /** A probability percent for a certain event. */
+    private static final int MAX_PROBABILITY_PERCENT = 100;
+
+    private final Random mRandom = new Random();
+
+    /**
+     * @return whether the message was handled
+     * @see ContextHubServiceCallback#handleNanoappMessage
+     */
+    public boolean handleNanoappMessage(Runnable handleMessage, NanoAppMessage message) {
+        if (Flags.reliableMessageDuplicateDetectionService()
+                && message.isReliable()
+                && mRandom.nextInt(MAX_PROBABILITY_PERCENT)
+                        < MESSAGE_DUPLICATION_PROBABILITY_PERCENT) {
+            Log.i(TAG, "[TEST MODE] Duplicating message ("
+                    + NUM_MESSAGES_TO_DUPLICATE
+                    + " sends) with message sequence number: "
+                    + message.getMessageSequenceNumber());
+            for (int i = 0; i < NUM_MESSAGES_TO_DUPLICATE; ++i) {
+                handleMessage.run();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @return whether the message was handled
+     * @see IContextHubWrapper#sendMessageToContextHub
+     */
+    public boolean sendMessageToContextHub(NanoAppMessage message) {
+        if (Flags.reliableMessageRetrySupportService()
+                && message.isReliable()
+                && mRandom.nextInt(MAX_PROBABILITY_PERCENT)
+                        < MESSAGE_DROP_PROBABILITY_PERCENT) {
+            Log.i(TAG, "[TEST MODE] Dropping message with message sequence number: "
+                    + message.getMessageSequenceNumber());
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index ec94e2b..3051379 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -16,19 +16,26 @@
 
 package com.android.server.location.contexthub;
 
+import android.chre.flags.Flags;
 import android.hardware.location.ContextHubTransaction;
 import android.hardware.location.IContextHubTransactionCallback;
 import android.hardware.location.NanoAppBinary;
 import android.hardware.location.NanoAppMessage;
 import android.hardware.location.NanoAppState;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.util.Log;
 
+import java.time.Duration;
 import java.util.ArrayDeque;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -47,34 +54,30 @@
 /* package */ class ContextHubTransactionManager {
     private static final String TAG = "ContextHubTransactionManager";
 
-    /*
-     * Maximum number of transaction requests that can be pending at a time
-     */
+    public static final Duration RELIABLE_MESSAGE_TIMEOUT = Duration.ofSeconds(1);
+
     private static final int MAX_PENDING_REQUESTS = 10000;
 
-    /*
-     * The proxy to talk to the Context Hub
-     */
+    private static final int RELIABLE_MESSAGE_MAX_NUM_RETRY = 3;
+
+    private static final Duration RELIABLE_MESSAGE_RETRY_WAIT_TIME = Duration.ofMillis(250);
+
+    private static final Duration RELIABLE_MESSAGE_MIN_WAIT_TIME = Duration.ofNanos(1000);
+
     private final IContextHubWrapper mContextHubProxy;
 
-    /*
-     * The manager for all clients for the service.
-     */
     private final ContextHubClientManager mClientManager;
 
-    /*
-     * The nanoapp state manager for the service
-     */
     private final NanoAppStateManager mNanoAppStateManager;
 
-    /*
-     * A queue containing the current transactions
-     */
     private final ArrayDeque<ContextHubServiceTransaction> mTransactionQueue = new ArrayDeque<>();
 
-    /*
-     * The next available transaction ID
-     */
+    private final Map<Integer, ContextHubServiceTransaction> mReliableMessageTransactionMap =
+            new HashMap<>();
+
+    /** A set of host endpoint IDs that have an active pending transaction. */
+    private final Set<Short> mReliableMessageHostEndpointIdActiveSet = new HashSet<>();
+
     private final AtomicInteger mNextAvailableId = new AtomicInteger();
 
     /**
@@ -86,10 +89,12 @@
             new AtomicInteger(new Random().nextInt(Integer.MAX_VALUE / 2));
 
     /*
-     * An executor and the future object for scheduling timeout timers
+     * An executor and the future object for scheduling timeout timers and
+     * for scheduling the processing of reliable message transactions.
      */
-    private final ScheduledThreadPoolExecutor mTimeoutExecutor = new ScheduledThreadPoolExecutor(1);
+    private final ScheduledThreadPoolExecutor mExecutor = new ScheduledThreadPoolExecutor(1);
     private ScheduledFuture<?> mTimeoutFuture = null;
+    private ScheduledFuture<?> mReliableMessageTransactionFuture = null;
 
     /*
      * The list of previous transaction records.
@@ -333,7 +338,7 @@
             IContextHubTransactionCallback transactionCallback, String packageName) {
         return new ContextHubServiceTransaction(mNextAvailableId.getAndIncrement(),
                 ContextHubTransaction.TYPE_RELIABLE_MESSAGE, packageName,
-                mNextAvailableMessageSequenceNumber.getAndIncrement()) {
+                mNextAvailableMessageSequenceNumber.getAndIncrement(), hostEndpointId) {
             @Override
             /* package */ int onTransact() {
                 try {
@@ -416,16 +421,23 @@
             return;
         }
 
-        if (mTransactionQueue.size() == MAX_PENDING_REQUESTS) {
+        if (mTransactionQueue.size() >= MAX_PENDING_REQUESTS
+                || mReliableMessageTransactionMap.size() >= MAX_PENDING_REQUESTS) {
             throw new IllegalStateException("Transaction queue is full (capacity = "
                     + MAX_PENDING_REQUESTS + ")");
         }
 
-        mTransactionQueue.add(transaction);
         mTransactionRecordDeque.add(new TransactionRecord(transaction.toString()));
-
-        if (mTransactionQueue.size() == 1) {
-            startNextTransaction();
+        if (Flags.reliableMessageRetrySupportService()
+                && transaction.getTransactionType()
+                        == ContextHubTransaction.TYPE_RELIABLE_MESSAGE) {
+            mReliableMessageTransactionMap.put(transaction.getMessageSequenceNumber(), transaction);
+            mExecutor.execute(() -> processMessageTransactions());
+        } else {
+            mTransactionQueue.add(transaction);
+            if (mTransactionQueue.size() == 1) {
+                startNextTransaction();
+            }
         }
     }
 
@@ -455,26 +467,42 @@
 
     /* package */
     synchronized void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) {
-        ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+        if (!Flags.reliableMessageRetrySupportService()) {
+            ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+            if (transaction == null) {
+                Log.w(TAG, "Received unexpected transaction response (no transaction pending)");
+                return;
+            }
+
+            Integer transactionMessageSequenceNumber = transaction.getMessageSequenceNumber();
+            if (transaction.getTransactionType() != ContextHubTransaction.TYPE_RELIABLE_MESSAGE
+                    || transactionMessageSequenceNumber == null
+                    || transactionMessageSequenceNumber != messageSequenceNumber) {
+                Log.w(TAG, "Received unexpected message transaction response (expected message "
+                        + "sequence number = "
+                        + transaction.getMessageSequenceNumber()
+                        + ", received messageSequenceNumber = " + messageSequenceNumber + ")");
+                return;
+            }
+
+            transaction.onTransactionComplete(success ? ContextHubTransaction.RESULT_SUCCESS :
+                            ContextHubTransaction.RESULT_FAILED_AT_HUB);
+            removeTransactionAndStartNext();
+            return;
+        }
+
+        ContextHubServiceTransaction transaction =
+                mReliableMessageTransactionMap.get(messageSequenceNumber);
         if (transaction == null) {
-            Log.w(TAG, "Received unexpected transaction response (no transaction pending)");
+            Log.w(TAG, "Could not find reliable message transaction with message sequence number"
+                    + messageSequenceNumber);
             return;
         }
 
-        Integer transactionMessageSequenceNumber = transaction.getMessageSequenceNumber();
-        if (transaction.getTransactionType() != ContextHubTransaction.TYPE_RELIABLE_MESSAGE
-                || transactionMessageSequenceNumber == null
-                || transactionMessageSequenceNumber != messageSequenceNumber) {
-            Log.w(TAG, "Received unexpected message transaction response (expected message "
-                    + "sequence number = "
-                    + transaction.getMessageSequenceNumber()
-                    + ", received messageSequenceNumber = " + messageSequenceNumber + ")");
-            return;
-        }
-
-        transaction.onTransactionComplete(success ? ContextHubTransaction.RESULT_SUCCESS :
-                        ContextHubTransaction.RESULT_FAILED_AT_HUB);
-        removeTransactionAndStartNext();
+        completeMessageTransaction(transaction,
+                success ? ContextHubTransaction.RESULT_SUCCESS
+                        : ContextHubTransaction.RESULT_FAILED_AT_HUB);
+        mExecutor.execute(() -> processMessageTransactions());
     }
 
     /**
@@ -503,6 +531,15 @@
      */
     /* package */
     synchronized void onHubReset() {
+        if (Flags.reliableMessageRetrySupportService()) {
+            Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter =
+                    mReliableMessageTransactionMap.entrySet().iterator();
+            while (iter.hasNext()) {
+                completeMessageTransaction(iter.next().getValue(),
+                        ContextHubTransaction.RESULT_FAILED_AT_HUB, iter);
+            }
+        }
+
         ContextHubServiceTransaction transaction = mTransactionQueue.peek();
         if (transaction == null) {
             return;
@@ -566,7 +603,7 @@
 
                 long timeoutMs = transaction.getTimeout(TimeUnit.MILLISECONDS);
                 try {
-                    mTimeoutFuture = mTimeoutExecutor.schedule(
+                    mTimeoutFuture = mExecutor.schedule(
                             onTimeoutFunc, timeoutMs, TimeUnit.MILLISECONDS);
                 } catch (Exception e) {
                     Log.e(TAG, "Error when schedule a timer", e);
@@ -579,6 +616,136 @@
         }
     }
 
+    /**
+     * Processes message transactions, starting and completing them as needed.
+     * This function is called when adding a message transaction or when a timer
+     * expires for an existing message transaction's retry or timeout. The
+     * internal processing loop will iterate at most twice as if one iteration
+     * completes a transaction, the next iteration can only start new transactions.
+     * If the first iteration does not complete any transaction, the loop will
+     * only iterate once.
+     */
+    private synchronized void processMessageTransactions() {
+        if (!Flags.reliableMessageRetrySupportService()) {
+            return;
+        }
+
+        if (mReliableMessageTransactionFuture != null) {
+            mReliableMessageTransactionFuture.cancel(/* mayInterruptIfRunning= */ false);
+            mReliableMessageTransactionFuture = null;
+        }
+
+        long now = SystemClock.elapsedRealtimeNanos();
+        long nextExecutionTime = Long.MAX_VALUE;
+        boolean continueProcessing;
+        do {
+            continueProcessing = false;
+            Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter =
+                    mReliableMessageTransactionMap.entrySet().iterator();
+            while (iter.hasNext()) {
+                ContextHubServiceTransaction transaction = iter.next().getValue();
+                short hostEndpointId = transaction.getHostEndpointId();
+                int numCompletedStartCalls = transaction.getNumCompletedStartCalls();
+                if (numCompletedStartCalls == 0
+                        && mReliableMessageHostEndpointIdActiveSet.contains(hostEndpointId)) {
+                    continue;
+                }
+
+                long nextRetryTime = transaction.getNextRetryTime();
+                long timeoutTime = transaction.getTimeoutTime();
+                boolean transactionTimedOut = timeoutTime <= now;
+                boolean transactionHitMaxRetries = nextRetryTime <= now
+                        && numCompletedStartCalls > RELIABLE_MESSAGE_MAX_NUM_RETRY;
+                if (transactionTimedOut || transactionHitMaxRetries) {
+                    completeMessageTransaction(transaction,
+                            ContextHubTransaction.RESULT_FAILED_TIMEOUT, iter);
+                    continueProcessing = true;
+                } else {
+                    if (nextRetryTime <= now || numCompletedStartCalls <= 0) {
+                        startMessageTransaction(transaction, now);
+                    }
+
+                    nextExecutionTime = Math.min(nextExecutionTime,
+                            transaction.getNextRetryTime());
+                    nextExecutionTime = Math.min(nextExecutionTime,
+                            transaction.getTimeoutTime());
+                }
+            }
+        } while (continueProcessing);
+
+        if (nextExecutionTime < Long.MAX_VALUE) {
+            mReliableMessageTransactionFuture = mExecutor.schedule(
+                    () -> processMessageTransactions(),
+                    Math.max(nextExecutionTime - SystemClock.elapsedRealtimeNanos(),
+                            RELIABLE_MESSAGE_MIN_WAIT_TIME.toNanos()),
+                    TimeUnit.NANOSECONDS);
+        }
+    }
+
+    /**
+     * Completes a message transaction and removes it from the reliable message map.
+     *
+     * @param transaction The transaction to complete.
+     * @param result The result code.
+     */
+    private void completeMessageTransaction(ContextHubServiceTransaction transaction,
+            @ContextHubTransaction.Result int result) {
+        completeMessageTransaction(transaction, result, /* iter= */ null);
+    }
+
+    /**
+     * Completes a message transaction and removes it from the reliable message map using iter.
+     *
+     * @param transaction The transaction to complete.
+     * @param result The result code.
+     * @param iter The iterator for the reliable message map - used to remove the message directly.
+     */
+    private void completeMessageTransaction(ContextHubServiceTransaction transaction,
+            @ContextHubTransaction.Result int result,
+            Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter) {
+        transaction.onTransactionComplete(result);
+
+        if (iter == null) {
+            mReliableMessageTransactionMap.remove(transaction.getMessageSequenceNumber());
+        } else {
+            iter.remove();
+        }
+        mReliableMessageHostEndpointIdActiveSet.remove(transaction.getHostEndpointId());
+
+        Log.d(TAG, "Successfully completed reliable message transaction with "
+                + "message sequence number: " + transaction.getMessageSequenceNumber()
+                + " and result: " + result);
+    }
+
+    /**
+     * Starts a message transaction.
+     *
+     * @param transaction The transaction to start.
+     * @param now The now time.
+     */
+    private void startMessageTransaction(ContextHubServiceTransaction transaction, long now) {
+        int numCompletedStartCalls = transaction.getNumCompletedStartCalls();
+        @ContextHubTransaction.Result int result = transaction.onTransact();
+        if (result == ContextHubTransaction.RESULT_SUCCESS) {
+            Log.d(TAG, "Successfully "
+                    + (numCompletedStartCalls == 0 ? "started" : "retried")
+                    + " reliable message transaction with message sequence number: "
+                    + transaction.getMessageSequenceNumber());
+        } else {
+            Log.w(TAG, "Could not start reliable message transaction with "
+                    + "message sequence number: "
+                    + transaction.getMessageSequenceNumber()
+                    + ", result: " + result);
+        }
+
+        transaction.setNextRetryTime(now + RELIABLE_MESSAGE_RETRY_WAIT_TIME.toNanos());
+        if (transaction.getTimeoutTime() == Long.MAX_VALUE) { // first time starting transaction
+            transaction.setTimeoutTime(now + RELIABLE_MESSAGE_TIMEOUT.toNanos());
+        }
+        transaction.setNumCompletedStartCalls(numCompletedStartCalls + 1);
+        mReliableMessageHostEndpointIdActiveSet.add(transaction.getHostEndpointId());
+    }
+
     private int toStatsTransactionResult(@ContextHubTransaction.Result int result) {
         switch (result) {
             case ContextHubTransaction.RESULT_SUCCESS:
@@ -605,19 +772,34 @@
 
     @Override
     public String toString() {
-        StringBuilder sb = new StringBuilder(100);
-        ContextHubServiceTransaction[] arr;
+        StringBuilder sb = new StringBuilder();
+        int i = 0;
         synchronized (this) {
-            arr = mTransactionQueue.toArray(new ContextHubServiceTransaction[0]);
-        }
-        for (int i = 0; i < arr.length; i++) {
-            sb.append(i + ": " + arr[i] + "\n");
-        }
+            for (ContextHubServiceTransaction transaction: mTransactionQueue) {
+                sb.append(i);
+                sb.append(": ");
+                sb.append(transaction.toString());
+                sb.append("\n");
+                ++i;
+            }
 
-        sb.append("Transaction History:\n");
-        Iterator<TransactionRecord> iterator = mTransactionRecordDeque.descendingIterator();
-        while (iterator.hasNext()) {
-            sb.append(iterator.next() + "\n");
+            if (Flags.reliableMessageRetrySupportService()) {
+                for (ContextHubServiceTransaction transaction:
+                        mReliableMessageTransactionMap.values()) {
+                    sb.append(i);
+                    sb.append(": ");
+                    sb.append(transaction.toString());
+                    sb.append("\n");
+                    ++i;
+                }
+            }
+
+            sb.append("Transaction History:\n");
+            Iterator<TransactionRecord> iterator = mTransactionRecordDeque.descendingIterator();
+            while (iterator.hasNext()) {
+                sb.append(iterator.next());
+                sb.append("\n");
+            }
         }
         return sb.toString();
     }
diff --git a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
index 552809b..4fc3d87 100644
--- a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
+++ b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
@@ -52,6 +52,7 @@
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * @hide
@@ -432,10 +433,16 @@
 
         // Use this thread in case where the execution requires to be on a service thread.
         // For instance, AppOpsManager.noteOp requires the UPDATE_APP_OPS_STATS permission.
-        private HandlerThread mHandlerThread =
+        private final HandlerThread mHandlerThread =
                 new HandlerThread("Context Hub AIDL callback", Process.THREAD_PRIORITY_BACKGROUND);
         private Handler mHandler;
 
+        // True if test mode is enabled for the Context Hub
+        private final AtomicBoolean mIsTestModeEnabled = new AtomicBoolean(false);
+
+        // The test mode manager that manages behaviors during test mode
+        private final ContextHubTestModeManager mTestModeManager = new ContextHubTestModeManager();
+
         private class ContextHubAidlCallback extends
                 android.hardware.contexthub.IContextHubCallback.Stub {
             private final int mContextHubId;
@@ -549,6 +556,8 @@
             } else {
                 Log.e(TAG, "mHandleServiceRestartCallback is not set");
             }
+
+            mIsTestModeEnabled.set(false);
         }
 
         public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException {
@@ -659,7 +668,17 @@
             try {
                 var msg = ContextHubServiceUtil.createAidlContextHubMessage(
                         hostEndpointId, message);
-                hub.sendMessageToHub(contextHubId, msg);
+
+                // Only process the message normally if not using test mode manager or if
+                // the test mode manager call returned false as this indicates it did not
+                // process the message.
+                boolean useTestModeManager = Flags.reliableMessageImplementation()
+                        && Flags.reliableMessageTestModeBehavior()
+                        && mIsTestModeEnabled.get();
+                if (!useTestModeManager || !mTestModeManager.sendMessageToContextHub(message)) {
+                    hub.sendMessageToHub(contextHubId, msg);
+                }
+
                 return ContextHubTransaction.RESULT_SUCCESS;
             } catch (RemoteException | ServiceSpecificException e) {
                 return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
@@ -828,6 +847,7 @@
                 return false;
             }
 
+            mIsTestModeEnabled.set(enable);
             try {
                 hub.setTestMode(enable);
                 return true;
diff --git a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
index ec95298..58b14b1 100644
--- a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
+++ b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.UserInfo;
 import android.media.AudioFocusInfo;
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
@@ -183,8 +184,8 @@
             foregroundContext) throws RemoteException {
         final int userId = UserHandle.getUserId(afi.getClientUid());
         final int usage = afi.getAttributes().getUsage();
-        String userName = mUserManager.getUserInfo(userId).name;
-        if (userId != foregroundContext.getUserId()) {
+        UserInfo userInfo = mUserManager.getUserInfo(userId);
+        if (userInfo != null && userId != foregroundContext.getUserId()) {
             //TODO: b/349138482 - Add handling of cases when usage == USAGE_NOTIFICATION_RINGTONE
             if (usage == USAGE_ALARM) {
                 Intent muteIntent = createIntent(ACTION_MUTE_SOUND, afi, foregroundContext, userId);
@@ -199,7 +200,7 @@
 
                 mUserWithNotification = foregroundContext.getUserId();
                 mNotificationManager.notifyAsUser(LOG_TAG, afi.getClientUid(),
-                        createNotification(userName, mutePI, switchPI, foregroundContext),
+                        createNotification(userInfo.name, mutePI, switchPI, foregroundContext),
                         foregroundContext.getUser());
             }
         }
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 4bba649..549a97e 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
@@ -154,8 +154,11 @@
                 batteryUsageStatsBuilder.getAggregateBatteryConsumerBuilder(
                         BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
         if (descriptor.powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) {
-            deviceScope.addConsumedPowerForCustomComponent(descriptor.powerComponentId,
-                    totalPower[0]);
+            if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(
+                    descriptor.powerComponentId)) {
+                deviceScope.addConsumedPowerForCustomComponent(descriptor.powerComponentId,
+                        totalPower[0]);
+            }
         } else {
             deviceScope.addConsumedPower(descriptor.powerComponentId,
                     totalPower[0], BatteryConsumer.POWER_MODEL_UNDEFINED);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 72ec058..5215609 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -3796,7 +3796,7 @@
             hideBootMessagesLocked();
             // If the screen still doesn't come up after 30 seconds, give
             // up and turn it on.
-            mH.sendEmptyMessageDelayed(H.BOOT_TIMEOUT, 30 * 1000);
+            mH.sendEmptyMessageDelayed(H.BOOT_TIMEOUT, 30 * 1000 * Build.HW_TIMEOUT_MULTIPLIER);
         }
 
         mPolicy.systemBooted();
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java
index 0f3b0aa..636cbee 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java
@@ -37,6 +37,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.media.permission.INativePermissionController;
+import com.android.media.permission.PermissionEnum;
 import com.android.media.permission.UidPackageState;
 import com.android.server.pm.pkg.PackageState;
 
@@ -353,6 +354,56 @@
     }
 
     @Test
+    public void testSpecialHotwordPermissions() throws Exception {
+        BiPredicate<Integer, String> customPermPred = mock(BiPredicate.class);
+        var initPackageListData =
+                List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two);
+        // expected state
+        // PERM[CAPTURE_AUDIO_HOTWORD]: [10000]
+        // PERM[CAPTURE_AUDIO_OUTPUT]: [10001]
+        // PERM[RECORD_AUDIO]: [10001]
+        // PERM[...]: []
+        when(customPermPred.test(
+                        eq(10000), eq(MONITORED_PERMS[PermissionEnum.CAPTURE_AUDIO_HOTWORD])))
+                .thenReturn(true);
+        when(customPermPred.test(
+                        eq(10001), eq(MONITORED_PERMS[PermissionEnum.CAPTURE_AUDIO_OUTPUT])))
+                .thenReturn(true);
+        when(customPermPred.test(eq(10001), eq(MONITORED_PERMS[PermissionEnum.RECORD_AUDIO])))
+                .thenReturn(true);
+        mPermissionProvider =
+                new AudioServerPermissionProvider(
+                        initPackageListData, customPermPred, () -> new int[] {0});
+        int HDS_UID = 99001;
+        mPermissionProvider.onServiceStart(mMockPc);
+        clearInvocations(mMockPc);
+        mPermissionProvider.setIsolatedServiceUid(HDS_UID, 10000);
+        verify(mMockPc)
+                .populatePermissionState(
+                        eq((byte) PermissionEnum.CAPTURE_AUDIO_HOTWORD),
+                        aryEq(new int[] {10000, HDS_UID}));
+        verify(mMockPc)
+                .populatePermissionState(
+                        eq((byte) PermissionEnum.CAPTURE_AUDIO_OUTPUT),
+                        aryEq(new int[] {10001, HDS_UID}));
+        verify(mMockPc)
+                .populatePermissionState(
+                        eq((byte) PermissionEnum.RECORD_AUDIO), aryEq(new int[] {10001, HDS_UID}));
+
+        clearInvocations(mMockPc);
+        mPermissionProvider.clearIsolatedServiceUid(HDS_UID);
+        verify(mMockPc)
+                .populatePermissionState(
+                        eq((byte) PermissionEnum.CAPTURE_AUDIO_HOTWORD), aryEq(new int[] {10000}));
+        verify(mMockPc)
+                .populatePermissionState(
+                        eq((byte) PermissionEnum.CAPTURE_AUDIO_OUTPUT), aryEq(new int[] {10001}));
+        verify(mMockPc)
+                .populatePermissionState(
+                        eq((byte) PermissionEnum.RECORD_AUDIO), aryEq(new int[] {10001}));
+    }
+
+    @Test
     public void testPermissionsPopulated_onChange() throws Exception {
         var initPackageListData =
                 List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index cfcc04b..89b9850 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -1165,7 +1165,7 @@
                 LocalServices.getService(PermissionManagerServiceInternal.class)
                         .setHotwordDetectionServiceProvider(() -> uid);
                 mIdentity = new HotwordDetectionServiceIdentity(uid, mVoiceInteractionServiceUid);
-                addServiceUidForAudioPolicy(uid);
+                addServiceUidForAudioPolicy(uid, mVoiceInteractionServiceUid);
             }
         }));
     }
@@ -1187,23 +1187,17 @@
         });
     }
 
-    private void addServiceUidForAudioPolicy(int uid) {
-        mScheduledExecutorService.execute(() -> {
-            AudioManagerInternal audioManager =
-                    LocalServices.getService(AudioManagerInternal.class);
-            if (audioManager != null) {
-                audioManager.addAssistantServiceUid(uid);
-            }
-        });
+    private void addServiceUidForAudioPolicy(int isolatedUid, int owningUid) {
+        AudioManagerInternal audioManager = LocalServices.getService(AudioManagerInternal.class);
+        if (audioManager != null) {
+            audioManager.addAssistantServiceUid(isolatedUid, owningUid);
+        }
     }
 
     private void removeServiceUidForAudioPolicy(int uid) {
-        mScheduledExecutorService.execute(() -> {
-            AudioManagerInternal audioManager =
-                    LocalServices.getService(AudioManagerInternal.class);
-            if (audioManager != null) {
-                audioManager.removeAssistantServiceUid(uid);
-            }
-        });
+        AudioManagerInternal audioManager = LocalServices.getService(AudioManagerInternal.class);
+        if (audioManager != null) {
+            audioManager.removeAssistantServiceUid(uid);
+        }
     }
 }