Merge "Add the aconfig flag enable_chips for WebView feature CHIPS" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index e40c78c..97d28d1 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -1216,6 +1216,7 @@
 // DevicePolicy
 aconfig_declarations {
     name: "device_policy_aconfig_flags",
+    exportable: true,
     package: "android.app.admin.flags",
     container: "system",
     srcs: [
@@ -1234,6 +1235,7 @@
     aconfig_declarations: "device_policy_aconfig_flags",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
     min_sdk_version: "30",
+    mode: "exported",
     apex_available: [
         "//apex_available:platform",
         "com.android.permission",
diff --git a/Android.bp b/Android.bp
index 529da53..a1f6e30 100644
--- a/Android.bp
+++ b/Android.bp
@@ -530,6 +530,50 @@
         ],
     },
     jarjar_prefix: "com.android.internal.hidden_from_bootclasspath",
+
+    jarjar_shards: select(release_flag("RELEASE_USE_SHARDED_JARJAR_ON_FRAMEWORK_MINUS_APEX"), {
+        true: "10",
+        default: "1",
+    }),
+}
+
+// This is identical to "framework-minus-apex" but with "jarjar_shards" hardcodd.
+// (also "stem" is commented out to avoid a conflict with the "framework-minus-apex")
+// TODO(b/383559945) This module is just for local testing / verification. It's not used
+// by anything. Remove it once we roll out RELEASE_USE_SHARDED_JARJAR_ON_FRAMEWORK_MINUS_APEX.
+java_library {
+    name: "framework-minus-apex_jarjar-sharded",
+    defaults: [
+        "framework-minus-apex-with-libs-defaults",
+        "framework-non-updatable-lint-defaults",
+    ],
+    installable: true,
+    // For backwards compatibility.
+    // stem: "framework",
+    apex_available: ["//apex_available:platform"],
+    visibility: [
+        "//frameworks/base",
+        "//frameworks/base/location",
+        // TODO(b/147128803) remove the below lines
+        "//frameworks/base/apex/blobstore/framework",
+        "//frameworks/base/apex/jobscheduler/framework",
+        "//frameworks/base/packages/Tethering/tests/unit",
+        "//packages/modules/Connectivity/Tethering/tests/unit",
+    ],
+    errorprone: {
+        javacflags: [
+            "-Xep:AndroidFrameworkCompatChange:ERROR",
+            "-Xep:AndroidFrameworkUid:ERROR",
+        ],
+    },
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+        warning_checks: [
+            "FlaggedApi",
+        ],
+    },
+    jarjar_prefix: "com.android.internal.hidden_from_bootclasspath",
+    jarjar_shards: "10",
 }
 
 java_library {
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index d83109a..5e0428b 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -18,7 +18,7 @@
                tests/
                tools/
 bpfmt = -d
-ktfmt = --kotlinlang-style --include-dirs=services/permission,packages/SystemUI,libs/WindowManager/Shell/src/com/android/wm/shell/freeform,libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode
+ktfmt = --kotlinlang-style --include-dirs=services/permission,packages/SystemUI,libs/WindowManager/Shell/src/com/android/wm/shell/freeform,libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode,libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode
 
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index b6f0c04..7fef4e5 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -23,6 +23,10 @@
 import android.annotation.TestApi;
 import android.app.ActivityManager;
 import android.app.usage.UsageStatsManager;
+import android.compat.Compatibility;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
+import android.compat.annotation.Overridable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ClipData;
 import android.content.pm.PackageManager;
@@ -349,6 +353,16 @@
     private JobCleanupCallback mJobCleanupCallback;
     @Nullable
     private Cleaner.Cleanable mCleanable;
+    /**
+     * Override handling of abandoned jobs in the system. Overriding this change
+     * will prevent the system to handle abandoned jobs and report it as a new
+     * stop reason STOP_REASON_TIMEOUT_ABANDONED.
+     * @hide
+     */
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long OVERRIDE_HANDLE_ABANDONED_JOBS = 372529068L;
 
     /** @hide */
     public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras,
@@ -677,6 +691,10 @@
      * @hide
      */
     public void enableCleaner() {
+        if (!Flags.handleAbandonedJobs()
+                || Compatibility.isChangeEnabled(OVERRIDE_HANDLE_ABANDONED_JOBS)) {
+            return;
+        }
         // JobParameters objects are passed by reference in local Binder
         // transactions for clients running as SYSTEM. The life cycle of the
         // JobParameters objects are no longer controlled by the client.
@@ -695,6 +713,10 @@
      * @hide
      */
     public void disableCleaner() {
+        if (!Flags.handleAbandonedJobs()
+                || Compatibility.isChangeEnabled(OVERRIDE_HANDLE_ABANDONED_JOBS)) {
+            return;
+        }
         if (mJobCleanupCallback != null) {
             mJobCleanupCallback.disableCleaner();
             if (mCleanable != null) {
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 69b83cc..d460dcc 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -165,11 +165,9 @@
                 case MSG_EXECUTE_JOB: {
                     final JobParameters params = (JobParameters) msg.obj;
                     try {
-                        if (Flags.handleAbandonedJobs()) {
-                            params.enableCleaner();
-                        }
+                        params.enableCleaner();
                         boolean workOngoing = JobServiceEngine.this.onStartJob(params);
-                        if (Flags.handleAbandonedJobs() && !workOngoing) {
+                        if (!workOngoing) {
                             params.disableCleaner();
                         }
                         ackStartMessage(params, workOngoing);
@@ -196,9 +194,7 @@
                     IJobCallback callback = params.getCallback();
                     if (callback != null) {
                         try {
-                            if (Flags.handleAbandonedJobs()) {
-                                params.disableCleaner();
-                            }
+                            params.disableCleaner();
                             callback.jobFinished(params.getJobId(), needsReschedule);
                         } catch (RemoteException e) {
                             Log.e(TAG, "Error reporting job finish to system: binder has gone" +
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 0b88405..4335cae 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.job;
 
+import static android.app.job.JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
 import static android.Manifest.permission.MANAGE_ACTIVITY_TASKS;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
@@ -1986,8 +1987,8 @@
                     jobStatus.getNumAbandonedFailures(),
                     /* 0 is reserved for UNKNOWN_POLICY */
                     jobStatus.getJob().getBackoffPolicy() + 1,
-                    shouldUseAggressiveBackoff(jobStatus.getNumAbandonedFailures()));
-
+                    shouldUseAggressiveBackoff(
+                            jobStatus.getNumAbandonedFailures(), jobStatus.getSourceUid()));
 
             // If the job is immediately ready to run, then we can just immediately
             // put it in the pending list and try to schedule it.  This is especially
@@ -2432,7 +2433,8 @@
                     cancelled.getNumAbandonedFailures(),
                     /* 0 is reserved for UNKNOWN_POLICY */
                     cancelled.getJob().getBackoffPolicy() + 1,
-                    shouldUseAggressiveBackoff(cancelled.getNumAbandonedFailures()));
+                    shouldUseAggressiveBackoff(
+                            cancelled.getNumAbandonedFailures(), cancelled.getSourceUid()));
         }
         // If this is a replacement, bring in the new version of the job
         if (incomingJob != null) {
@@ -3024,6 +3026,7 @@
         int numFailures = failureToReschedule.getNumFailures();
         int numAbandonedFailures = failureToReschedule.getNumAbandonedFailures();
         int numSystemStops = failureToReschedule.getNumSystemStops();
+        final int uid = failureToReschedule.getSourceUid();
         // We should back off slowly if JobScheduler keeps stopping the job,
         // but back off immediately if the issue appeared to be the app's fault
         // or the user stopped the job somehow.
@@ -3033,6 +3036,7 @@
                 || stopReason == JobParameters.STOP_REASON_USER) {
             numFailures++;
         } else if (android.app.job.Flags.handleAbandonedJobs()
+                && !CompatChanges.isChangeEnabled(OVERRIDE_HANDLE_ABANDONED_JOBS, uid)
                 && internalStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED) {
             numAbandonedFailures++;
             numFailures++;
@@ -3041,7 +3045,7 @@
         }
 
         int backoffPolicy = job.getBackoffPolicy();
-        if (shouldUseAggressiveBackoff(numAbandonedFailures)) {
+        if (shouldUseAggressiveBackoff(numAbandonedFailures, uid)) {
             backoffPolicy = JobInfo.BACKOFF_POLICY_EXPONENTIAL;
         }
 
@@ -3112,8 +3116,9 @@
      * @return {@code true} if the given number of abandoned failures indicates that JobScheduler
      *     should use an aggressive backoff policy.
      */
-    public boolean shouldUseAggressiveBackoff(int numAbandonedFailures) {
+    public boolean shouldUseAggressiveBackoff(int numAbandonedFailures, int uid) {
         return android.app.job.Flags.handleAbandonedJobs()
+                && !CompatChanges.isChangeEnabled(OVERRIDE_HANDLE_ABANDONED_JOBS, uid)
                 && numAbandonedFailures
                         > mConstants.ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF;
     }
@@ -3223,7 +3228,9 @@
     @VisibleForTesting
     void maybeProcessBuggyJob(@NonNull JobStatus jobStatus, int debugStopReason) {
         boolean jobTimedOut = debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT;
-        if (android.app.job.Flags.handleAbandonedJobs()) {
+        if (android.app.job.Flags.handleAbandonedJobs()
+                && !CompatChanges.isChangeEnabled(
+                        OVERRIDE_HANDLE_ABANDONED_JOBS, jobStatus.getSourceUid())) {
             jobTimedOut |= (debugStopReason
                 == JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED);
         }
@@ -3309,6 +3316,8 @@
         final JobStatus rescheduledJob = needsReschedule
                 ? getRescheduleJobForFailureLocked(jobStatus, stopReason, debugStopReason) : null;
         final boolean isStopReasonAbandoned = android.app.job.Flags.handleAbandonedJobs()
+                && !CompatChanges.isChangeEnabled(
+                        OVERRIDE_HANDLE_ABANDONED_JOBS, jobStatus.getSourceUid())
                 && (debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED);
         if (rescheduledJob != null
                 && !rescheduledJob.shouldTreatAsUserInitiatedJob()
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 2b401c8..ebfda52 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -16,6 +16,8 @@
 
 package com.android.server.job;
 
+import static android.app.job.JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS;
+
 import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 import static com.android.server.job.JobSchedulerService.safelyScaleBytesToKBForHistogram;
@@ -550,7 +552,8 @@
                     job.getNumAbandonedFailures(),
                     /* 0 is reserved for UNKNOWN_POLICY */
                     job.getJob().getBackoffPolicy() + 1,
-                    mService.shouldUseAggressiveBackoff(job.getNumAbandonedFailures()));
+                    mService.shouldUseAggressiveBackoff(
+                            job.getNumAbandonedFailures(), job.getSourceUid()));
             sEnqueuedJwiAtJobStart.logSampleWithUid(job.getUid(), job.getWorkCount());
             final String sourcePackage = job.getSourcePackageName();
             if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
@@ -1461,7 +1464,10 @@
                     final StringBuilder debugStopReason = new StringBuilder("client timed out");
 
                     if (android.app.job.Flags.handleAbandonedJobs()
-                            && executing != null && executing.isAbandoned()) {
+                            && executing != null
+                            && !CompatChanges.isChangeEnabled(
+                                    OVERRIDE_HANDLE_ABANDONED_JOBS, executing.getSourceUid())
+                            && executing.isAbandoned()) {
                         final String abandonedMessage = " and maybe abandoned";
                         stopReason = JobParameters.STOP_REASON_TIMEOUT_ABANDONED;
                         internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED;
@@ -1689,7 +1695,8 @@
                 completedJob.getNumAbandonedFailures(),
                 /* 0 is reserved for UNKNOWN_POLICY */
                 completedJob.getJob().getBackoffPolicy() + 1,
-                mService.shouldUseAggressiveBackoff(completedJob.getNumAbandonedFailures()));
+                mService.shouldUseAggressiveBackoff(
+                        completedJob.getNumAbandonedFailures(), completedJob.getSourceUid()));
         if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
             Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER,
                     JobSchedulerService.TRACE_TRACK_NAME, getId());
diff --git a/boot/preloaded-classes b/boot/preloaded-classes
index afd9984..b83bd4e 100644
--- a/boot/preloaded-classes
+++ b/boot/preloaded-classes
@@ -6583,6 +6583,7 @@
 android.permission.PermissionCheckerManager
 android.permission.PermissionControllerManager$1
 android.permission.PermissionControllerManager
+android.permission.PermissionManager
 android.permission.PermissionManager$1
 android.permission.PermissionManager$2
 android.permission.PermissionManager$OnPermissionsChangeListenerDelegate
diff --git a/config/preloaded-classes b/config/preloaded-classes
index 343de0b..e53c78f 100644
--- a/config/preloaded-classes
+++ b/config/preloaded-classes
@@ -6587,6 +6587,7 @@
 android.permission.PermissionCheckerManager
 android.permission.PermissionControllerManager$1
 android.permission.PermissionControllerManager
+android.permission.PermissionManager
 android.permission.PermissionManager$1
 android.permission.PermissionManager$2
 android.permission.PermissionManager$OnPermissionsChangeListenerDelegate
diff --git a/config/preloaded-classes-denylist b/config/preloaded-classes-denylist
index 16f0693..e3e929c 100644
--- a/config/preloaded-classes-denylist
+++ b/config/preloaded-classes-denylist
@@ -3,7 +3,6 @@
 android.net.ConnectivityThread$Singleton
 android.os.FileObserver
 android.os.NullVibrator
-android.permission.PermissionManager
 android.provider.MediaStore
 android.speech.tts.TextToSpeech$Connection$SetupConnectionAsyncTask
 android.view.HdrRenderState
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 03ef669..fee8cdb 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -5782,8 +5782,7 @@
         }
 
         if (!getAttributionSource().getRenouncedPermissions().isEmpty()) {
-            final int permissionCount = permissions.length;
-            for (int i = 0; i < permissionCount; i++) {
+            for (int i = 0; i < permissions.length; i++) {
                 if (getAttributionSource().getRenouncedPermissions().contains(permissions[i])) {
                     throw new IllegalArgumentException("Cannot request renounced permission: "
                             + permissions[i]);
@@ -5791,13 +5790,55 @@
             }
         }
 
-        PackageManager packageManager = getDeviceId() == deviceId ? getPackageManager()
-                : createDeviceContext(deviceId).getPackageManager();
+        final Context context = getDeviceId() == deviceId ? this : createDeviceContext(deviceId);
+        if (Flags.permissionRequestShortCircuitEnabled()) {
+            int[] permissionsState = getPermissionRequestStates(context, permissions);
+            boolean hasRequestablePermission = false;
+            for (int i = 0; i < permissionsState.length; i++) {
+                if (permissionsState[i] == Context.PERMISSION_REQUEST_STATE_REQUESTABLE) {
+                    hasRequestablePermission = true;
+                    break;
+                }
+            }
+            // If none of the permissions is requestable, finish the request here.
+            if (!hasRequestablePermission) {
+                mHasCurrentPermissionsRequest = true;
+                Log.v(TAG, "No requestable permission in the request.");
+                int[] results = new int[permissionsState.length];
+                for (int i = 0; i < permissionsState.length; i++) {
+                    if (permissionsState[i] == Context.PERMISSION_REQUEST_STATE_GRANTED) {
+                        results[i] = PackageManager.PERMISSION_GRANTED;
+                    } else {
+                        results[i] = PackageManager.PERMISSION_DENIED;
+                    }
+                }
+                // Currently permission request result is passed to the client app asynchronously
+                // in onRequestPermissionsResult, lets keep async behavior here as well.
+                mHandler.post(() -> {
+                    mHasCurrentPermissionsRequest = false;
+                    onRequestPermissionsResult(requestCode, permissions, results, deviceId);
+                });
+                return;
+            }
+        }
+
+        final PackageManager packageManager = context.getPackageManager();
         final Intent intent = packageManager.buildRequestPermissionsIntent(permissions);
         startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
         mHasCurrentPermissionsRequest = true;
     }
 
+    @NonNull
+    private int[] getPermissionRequestStates(@NonNull Context deviceContext,
+            @NonNull String[] permissions) {
+        final int size = permissions.length;
+        int[] results = new int[size];
+        for (int i = 0; i < size; i++) {
+            results[i] = deviceContext.getPermissionRequestState(permissions[i]);
+        }
+        return results;
+    }
+
     /**
      * Callback for the result from requesting permissions. This method
      * is invoked for every call on {@link #requestPermissions}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index ec17333..717a2ac 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -7098,12 +7098,9 @@
             System.runFinalization();
             System.gc();
         }
-        if (dhd.dumpBitmaps != null) {
-            Bitmap.dumpAll(dhd.dumpBitmaps);
-        }
         try (ParcelFileDescriptor fd = dhd.fd) {
             if (dhd.managed) {
-                Debug.dumpHprofData(dhd.path, fd.getFileDescriptor());
+                Debug.dumpHprofData(dhd.path, fd.getFileDescriptor(), dhd.dumpBitmaps);
             } else if (dhd.mallocInfo) {
                 Debug.dumpNativeMallocInfo(fd.getFileDescriptor());
             } else {
@@ -7128,9 +7125,6 @@
         if (dhd.finishCallback != null) {
             dhd.finishCallback.sendResult(null);
         }
-        if (dhd.dumpBitmaps != null) {
-            Bitmap.dumpAll(null); // clear dump
-        }
     }
 
     final void handleDispatchPackageBroadcast(int cmd, String[] packages) {
diff --git a/core/java/android/app/CameraCompatTaskInfo.java b/core/java/android/app/CameraCompatTaskInfo.java
index 845d2ac..aff6b35 100644
--- a/core/java/android/app/CameraCompatTaskInfo.java
+++ b/core/java/android/app/CameraCompatTaskInfo.java
@@ -36,36 +36,42 @@
  */
 public class CameraCompatTaskInfo implements Parcelable {
     /**
+     * Undefined camera compat mode.
+     */
+    public static final int CAMERA_COMPAT_FREEFORM_UNSPECIFIED = 0;
+
+    /**
      * The value to use when no camera compat treatment should be applied to a windowed task.
      */
-    public static final int CAMERA_COMPAT_FREEFORM_NONE = 0;
+    public static final int CAMERA_COMPAT_FREEFORM_NONE = 1;
 
     /**
      * The value to use when camera compat treatment should be applied to an activity requesting
      * portrait orientation, while a device is in landscape. Applies only to freeform tasks.
      */
-    public static final int CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE = 1;
+    public static final int CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE = 2;
 
     /**
      * The value to use when camera compat treatment should be applied to an activity requesting
      * landscape orientation, while a device is in landscape. Applies only to freeform tasks.
      */
-    public static final int CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE = 2;
+    public static final int CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE = 3;
 
     /**
      * The value to use when camera compat treatment should be applied to an activity requesting
      * portrait orientation, while a device is in portrait. Applies only to freeform tasks.
      */
-    public static final int CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT = 3;
+    public static final int CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT = 4;
 
     /**
      * The value to use when camera compat treatment should be applied to an activity requesting
      * landscape orientation, while a device is in portrait. Applies only to freeform tasks.
      */
-    public static final int CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT = 4;
+    public static final int CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT = 5;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = { "CAMERA_COMPAT_FREEFORM_" }, value = {
+            CAMERA_COMPAT_FREEFORM_UNSPECIFIED,
             CAMERA_COMPAT_FREEFORM_NONE,
             CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE,
             CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE,
@@ -184,6 +190,7 @@
     public static String freeformCameraCompatModeToString(
             @FreeformCameraCompatMode int freeformCameraCompatMode) {
         return switch (freeformCameraCompatMode) {
+            case CAMERA_COMPAT_FREEFORM_UNSPECIFIED -> "undefined";
             case CAMERA_COMPAT_FREEFORM_NONE -> "inactive";
             case CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE ->
                     "app-portrait-device-landscape";
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index dcbdc23..d8aa8b3 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -2366,7 +2366,11 @@
             Log.v(TAG, "Treating renounced permission " + permission + " as denied");
             return PERMISSION_DENIED;
         }
+        int deviceId = resolveDeviceIdForPermissionCheck(permission);
+        return PermissionManager.checkPermission(permission, pid, uid, deviceId);
+    }
 
+    private int resolveDeviceIdForPermissionCheck(String permission) {
         // When checking a device-aware permission on a remote device, if the permission is CAMERA
         // or RECORD_AUDIO we need to check remote device's corresponding capability. If the remote
         // device doesn't have capability fall back to checking permission on the default device.
@@ -2387,9 +2391,9 @@
                 VirtualDevice virtualDevice = virtualDeviceManager.getVirtualDevice(deviceId);
                 if (virtualDevice != null) {
                     if ((Objects.equals(permission, Manifest.permission.RECORD_AUDIO)
-                                    && !virtualDevice.hasCustomAudioInputSupport())
+                            && !virtualDevice.hasCustomAudioInputSupport())
                             || (Objects.equals(permission, Manifest.permission.CAMERA)
-                                    && !virtualDevice.hasCustomCameraSupport())) {
+                            && !virtualDevice.hasCustomCameraSupport())) {
                         deviceId = Context.DEVICE_ID_DEFAULT;
                     }
                 } else {
@@ -2400,8 +2404,7 @@
                 }
             }
         }
-
-        return PermissionManager.checkPermission(permission, pid, uid, deviceId);
+        return deviceId;
     }
 
     /** @hide */
@@ -2503,6 +2506,16 @@
                 message);
     }
 
+    /** @hide */
+    @Override
+    public int getPermissionRequestState(String permission) {
+        Objects.requireNonNull(permission, "Permission name can't be null");
+        int deviceId = resolveDeviceIdForPermissionCheck(permission);
+        PermissionManager permissionManager = getSystemService(PermissionManager.class);
+        return permissionManager.getPermissionRequestState(getOpPackageName(), permission,
+                deviceId);
+    }
+
     @Override
     public void grantUriPermission(String toPackage, Uri uri, int modeFlags) {
          try {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index aa2ada5..17638ee 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3323,6 +3323,18 @@
     }
 
     /**
+     * Make sure this String is safe to put into a bundle.
+     * @hide
+     */
+    public static String safeString(String str) {
+        if (str == null) return str;
+        if (str.length() > MAX_CHARSEQUENCE_LENGTH) {
+            str = str.substring(0, MAX_CHARSEQUENCE_LENGTH);
+        }
+        return str;
+    }
+
+    /**
      * Make sure this CharSequence is safe to put into a bundle, which basically
      * means it had better not be some custom Parcelable implementation.
      * @hide
@@ -5051,7 +5063,7 @@
         @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
         @NonNull
         public Builder setShortCriticalText(@Nullable String shortCriticalText) {
-            mN.extras.putString(EXTRA_SHORT_CRITICAL_TEXT, shortCriticalText);
+            mN.extras.putString(EXTRA_SHORT_CRITICAL_TEXT, safeString(shortCriticalText));
             return this;
         }
 
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index c72c4c8..5567c08 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1356,7 +1356,7 @@
             @Nullable QueryHandler<Query, Result> computer) {
         mPropertyName = createPropertyName(args.mModule, args.mApi);
         mCacheName = cacheName;
-        mCacheNullResults = args.mCacheNulls && Flags.picCacheNulls();
+        mCacheNullResults = args.mCacheNulls;
         mNonce = getNonceHandler(mPropertyName);
         mMaxEntries = args.mMaxEntries;
         mCache = new CacheMap<>(args.mIsolateUids, args.mTestMode);
diff --git a/core/java/android/app/compat/ChangeIdStateCache.java b/core/java/android/app/compat/ChangeIdStateCache.java
index 7d21cbf..258ce06 100644
--- a/core/java/android/app/compat/ChangeIdStateCache.java
+++ b/core/java/android/app/compat/ChangeIdStateCache.java
@@ -16,8 +16,6 @@
 
 package android.app.compat;
 
-import static android.app.PropertyInvalidatedCache.createSystemCacheKey;
-
 import android.annotation.NonNull;
 import android.app.PropertyInvalidatedCache;
 import android.content.Context;
@@ -34,7 +32,10 @@
 @android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class ChangeIdStateCache
         extends PropertyInvalidatedCache<ChangeIdStateQuery, Boolean> {
-    private static final String CACHE_KEY = createSystemCacheKey("is_compat_change_enabled");
+
+    private static final String CACHE_MODULE = PropertyInvalidatedCache.MODULE_SYSTEM;
+    private static final String CACHE_API = "is_compat_change_enabled";
+
     private static final int MAX_ENTRIES = 2048;
     private static boolean sDisabled = getDefaultDisabled();
     private volatile IPlatformCompat mPlatformCompat;
@@ -51,7 +52,12 @@
 
     /** @hide */
     public ChangeIdStateCache() {
-        super(MAX_ENTRIES, CACHE_KEY);
+        super(new PropertyInvalidatedCache.Args(CACHE_MODULE)
+                .maxEntries(MAX_ENTRIES)
+                .isolateUids(false)
+                .cacheNulls(false)
+                .api(CACHE_API),
+                CACHE_API, null);
     }
 
     /**
@@ -72,7 +78,7 @@
      */
     public static void invalidate() {
         if (!sDisabled) {
-            PropertyInvalidatedCache.invalidateCache(CACHE_KEY);
+            PropertyInvalidatedCache.invalidateCache(CACHE_MODULE, CACHE_API);
         }
     }
 
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 6ec6a62..7abf560 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -788,6 +788,40 @@
     public static final int RECEIVER_NOT_EXPORTED = 0x4;
 
     /**
+     * The permission is granted.
+     *
+     * @hide
+     */
+    public static final int PERMISSION_REQUEST_STATE_GRANTED = 0;
+
+    /**
+     * The permission isn't granted, but apps can request the permission. When the app request
+     * the permission, user will be prompted with permission dialog to grant or deny the request.
+     *
+     * @hide
+     */
+    public static final int PERMISSION_REQUEST_STATE_REQUESTABLE = 1;
+
+    /**
+     * The permission is denied, and shouldn't be requested by apps. Permission request
+     * will be automatically denied by the system, preventing the permission dialog from being
+     * displayed to the user.
+     *
+     * @hide
+     */
+    public static final int PERMISSION_REQUEST_STATE_UNREQUESTABLE = 2;
+
+
+    /** @hide */
+    @IntDef(prefix = { "PERMISSION_REQUEST_STATE_" }, value = {
+            PERMISSION_REQUEST_STATE_GRANTED,
+            PERMISSION_REQUEST_STATE_REQUESTABLE,
+            PERMISSION_REQUEST_STATE_UNREQUESTABLE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PermissionRequestState {}
+
+    /**
      * Returns an AssetManager instance for the application's package.
      * <p>
      * <strong>Note:</strong> Implementations of this method should return
@@ -6989,6 +7023,31 @@
             @NonNull @PermissionName String permission, @Nullable String message);
 
     /**
+     * Returns the permission request state for a given runtime permission. This method provides a
+     * streamlined mechanism for applications to determine whether a permission can be
+     * requested (i.e. whether the user will be prompted with a permission dialog).
+     *
+     * <p>Traditionally, determining if a permission has been permanently denied (unrequestable)
+     * required applications to initiate a permission request and subsequently analyze the result
+     * of {@link android.app.Activity#shouldShowRequestPermissionRationale} in conjunction with the
+     * grant result within the {@link android.app.Activity#onRequestPermissionsResult} callback.
+     *
+     * @param permission The name of the permission.
+     *
+     * @return The current request state of the specified permission, represented by one of the
+     * following constants: {@link PermissionRequestState#PERMISSION_REQUEST_STATE_GRANTED},
+     * {@link PermissionRequestState#PERMISSION_REQUEST_STATE_REQUESTABLE}, or
+     * {@link PermissionRequestState#PERMISSION_REQUEST_STATE_UNREQUESTABLE}.
+     *
+     * @hide
+     */
+    @CheckResult
+    @PermissionRequestState
+    public int getPermissionRequestState(@NonNull String permission) {
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
+    }
+
+    /**
      * Grant permission to access a specific Uri to another package, regardless
      * of whether that package has general permission to access the Uri's
      * content provider.  This can be used to grant specific, temporary
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 413eb98..a146807 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -1012,6 +1012,12 @@
         mBase.enforceCallingOrSelfPermission(permission, message);
     }
 
+    /** @hide */
+    @Override
+    public int getPermissionRequestState(String permission) {
+        return mBase.getPermissionRequestState(permission);
+    }
+
     @Override
     public void grantUriPermission(String toPackage, Uri uri, int modeFlags) {
         mBase.grantUriPermission(toPackage, uri, modeFlags);
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 4b579e7..219b204 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -2322,10 +2322,10 @@
 
             } else if (tagName.equals(TAG_ADOPT_PERMISSIONS)) {
                 sa = res.obtainAttributes(parser,
-                        com.android.internal.R.styleable.AndroidManifestOriginalPackage);
+                        com.android.internal.R.styleable.AndroidManifestAdoptPermissions);
 
                 String name = sa.getNonConfigurationString(
-                        com.android.internal.R.styleable.AndroidManifestOriginalPackage_name, 0);
+                        com.android.internal.R.styleable.AndroidManifestAdoptPermissions_name, 0);
 
                 sa.recycle();
 
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 00ddae3..0d219a9 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -149,6 +149,13 @@
 }
 
 flag {
+    name: "cache_sdk_system_features"
+    namespace: "system_performance"
+    description: "Feature flag to enable optimized cache for SDK-defined system feature lookups."
+    bug: "375000483"
+}
+
+flag {
     name: "provide_info_of_apk_in_apex"
     is_exported: true
     namespace: "package_manager_service"
diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java
index b251aa1..99f331f 100644
--- a/core/java/android/hardware/contexthub/HubEndpoint.java
+++ b/core/java/android/hardware/contexthub/HubEndpoint.java
@@ -404,11 +404,11 @@
 
         HubEndpointSession newSession;
         try {
-            // Request system service to assign session id.
-            int sessionId = mServiceToken.openSession(destinationInfo, serviceDescriptor);
-
-            // Save the newly created session
             synchronized (mLock) {
+                // Request system service to assign session id.
+                int sessionId = mServiceToken.openSession(destinationInfo, serviceDescriptor);
+
+                // Save the newly created session
                 newSession =
                         new HubEndpointSession(
                                 sessionId,
diff --git a/core/java/android/hardware/contexthub/HubServiceInfo.java b/core/java/android/hardware/contexthub/HubServiceInfo.java
index a1c52fb..2f33e8f 100644
--- a/core/java/android/hardware/contexthub/HubServiceInfo.java
+++ b/core/java/android/hardware/contexthub/HubServiceInfo.java
@@ -132,6 +132,21 @@
         return 0;
     }
 
+    @Override
+    public String toString() {
+        StringBuilder out = new StringBuilder();
+        out.append("Service: ");
+        out.append("descriptor=");
+        out.append(mServiceDescriptor);
+        out.append(", format=");
+        out.append(mFormat);
+        out.append(", version=");
+        out.append(Integer.toHexString(mMajorVersion));
+        out.append(".");
+        out.append(Integer.toHexString(mMinorVersion));
+        return out.toString();
+    }
+
     /** Parcel implementation details */
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
diff --git a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl
index b76b227..44f80c8 100644
--- a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl
+++ b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl
@@ -39,6 +39,7 @@
      * @throws IllegalArgumentException If the HubEndpointInfo is not valid.
      * @throws IllegalStateException If there are too many opened sessions.
      */
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
     int openSession(in HubEndpointInfo destination, in @nullable String serviceDescriptor);
 
     /**
@@ -49,6 +50,7 @@
      *
      * @throws IllegalStateException If the session wasn't opened.
      */
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
     void closeSession(int sessionId, int reason);
 
     /**
@@ -60,11 +62,13 @@
      *
      * @throws IllegalStateException If the session wasn't opened.
      */
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
     void openSessionRequestComplete(int sessionId);
 
     /**
      * Unregister this endpoint from the HAL, invalidate the EndpointInfo previously assigned.
      */
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
     void unregister();
 
     /**
@@ -76,6 +80,7 @@
      * @param transactionCallback Nullable. If the hub message requires a reply, the transactionCallback
      *                            will be set to non-null.
      */
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
     void sendMessage(int sessionId, in HubMessage message,
                      in @nullable IContextHubTransactionCallback transactionCallback);
 
@@ -87,5 +92,6 @@
      * @param messageSeqNumber The message sequence number, this should match a previously received HubMessage.
      * @param errorCode The message delivery status detail.
      */
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
     void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode);
 }
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index aaa78aa..313bad5 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -196,4 +196,11 @@
     namespace: "input"
     description: "Allows the user to disable pointer acceleration for mouse and touchpads."
     bug: "349006858"
-}
\ No newline at end of file
+}
+
+flag {
+    name: "mouse_scrolling_acceleration"
+    namespace: "input"
+    description: "Allows the user to disable input scrolling acceleration for mouse."
+    bug: "383555305"
+}
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index ef1e6c94..2bc6ab5 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -22,6 +22,7 @@
 import android.app.AppGlobals;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.util.Log;
 
 import com.android.internal.util.FastPrintWriter;
@@ -2139,6 +2140,47 @@
     }
 
     /**
+     * Like dumpHprofData(String), but takes an argument of bitmapFormat,
+     * which can be png, jpg, webp, or null (no bitmaps in heapdump).
+     *
+     * @hide
+     */
+    public static void dumpHprofData(String fileName, String bitmapFormat)
+            throws IOException {
+        try {
+            if (bitmapFormat != null) {
+                Bitmap.dumpAll(bitmapFormat);
+            }
+            VMDebug.dumpHprofData(fileName);
+        } finally {
+            if (bitmapFormat != null) {
+                Bitmap.dumpAll(null); // clear dump data
+            }
+        }
+    }
+
+    /**
+     * Like dumpHprofData(String, FileDescriptor), but takes an argument
+     * of bitmapFormat, which can be png, jpg, webp, or null (no bitmaps
+     * in heapdump).
+     *
+     * @hide
+     */
+    public static void dumpHprofData(String fileName, FileDescriptor fd,
+            String bitmapFormat) throws IOException {
+        try {
+            if (bitmapFormat != null) {
+                Bitmap.dumpAll(bitmapFormat);
+            }
+            VMDebug.dumpHprofData(fileName, fd);
+        } finally {
+            if (bitmapFormat != null) {
+                Bitmap.dumpAll(null); // clear dump data
+            }
+        }
+    }
+
+    /**
      * Collect "hprof" and send it to DDMS.  This may cause a GC.
      *
      * @throws UnsupportedOperationException if the VM was built without
diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl
index 4a14a8d..56a089a 100644
--- a/core/java/android/os/IHintManager.aidl
+++ b/core/java/android/os/IHintManager.aidl
@@ -21,11 +21,13 @@
 import android.os.GpuHeadroomParamsInternal;
 import android.os.IHintSession;
 import android.os.SessionCreationConfig;
-import android.hardware.power.CpuHeadroomResult;
+
 import android.hardware.power.ChannelConfig;
+import android.hardware.power.CpuHeadroomResult;
 import android.hardware.power.GpuHeadroomResult;
 import android.hardware.power.SessionConfig;
 import android.hardware.power.SessionTag;
+import android.hardware.power.SupportInfo;
 
 /** {@hide} */
 interface IHintManager {
@@ -40,11 +42,6 @@
     IHintSession createHintSessionWithConfig(in IBinder token, in SessionTag tag,
             in SessionCreationConfig creationConfig, out SessionConfig config);
 
-    /**
-     * Get preferred rate limit in nanoseconds.
-     */
-    long getHintSessionPreferredRate();
-
     void setHintSessionThreads(in IHintSession hintSession, in int[] tids);
     int[] getHintSessionThreadIds(in IHintSession hintSession);
 
@@ -61,13 +58,28 @@
     long getGpuHeadroomMinIntervalMillis();
 
     /**
-     * Get Maximum number of graphics pipeline threads allowed per-app.
-     */
-    int getMaxGraphicsPipelineThreadsCount();
-
-    /**
      * Used by the JNI to pass an interface to the SessionManager;
      * for internal use only.
      */
     oneway void passSessionManagerBinder(in IBinder sessionManager);
+
+    parcelable HintManagerClientData {
+        int powerHalVersion;
+        int maxGraphicsPipelineThreads;
+        long preferredRateNanos;
+        SupportInfo supportInfo;
+    }
+
+    interface IHintManagerClient {
+        /**
+        * Returns FMQ channel information for the caller, which it associates to the callback binder lifespan.
+        */
+        oneway void receiveChannelConfig(in ChannelConfig config);
+    }
+
+    /**
+     * Set up an ADPF client, receiving a remote client binder interface and
+     * passing back a bundle of support and configuration information.
+     */
+    HintManagerClientData registerClient(in IHintManagerClient client);
 }
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index b9f2cfc..132805d 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -4197,12 +4197,21 @@
             android.Manifest.permission.MANAGE_USERS,
             android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
     private boolean hasUserRestrictionForUser(@NonNull @UserRestrictionKey String restrictionKey,
-            @UserIdInt int userId) {
-        try {
-            return mService.hasUserRestriction(restrictionKey, userId);
-        } catch (RemoteException re) {
-            throw re.rethrowFromSystemServer();
-        }
+            @NonNull @UserIdInt int userId) {
+        return getUserRestrictionFromQuery(new Pair(restrictionKey, userId));
+    }
+
+    /** @hide */
+    @CachedProperty()
+    private boolean getUserRestrictionFromQuery(@NonNull Pair<String, Integer> restrictionPerUser) {
+        return UserManagerCache.getUserRestrictionFromQuery(
+                (Pair<String, Integer> q) -> mService.hasUserRestriction(q.first, q.second),
+                restrictionPerUser);
+    }
+
+    /** @hide */
+    public static final void invalidateUserRestriction() {
+        UserManagerCache.invalidateUserRestrictionFromQuery();
     }
 
     /**
@@ -6477,6 +6486,7 @@
             UserManagerCache.invalidateProfileParent();
         }
         invalidateEnabledProfileIds();
+        invalidateUserRestriction();
     }
 
     /**
diff --git a/core/java/android/os/health/OWNERS b/core/java/android/os/health/OWNERS
index 6045344..26fc8fa 100644
--- a/core/java/android/os/health/OWNERS
+++ b/core/java/android/os/health/OWNERS
@@ -2,3 +2,6 @@
 
 dplotnikov@google.com
 mwachens@google.com
+
+# for headroom API only
+xwxw@google.com
\ No newline at end of file
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
index a95ce79..c7778de 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
@@ -22,7 +22,8 @@
 import android.os.VibrationEffect;
 import android.util.Xml;
 
-import com.android.internal.vibrator.persistence.VibrationEffectXmlSerializer;
+import com.android.internal.vibrator.persistence.LegacyVibrationEffectXmlSerializer;
+import com.android.internal.vibrator.persistence.VibrationEffectSerializer;
 import com.android.internal.vibrator.persistence.XmlConstants;
 import com.android.internal.vibrator.persistence.XmlSerializedVibration;
 import com.android.internal.vibrator.persistence.XmlSerializerException;
@@ -123,7 +124,13 @@
         }
 
         try {
-            serializedVibration = VibrationEffectXmlSerializer.serialize(effect, serializerFlags);
+            if (android.os.vibrator.Flags.normalizedPwleEffects()) {
+                serializedVibration = VibrationEffectSerializer.serialize(effect,
+                        serializerFlags);
+            } else {
+                serializedVibration = LegacyVibrationEffectXmlSerializer.serialize(effect,
+                        serializerFlags);
+            }
             XmlValidator.checkSerializedVibration(serializedVibration, effect);
         } catch (XmlSerializerException e) {
             // Serialization failed or created incomplete representation, fail before writing.
diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl
index 55011e5..b375812 100644
--- a/core/java/android/permission/IPermissionManager.aidl
+++ b/core/java/android/permission/IPermissionManager.aidl
@@ -108,6 +108,8 @@
     int checkUidPermission(int uid, String permissionName, int deviceId);
 
     Map<String, PermissionState> getAllPermissionStates(String packageName, String persistentDeviceId, int userId);
+
+    int getPermissionRequestState(String packageName, String permissionName, int deviceId);
 }
 
 /**
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 2473de4..bdf8d23 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1742,6 +1742,16 @@
         }
     }
 
+    private static int getPermissionRequestStateUncached(String packageName, String permission,
+            int deviceId) {
+        try {
+            return AppGlobals.getPermissionManager().getPermissionRequestState(
+                    packageName, permission, deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /**
      * Identifies a permission query.
      *
@@ -1795,6 +1805,46 @@
         }
     }
 
+    private static final class PermissionRequestStateQuery {
+        final String mPackageName;
+        final String mPermission;
+        final int mDeviceId;
+
+        PermissionRequestStateQuery(@NonNull String packageName, @NonNull String permission,
+                int deviceId) {
+            mPackageName = packageName;
+            mPermission = permission;
+            mDeviceId = deviceId;
+        }
+
+        @Override
+        public String toString() {
+            return TextUtils.formatSimple("PermissionRequestStateQuery(package=\"%s\","
+                            + " permission=\"%s\", " + "deviceId=%d)",
+                    mPackageName, mPermission, mDeviceId);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mPackageName, mPermission, mDeviceId);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object rval) {
+            if (rval == null) {
+                return false;
+            }
+            PermissionRequestStateQuery other;
+            try {
+                other = (PermissionRequestStateQuery) rval;
+            } catch (ClassCastException ex) {
+                return false;
+            }
+            return mDeviceId == other.mDeviceId && Objects.equals(mPackageName, other.mPackageName)
+                    && Objects.equals(mPermission, other.mPermission);
+        }
+    }
+
     // The legacy system property "package_info" had two purposes: to invalidate PIC caches and to
     // signal that package information, and therefore permissions, might have changed.
     // AudioSystem is the only client of the signaling behavior.  The "separate permissions
@@ -1842,10 +1892,30 @@
             };
 
     /** @hide */
+    private static final PropertyInvalidatedCache<PermissionRequestStateQuery, Integer>
+            sPermissionRequestStateCache =
+            new PropertyInvalidatedCache<>(
+                    512, CACHE_KEY_PACKAGE_INFO_CACHE, "getPermissionRequestState") {
+                @Override
+                public Integer recompute(PermissionRequestStateQuery query) {
+                    return getPermissionRequestStateUncached(query.mPackageName, query.mPermission,
+                            query.mDeviceId);
+                }
+            };
+
+    /** @hide */
     public static int checkPermission(@Nullable String permission, int pid, int uid, int deviceId) {
         return sPermissionCache.query(new PermissionQuery(permission, pid, uid, deviceId));
     }
 
+    /** @hide */
+    @Context.PermissionRequestState
+    public int getPermissionRequestState(@NonNull String packageName, @NonNull String permission,
+            int deviceId) {
+        return sPermissionRequestStateCache.query(
+                new PermissionRequestStateQuery(packageName, permission, deviceId));
+    }
+
     /**
      * Gets the permission states for requested package and persistent device.
      * <p>
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 07b9f52..70d8891 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -340,7 +340,7 @@
     is_fixed_read_only: true
     namespace: "health_fitness_aconfig"
     description: "This flag protects the permission that is required to call Health Connect backup and restore apis"
-    bug: "376014879" # android_fr bug
+    bug: "324019102" # android_fr bug
     is_exported: true
 }
 
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index cf0e90f..c3a4930 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8950,6 +8950,18 @@
                 "high_text_contrast_enabled";
 
         /**
+         * Setting that specifies the status of the High Contrast Text
+         * rectangle refresh's one-time prompt.
+         * 0 = UNKNOWN
+         * 1 = PROMPT_SHOWN
+         * 2 = PROMPT_UNNECESSARY
+         *
+         * @hide
+         */
+        public static final String ACCESSIBILITY_HCT_RECT_PROMPT_STATUS =
+                "accessibility_hct_rect_prompt_status";
+
+        /**
          * The color contrast, float in [-1, 1], 1 being the highest contrast.
          *
          * @hide
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 1ea226b..0c8a0d6 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -2056,6 +2056,22 @@
     }
 
     /**
+     * Returns whether the display is eligible for hosting tasks.
+     *
+     * For example, if the display is used for mirroring, this will return {@code false}.
+     *
+     * TODO (b/383666349): Rename this later once there is a better option.
+     *
+     * @hide
+     */
+    public boolean canHostTasks() {
+        synchronized (mLock) {
+            updateDisplayInfoLocked();
+            return mIsValid && mDisplayInfo.canHostTasks;
+        }
+    }
+
+    /**
      * Returns true if the specified UID has access to this display.
      * @hide
      */
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 4307884..ba098eb5 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -408,6 +408,15 @@
     @Nullable
     public String thermalBrightnessThrottlingDataId;
 
+    /**
+     * Indicates whether the display is eligible for hosting tasks.
+     *
+     * For example, if the display is used for mirroring, this will be {@code false}.
+     *
+     * @hide
+     */
+    public boolean canHostTasks;
+
     public static final @android.annotation.NonNull Creator<DisplayInfo> CREATOR = new Creator<DisplayInfo>() {
         @Override
         public DisplayInfo createFromParcel(Parcel source) {
@@ -493,7 +502,8 @@
                 && BrightnessSynchronizer.floatEquals(hdrSdrRatio, other.hdrSdrRatio)
                 && thermalRefreshRateThrottling.contentEquals(other.thermalRefreshRateThrottling)
                 && Objects.equals(
-                thermalBrightnessThrottlingDataId, other.thermalBrightnessThrottlingDataId);
+                thermalBrightnessThrottlingDataId, other.thermalBrightnessThrottlingDataId)
+                && canHostTasks == other.canHostTasks;
     }
 
     @Override
@@ -561,6 +571,7 @@
         hdrSdrRatio = other.hdrSdrRatio;
         thermalRefreshRateThrottling = other.thermalRefreshRateThrottling;
         thermalBrightnessThrottlingDataId = other.thermalBrightnessThrottlingDataId;
+        canHostTasks = other.canHostTasks;
     }
 
     public void readFromParcel(Parcel source) {
@@ -642,6 +653,7 @@
         thermalRefreshRateThrottling = source.readSparseArray(null,
                 SurfaceControl.RefreshRateRange.class);
         thermalBrightnessThrottlingDataId = source.readString8();
+        canHostTasks = source.readBoolean();
     }
 
     @Override
@@ -717,6 +729,7 @@
         dest.writeFloat(hdrSdrRatio);
         dest.writeSparseArray(thermalRefreshRateThrottling);
         dest.writeString8(thermalBrightnessThrottlingDataId);
+        dest.writeBoolean(canHostTasks);
     }
 
     @Override
@@ -1020,6 +1033,8 @@
         sb.append(thermalRefreshRateThrottling);
         sb.append(", thermalBrightnessThrottlingDataId ");
         sb.append(thermalBrightnessThrottlingDataId);
+        sb.append(", canHostTasks ");
+        sb.append(canHostTasks);
         sb.append("}");
         return sb.toString();
     }
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index dd9a95e..f22505b 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -4671,8 +4671,7 @@
          * Sets the importance the layer's contents has to the app's user experience.
          * <p>
          * When a two layers within the same app are competing for a limited rendering resource,
-         * the priority will determine which layer gets access to the resource. The lower the
-         * priority, the more likely the layer will get access to the resource.
+         * the layer with the highest priority will gets access to the resource.
          * <p>
          * Resources managed by this priority:
          * <ul>
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 609f1ef..1d27574 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -4435,7 +4435,8 @@
                 // merged with a sync group or BLASTBufferQueue before making it to this point
                 // But better a one or two frame flicker than steady-state broken from dropping
                 // whatever is in this transaction
-                mPendingTransaction.apply();
+                // apply immediately with bbq apply token
+                mergeWithNextTransaction(mPendingTransaction, 0);
                 mHasPendingTransactions = false;
             }
             mSyncBuffer = false;
@@ -5501,7 +5502,8 @@
                 Log.d(mTag, "Pending transaction will not be applied in sync with a draw due to "
                         + logReason);
             }
-            pendingTransaction.apply();
+            // apply immediately with bbq apply token
+            mergeWithNextTransaction(pendingTransaction, 0);
         }
     }
     /**
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 049ad20..294e5da 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -233,6 +233,16 @@
 }
 
 flag {
+    name: "restore_a11y_secure_settings_on_hsum_device"
+    namespace: "accessibility"
+    description: "Grab the a11y settings and send the settings restored broadcast for current visible foreground user"
+    bug: "381294327"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "supplemental_description"
     namespace: "accessibility"
     description: "Feature flag for supplemental description api"
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 6caa20e2..1707e61b 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -175,13 +175,6 @@
 }
 
 flag {
-    name: "enable_a11y_metrics"
-    namespace: "lse_desktop_experience"
-    description: "Whether to enable log collection for a11y actions in desktop windowing mode"
-    bug: "341319597"
-}
-
-flag {
     name: "enable_caption_compat_inset_force_consumption"
     namespace: "lse_desktop_experience"
     description: "Enables force-consumption of caption bar insets for immersive apps in freeform"
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index b56aadd..c9c4be1 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -250,23 +250,22 @@
     private static class BatteryHistoryDirectory {
         private final File mDirectory;
         private final MonotonicClock mMonotonicClock;
-        private int mMaxHistoryFiles;
+        private int mMaxHistorySize;
         private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>();
         private final ReentrantLock mLock = new ReentrantLock();
         private boolean mCleanupNeeded;
 
-        BatteryHistoryDirectory(File directory, MonotonicClock monotonicClock,
-                int maxHistoryFiles) {
+        BatteryHistoryDirectory(File directory, MonotonicClock monotonicClock, int maxHistorySize) {
             mDirectory = directory;
             mMonotonicClock = monotonicClock;
-            mMaxHistoryFiles = maxHistoryFiles;
-            if (mMaxHistoryFiles == 0) {
-                Slog.wtf(TAG, "mMaxHistoryFiles should not be zero when writing history");
+            mMaxHistorySize = maxHistorySize;
+            if (mMaxHistorySize == 0) {
+                Slog.w(TAG, "mMaxHistorySize should not be zero when writing history");
             }
         }
 
-        void setMaxHistoryFiles(int maxHistoryFiles) {
-            mMaxHistoryFiles = maxHistoryFiles;
+        void setMaxHistorySize(int maxHistorySize) {
+            mMaxHistorySize = maxHistorySize;
             cleanup();
         }
 
@@ -500,13 +499,14 @@
                     oldest.atomicFile.delete();
                 }
 
-                // if there are more history files than allowed, delete oldest history files.
-                // mMaxHistoryFiles comes from Constants.MAX_HISTORY_FILES and
-                // can be updated by DeviceConfig at run time.
-                while (mHistoryFiles.size() > mMaxHistoryFiles) {
+                // if there is more history stored than allowed, delete oldest history files.
+                int size = getSize();
+                while (size > mMaxHistorySize) {
                     BatteryHistoryFile oldest = mHistoryFiles.get(0);
+                    int length = (int) oldest.atomicFile.getBaseFile().length();
                     oldest.atomicFile.delete();
                     mHistoryFiles.remove(0);
+                    size -= length;
                 }
             } finally {
                 unlock();
@@ -595,19 +595,19 @@
      * Constructor
      *
      * @param systemDir            typically /data/system
-     * @param maxHistoryFiles      the largest number of history buffer files to keep
+     * @param maxHistorySize       the largest amount of battery history to keep on disk
      * @param maxHistoryBufferSize the most amount of RAM to used for buffering of history steps
      */
     public BatteryStatsHistory(Parcel historyBuffer, File systemDir,
-            int maxHistoryFiles, int maxHistoryBufferSize,
+            int maxHistorySize, int maxHistoryBufferSize,
             HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
             MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) {
-        this(historyBuffer, systemDir, maxHistoryFiles, maxHistoryBufferSize, stepDetailsCalculator,
+        this(historyBuffer, systemDir, maxHistorySize, maxHistoryBufferSize, stepDetailsCalculator,
                 clock, monotonicClock, tracer, eventLogger, null);
     }
 
     private BatteryStatsHistory(@Nullable Parcel historyBuffer, @Nullable File systemDir,
-            int maxHistoryFiles, int maxHistoryBufferSize,
+            int maxHistorySize, int maxHistoryBufferSize,
             @NonNull HistoryStepDetailsCalculator stepDetailsCalculator, @NonNull Clock clock,
             @NonNull MonotonicClock monotonicClock, @NonNull TraceDelegate tracer,
             @NonNull EventLogger eventLogger, @Nullable BatteryStatsHistory writableHistory) {
@@ -634,7 +634,7 @@
             mHistoryDir = writableHistory.mHistoryDir;
         } else if (systemDir != null) {
             mHistoryDir = new BatteryHistoryDirectory(new File(systemDir, HISTORY_DIR),
-                    monotonicClock, maxHistoryFiles);
+                    monotonicClock, maxHistorySize);
             mHistoryDir.load();
             BatteryHistoryFile activeFile = mHistoryDir.getLastFile();
             if (activeFile == null) {
@@ -690,11 +690,11 @@
     }
 
     /**
-     * Changes the maximum number of history files to be kept.
+     * Changes the maximum amount of history to be kept on disk.
      */
-    public void setMaxHistoryFiles(int maxHistoryFiles) {
+    public void setMaxHistorySize(int maxHistorySize) {
         if (mHistoryDir != null) {
-            mHistoryDir.setMaxHistoryFiles(maxHistoryFiles);
+            mHistoryDir.setMaxHistorySize(maxHistorySize);
         }
     }
 
@@ -1175,6 +1175,13 @@
     }
 
     /**
+     * Returns the maximum storage size allocated to battery history.
+     */
+    public int getMaxHistorySize() {
+        return mHistoryDir.mMaxHistorySize;
+    }
+
+    /**
      * @return the total size of all history files and history buffer.
      */
     public int getHistoryUsedSize() {
diff --git a/core/java/com/android/internal/os/logging/MetricsLoggerWrapper.java b/core/java/com/android/internal/os/logging/MetricsLoggerWrapper.java
index 0e0098e..efdc8ca 100644
--- a/core/java/com/android/internal/os/logging/MetricsLoggerWrapper.java
+++ b/core/java/com/android/internal/os/logging/MetricsLoggerWrapper.java
@@ -61,7 +61,7 @@
             return;
         }
         int pid = Process.myPid();
-        String processName = Application.getProcessName();
+        String processName = Process.myProcessName();
         Collection<NativeAllocationRegistry.Metrics> metrics =
             NativeAllocationRegistry.getMetrics();
         int nMetrics = metrics.size();
diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
index c160b42..5c08dc6 100644
--- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
@@ -3133,9 +3133,9 @@
 
     private static ParseResult<ParsingPackage> parseAdoptPermissions(ParseInput input,
             ParsingPackage pkg, Resources res, XmlResourceParser parser) {
-        TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestOriginalPackage);
+        TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestAdoptPermissions);
         try {
-            String name = nonConfigString(0, R.styleable.AndroidManifestOriginalPackage_name, sa);
+            String name = nonConfigString(0, R.styleable.AndroidManifestAdoptPermissions_name, sa);
             if (name != null) {
                 pkg.addAdoptPermission(name);
             }
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/LegacyVibrationEffectXmlSerializer.java
similarity index 99%
rename from core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
rename to core/java/com/android/internal/vibrator/persistence/LegacyVibrationEffectXmlSerializer.java
index ebe3434..be30750 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
+++ b/core/java/com/android/internal/vibrator/persistence/LegacyVibrationEffectXmlSerializer.java
@@ -53,7 +53,7 @@
  *
  * @hide
  */
-public final class VibrationEffectXmlSerializer {
+public final class LegacyVibrationEffectXmlSerializer {
 
     /**
      * Creates a serialized representation of the input {@code vibration}.
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
index cd7dcfd..efc7e35 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
@@ -35,6 +35,7 @@
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.function.BiConsumer;
 
 /**
  * Serialized representation of a waveform effect created via
@@ -144,7 +145,7 @@
             // Read all nested tag that is not a repeating tag as a waveform entry.
             while (XmlReader.readNextTagWithin(parser, outerDepth)
                     && !TAG_REPEATING.equals(parser.getName())) {
-                parseWaveformEntry(parser, waveformBuilder);
+                parseWaveformEntry(parser, waveformBuilder::addDurationAndAmplitude);
             }
 
             // If found a repeating tag, read its content.
@@ -162,29 +163,8 @@
             return waveformBuilder.build();
         }
 
-        private static void parseRepeating(TypedXmlPullParser parser, Builder waveformBuilder)
-                throws XmlParserException, IOException {
-            XmlValidator.checkStartTag(parser, TAG_REPEATING);
-            XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
-
-            waveformBuilder.setRepeatIndexToCurrentEntry();
-
-            boolean hasEntry = false;
-            int outerDepth = parser.getDepth();
-            while (XmlReader.readNextTagWithin(parser, outerDepth)) {
-                parseWaveformEntry(parser, waveformBuilder);
-                hasEntry = true;
-            }
-
-            // Check schema assertions about <repeating>
-            XmlValidator.checkParserCondition(hasEntry, "Unexpected empty %s tag", TAG_REPEATING);
-
-            // Consume tag
-            XmlReader.readEndTag(parser, TAG_REPEATING, outerDepth);
-        }
-
-        private static void parseWaveformEntry(TypedXmlPullParser parser, Builder waveformBuilder)
-                throws XmlParserException, IOException {
+        static void parseWaveformEntry(TypedXmlPullParser parser,
+                BiConsumer<Integer, Integer> builder) throws XmlParserException, IOException {
             XmlValidator.checkStartTag(parser, TAG_WAVEFORM_ENTRY);
             XmlValidator.checkTagHasNoUnexpectedAttributes(
                     parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_AMPLITUDE);
@@ -196,10 +176,31 @@
                             parser, ATTRIBUTE_AMPLITUDE, 0, VibrationEffect.MAX_AMPLITUDE);
             int durationMs = XmlReader.readAttributeIntNonNegative(parser, ATTRIBUTE_DURATION_MS);
 
-            waveformBuilder.addDurationAndAmplitude(durationMs, amplitude);
+            builder.accept(durationMs, amplitude);
 
             // Consume tag
             XmlReader.readEndTag(parser);
         }
+
+        private static void parseRepeating(TypedXmlPullParser parser, Builder waveformBuilder)
+                throws XmlParserException, IOException {
+            XmlValidator.checkStartTag(parser, TAG_REPEATING);
+            XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
+
+            waveformBuilder.setRepeatIndexToCurrentEntry();
+
+            boolean hasEntry = false;
+            int outerDepth = parser.getDepth();
+            while (XmlReader.readNextTagWithin(parser, outerDepth)) {
+                parseWaveformEntry(parser, waveformBuilder::addDurationAndAmplitude);
+                hasEntry = true;
+            }
+
+            // Check schema assertions about <repeating>
+            XmlValidator.checkParserCondition(hasEntry, "Unexpected empty %s tag", TAG_REPEATING);
+
+            // Consume tag
+            XmlReader.readEndTag(parser, TAG_REPEATING, outerDepth);
+        }
     }
 }
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedRepeatingEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedRepeatingEffect.java
new file mode 100644
index 0000000..12acc72
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedRepeatingEffect.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREAMBLE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PRIMITIVE_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_REPEATING;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_REPEATING_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENTRY;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.VibrationEffect;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Serialized representation of a repeating effect created via
+ * {@link VibrationEffect#createRepeatingEffect}.
+ *
+ * @hide
+ */
+public class SerializedRepeatingEffect implements SerializedComposedEffect.SerializedSegment {
+
+    @Nullable
+    private final SerializedComposedEffect mSerializedPreamble;
+    @NonNull
+    private final SerializedComposedEffect mSerializedRepeating;
+
+    SerializedRepeatingEffect(@Nullable SerializedComposedEffect serializedPreamble,
+            @NonNull SerializedComposedEffect serializedRepeating) {
+        mSerializedPreamble = serializedPreamble;
+        mSerializedRepeating = serializedRepeating;
+    }
+
+    @Override
+    public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+        serializer.startTag(NAMESPACE, TAG_REPEATING_EFFECT);
+
+        if (mSerializedPreamble != null) {
+            serializer.startTag(NAMESPACE, TAG_PREAMBLE);
+            mSerializedPreamble.writeContent(serializer);
+            serializer.endTag(NAMESPACE, TAG_PREAMBLE);
+        }
+
+        serializer.startTag(NAMESPACE, TAG_REPEATING);
+        mSerializedRepeating.writeContent(serializer);
+        serializer.endTag(NAMESPACE, TAG_REPEATING);
+
+        serializer.endTag(NAMESPACE, TAG_REPEATING_EFFECT);
+    }
+
+    @Override
+    public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+        if (mSerializedPreamble != null) {
+            composition.addEffect(
+                    VibrationEffect.createRepeatingEffect(mSerializedPreamble.deserialize(),
+                            mSerializedRepeating.deserialize()));
+            return;
+        }
+
+        composition.addEffect(
+                VibrationEffect.createRepeatingEffect(mSerializedRepeating.deserialize()));
+    }
+
+    @Override
+    public String toString() {
+        return "SerializedRepeatingEffect{"
+                + "preamble=" + mSerializedPreamble
+                + ", repeating=" + mSerializedRepeating
+                + '}';
+    }
+
+    static final class Builder {
+        private SerializedComposedEffect mPreamble;
+        private SerializedComposedEffect mRepeating;
+
+        void setPreamble(SerializedComposedEffect effect) {
+            mPreamble = effect;
+        }
+
+        void setRepeating(SerializedComposedEffect effect) {
+            mRepeating = effect;
+        }
+
+        boolean hasRepeatingSegment() {
+            return mRepeating != null;
+        }
+
+        SerializedRepeatingEffect build() {
+            return new SerializedRepeatingEffect(mPreamble, mRepeating);
+        }
+    }
+
+    /** Parser implementation for {@link SerializedRepeatingEffect}. */
+    static final class Parser {
+
+        @NonNull
+        static SerializedRepeatingEffect parseNext(@NonNull TypedXmlPullParser parser,
+                @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+            XmlValidator.checkStartTag(parser, TAG_REPEATING_EFFECT);
+            XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
+
+            Builder builder = new Builder();
+            int outerDepth = parser.getDepth();
+
+            boolean hasNestedTag = XmlReader.readNextTagWithin(parser, outerDepth);
+            if (hasNestedTag && TAG_PREAMBLE.equals(parser.getName())) {
+                builder.setPreamble(parseEffect(parser, TAG_PREAMBLE, flags));
+                hasNestedTag = XmlReader.readNextTagWithin(parser, outerDepth);
+            }
+
+            XmlValidator.checkParserCondition(hasNestedTag,
+                    "Missing %s tag in %s", TAG_REPEATING, TAG_REPEATING_EFFECT);
+            builder.setRepeating(parseEffect(parser, TAG_REPEATING, flags));
+
+            XmlValidator.checkParserCondition(builder.hasRepeatingSegment(),
+                    "Unexpected %s tag with no repeating segment", TAG_REPEATING_EFFECT);
+
+            // Consume tag
+            XmlReader.readEndTag(parser, TAG_REPEATING_EFFECT, outerDepth);
+
+            return builder.build();
+        }
+
+        private static SerializedComposedEffect parseEffect(TypedXmlPullParser parser,
+                String tagName, int flags) throws XmlParserException, IOException {
+            XmlValidator.checkStartTag(parser, tagName);
+            XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
+            int vibrationTagDepth = parser.getDepth();
+            XmlValidator.checkParserCondition(
+                    XmlReader.readNextTagWithin(parser, vibrationTagDepth),
+                    "Unsupported empty %s tag", tagName);
+
+            SerializedComposedEffect effect;
+            switch (parser.getName()) {
+                case TAG_PREDEFINED_EFFECT:
+                    effect = new SerializedComposedEffect(
+                            SerializedPredefinedEffect.Parser.parseNext(parser, flags));
+                    break;
+                case TAG_PRIMITIVE_EFFECT:
+                    effect = parsePrimitiveEffects(parser, vibrationTagDepth);
+                    break;
+                case TAG_WAVEFORM_ENTRY:
+                    effect = parseWaveformEntries(parser, vibrationTagDepth);
+                    break;
+                case TAG_WAVEFORM_ENVELOPE_EFFECT:
+                    effect = new SerializedComposedEffect(
+                            SerializedWaveformEnvelopeEffect.Parser.parseNext(parser, flags));
+                    break;
+                case TAG_BASIC_ENVELOPE_EFFECT:
+                    effect = new SerializedComposedEffect(
+                            SerializedBasicEnvelopeEffect.Parser.parseNext(parser, flags));
+                    break;
+                default:
+                    throw new XmlParserException("Unexpected tag " + parser.getName()
+                            + " in vibration tag " + tagName);
+            }
+
+            // Consume tag
+            XmlReader.readEndTag(parser, tagName, vibrationTagDepth);
+
+            return effect;
+        }
+
+        private static SerializedComposedEffect parsePrimitiveEffects(TypedXmlPullParser parser,
+                int vibrationTagDepth)
+                throws IOException, XmlParserException {
+            List<SerializedComposedEffect.SerializedSegment> primitives = new ArrayList<>();
+            do { // First primitive tag already open
+                primitives.add(SerializedCompositionPrimitive.Parser.parseNext(parser));
+            } while (XmlReader.readNextTagWithin(parser, vibrationTagDepth));
+            return new SerializedComposedEffect(primitives.toArray(
+                    new SerializedComposedEffect.SerializedSegment[
+                            primitives.size()]));
+        }
+
+        private static SerializedComposedEffect parseWaveformEntries(TypedXmlPullParser parser,
+                int vibrationTagDepth)
+                throws IOException, XmlParserException {
+            SerializedWaveformEffectEntries.Builder waveformBuilder =
+                    new SerializedWaveformEffectEntries.Builder();
+            do { // First waveform-entry tag already open
+                SerializedWaveformEffectEntries
+                        .Parser.parseWaveformEntry(parser, waveformBuilder);
+            } while (XmlReader.readNextTagWithin(parser, vibrationTagDepth));
+            XmlValidator.checkParserCondition(waveformBuilder.hasNonZeroDuration(),
+                    "Unexpected %s tag with total duration zero", TAG_WAVEFORM_ENTRY);
+            return new SerializedComposedEffect(waveformBuilder.build());
+        }
+    }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEffectEntries.java b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEffectEntries.java
new file mode 100644
index 0000000..8849e75
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEffectEntries.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_AMPLITUDE;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_DURATION_MS;
+import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENTRY;
+import static com.android.internal.vibrator.persistence.XmlConstants.VALUE_AMPLITUDE_DEFAULT;
+
+import android.annotation.NonNull;
+import android.os.VibrationEffect;
+import android.util.IntArray;
+import android.util.LongArray;
+
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Serialized representation of a list of waveform entries created via
+ * {@link VibrationEffect#createWaveform(long[], int[], int)}.
+ *
+ * @hide
+ */
+final class SerializedWaveformEffectEntries implements SerializedSegment {
+
+    @NonNull
+    private final long[] mTimings;
+    @NonNull
+    private final int[] mAmplitudes;
+
+    private SerializedWaveformEffectEntries(@NonNull long[] timings,
+            @NonNull int[] amplitudes) {
+        mTimings = timings;
+        mAmplitudes = amplitudes;
+    }
+
+    @Override
+    public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+        composition.addEffect(VibrationEffect.createWaveform(mTimings, mAmplitudes, -1));
+    }
+
+    @Override
+    public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+        for (int i = 0; i < mTimings.length; i++) {
+            serializer.startTag(NAMESPACE, TAG_WAVEFORM_ENTRY);
+
+            if (mAmplitudes[i] == VibrationEffect.DEFAULT_AMPLITUDE) {
+                serializer.attribute(NAMESPACE, ATTRIBUTE_AMPLITUDE, VALUE_AMPLITUDE_DEFAULT);
+            } else {
+                serializer.attributeInt(NAMESPACE, ATTRIBUTE_AMPLITUDE, mAmplitudes[i]);
+            }
+
+            serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, mTimings[i]);
+            serializer.endTag(NAMESPACE, TAG_WAVEFORM_ENTRY);
+        }
+
+    }
+
+    @Override
+    public String toString() {
+        return "SerializedWaveformEffectEntries{"
+                + "timings=" + Arrays.toString(mTimings)
+                + ", amplitudes=" + Arrays.toString(mAmplitudes)
+                + '}';
+    }
+
+    /** Builder for {@link SerializedWaveformEffectEntries}. */
+    static final class Builder {
+        private final LongArray mTimings = new LongArray();
+        private final IntArray mAmplitudes = new IntArray();
+
+        void addDurationAndAmplitude(long durationMs, int amplitude) {
+            mTimings.add(durationMs);
+            mAmplitudes.add(amplitude);
+        }
+
+        boolean hasNonZeroDuration() {
+            for (int i = 0; i < mTimings.size(); i++) {
+                if (mTimings.get(i) > 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        SerializedWaveformEffectEntries build() {
+            return new SerializedWaveformEffectEntries(
+                    mTimings.toArray(), mAmplitudes.toArray());
+        }
+    }
+
+    /** Parser implementation for the {@link XmlConstants#TAG_WAVEFORM_ENTRY}. */
+    static final class Parser {
+
+        /** Parses a single {@link XmlConstants#TAG_WAVEFORM_ENTRY} into the builder. */
+        public static void parseWaveformEntry(TypedXmlPullParser parser, Builder waveformBuilder)
+                throws XmlParserException, IOException {
+            SerializedAmplitudeStepWaveform.Parser.parseWaveformEntry(parser,
+                    waveformBuilder::addDurationAndAmplitude);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectSerializer.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectSerializer.java
new file mode 100644
index 0000000..df483ec
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectSerializer.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.vibrator.persistence;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.PersistableBundle;
+import android.os.VibrationEffect;
+import android.os.vibrator.BasicPwleSegment;
+import android.os.vibrator.Flags;
+import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationEffectSegment;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * Serializer implementation for {@link VibrationEffect}.
+ *
+ * <p>This serializer does not support effects created with {@link VibrationEffect.WaveformBuilder}
+ * nor {@link VibrationEffect.Composition#addEffect(VibrationEffect)}. It only supports vibration
+ * effects defined as:
+ *
+ * <ul>
+ *     <li>{@link VibrationEffect#createPredefined(int)}
+ *     <li>{@link VibrationEffect#createWaveform(long[], int[], int)}
+ *     <li>A composition created exclusively via
+ *         {@link VibrationEffect.Composition#addPrimitive(int, float, int)}
+ *     <li>{@link VibrationEffect#createVendorEffect(PersistableBundle)}
+ *     <li>{@link VibrationEffect.WaveformEnvelopeBuilder}
+ *     <li>{@link VibrationEffect.BasicEnvelopeBuilder}
+ * </ul>
+ *
+ * <p>This serializer also supports repeating effects. For repeating waveform effects, it attempts
+ * to serialize the effect as a single unit. If this fails, it falls back to serializing it as a
+ * sequence of individual waveform entries.
+ *
+ * @hide
+ */
+public class VibrationEffectSerializer {
+    private static final String TAG = "VibrationEffectSerializer";
+
+    /**
+     * Creates a serialized representation of the input {@code vibration}.
+     */
+    @NonNull
+    public static XmlSerializedVibration<? extends VibrationEffect> serialize(
+            @NonNull VibrationEffect vibration, @XmlConstants.Flags int flags)
+            throws XmlSerializerException {
+
+        if (Flags.vendorVibrationEffects()
+                && (vibration instanceof VibrationEffect.VendorEffect vendorEffect)) {
+            return serializeVendorEffect(vendorEffect);
+        }
+
+        XmlValidator.checkSerializerCondition(vibration instanceof VibrationEffect.Composed,
+                "Unsupported VibrationEffect type %s", vibration);
+
+        VibrationEffect.Composed composed = (VibrationEffect.Composed) vibration;
+        XmlValidator.checkSerializerCondition(!composed.getSegments().isEmpty(),
+                "Unsupported empty VibrationEffect %s", vibration);
+
+        List<VibrationEffectSegment> segments = composed.getSegments();
+        int repeatIndex = composed.getRepeatIndex();
+
+        SerializedComposedEffect serializedEffect;
+        if (repeatIndex >= 0) {
+            serializedEffect = trySerializeRepeatingAmplitudeWaveformEffect(segments, repeatIndex);
+            if (serializedEffect == null) {
+                serializedEffect = serializeRepeatingEffect(segments, repeatIndex, flags);
+            }
+        } else {
+            serializedEffect = serializeNonRepeatingEffect(segments, flags);
+        }
+
+        return serializedEffect;
+    }
+
+    private static SerializedComposedEffect serializeRepeatingEffect(
+            List<VibrationEffectSegment> segments, int repeatIndex, @XmlConstants.Flags int flags)
+            throws XmlSerializerException {
+
+        SerializedRepeatingEffect.Builder builder = new SerializedRepeatingEffect.Builder();
+        if (repeatIndex > 0) {
+            List<VibrationEffectSegment> preambleSegments = segments.subList(0, repeatIndex);
+            builder.setPreamble(serializeEffectEntries(preambleSegments, flags));
+
+            // Update segments to match the repeating block only, after preamble was consumed.
+            segments = segments.subList(repeatIndex, segments.size());
+        }
+
+        builder.setRepeating(serializeEffectEntries(segments, flags));
+
+        return new SerializedComposedEffect(builder.build());
+    }
+
+    @NonNull
+    private static SerializedComposedEffect serializeNonRepeatingEffect(
+            List<VibrationEffectSegment> segments, @XmlConstants.Flags int flags)
+            throws XmlSerializerException {
+        SerializedComposedEffect effect = trySerializeNonWaveformEffect(segments, flags);
+        if (effect == null) {
+            effect = serializeWaveformEffect(segments);
+        }
+
+        return effect;
+    }
+
+    @NonNull
+    private static SerializedComposedEffect serializeEffectEntries(
+            List<VibrationEffectSegment> segments, @XmlConstants.Flags int flags)
+            throws XmlSerializerException {
+        SerializedComposedEffect effect = trySerializeNonWaveformEffect(segments, flags);
+        if (effect == null) {
+            effect = serializeWaveformEffectEntries(segments);
+        }
+
+        return effect;
+    }
+
+    @Nullable
+    private static SerializedComposedEffect trySerializeNonWaveformEffect(
+            List<VibrationEffectSegment> segments, int flags) throws XmlSerializerException {
+        VibrationEffectSegment firstSegment = segments.getFirst();
+
+        if (firstSegment instanceof PrebakedSegment) {
+            return serializePredefinedEffect(segments, flags);
+        }
+        if (firstSegment instanceof PrimitiveSegment) {
+            return serializePrimitiveEffect(segments);
+        }
+        if (firstSegment instanceof PwleSegment) {
+            return serializeWaveformEnvelopeEffect(segments);
+        }
+        if (firstSegment instanceof BasicPwleSegment) {
+            return serializeBasicEnvelopeEffect(segments);
+        }
+
+        return null;
+    }
+
+    private static SerializedComposedEffect serializePredefinedEffect(
+            List<VibrationEffectSegment> segments, @XmlConstants.Flags int flags)
+            throws XmlSerializerException {
+        XmlValidator.checkSerializerCondition(segments.size() == 1,
+                "Unsupported multiple segments in predefined effect: %s", segments);
+        return new SerializedComposedEffect(serializePrebakedSegment(segments.getFirst(), flags));
+    }
+
+    private static SerializedVendorEffect serializeVendorEffect(
+            VibrationEffect.VendorEffect effect) {
+        return new SerializedVendorEffect(effect.getVendorData());
+    }
+
+    private static SerializedComposedEffect serializePrimitiveEffect(
+            List<VibrationEffectSegment> segments) throws XmlSerializerException {
+        SerializedComposedEffect.SerializedSegment[] primitives =
+                new SerializedComposedEffect.SerializedSegment[segments.size()];
+        for (int i = 0; i < segments.size(); i++) {
+            primitives[i] = serializePrimitiveSegment(segments.get(i));
+        }
+
+        return new SerializedComposedEffect(primitives);
+    }
+
+    private static SerializedComposedEffect serializeWaveformEnvelopeEffect(
+            List<VibrationEffectSegment> segments) throws XmlSerializerException {
+        SerializedWaveformEnvelopeEffect.Builder builder =
+                new SerializedWaveformEnvelopeEffect.Builder();
+        for (int i = 0; i < segments.size(); i++) {
+            XmlValidator.checkSerializerCondition(segments.get(i) instanceof PwleSegment,
+                    "Unsupported segment for waveform envelope effect %s", segments.get(i));
+            PwleSegment segment = (PwleSegment) segments.get(i);
+
+            if (i == 0 && segment.getStartFrequencyHz() != segment.getEndFrequencyHz()) {
+                // Initial frequency explicitly defined.
+                builder.setInitialFrequencyHz(segment.getStartFrequencyHz());
+            }
+
+            builder.addControlPoint(segment.getEndAmplitude(), segment.getEndFrequencyHz(),
+                    segment.getDuration());
+        }
+
+        return new SerializedComposedEffect(builder.build());
+    }
+
+    private static SerializedComposedEffect serializeBasicEnvelopeEffect(
+            List<VibrationEffectSegment> segments) throws XmlSerializerException {
+        SerializedBasicEnvelopeEffect.Builder builder = new SerializedBasicEnvelopeEffect.Builder();
+        for (int i = 0; i < segments.size(); i++) {
+            XmlValidator.checkSerializerCondition(segments.get(i) instanceof BasicPwleSegment,
+                    "Unsupported segment for basic envelope effect %s", segments.get(i));
+            BasicPwleSegment segment = (BasicPwleSegment) segments.get(i);
+
+            if (i == 0 && segment.getStartSharpness() != segment.getEndSharpness()) {
+                // Initial sharpness explicitly defined.
+                builder.setInitialSharpness(segment.getStartSharpness());
+            }
+
+            builder.addControlPoint(segment.getEndIntensity(), segment.getEndSharpness(),
+                    segment.getDuration());
+        }
+
+        return new SerializedComposedEffect(builder.build());
+    }
+
+    private static SerializedComposedEffect trySerializeRepeatingAmplitudeWaveformEffect(
+            List<VibrationEffectSegment> segments, int repeatingIndex) {
+        SerializedAmplitudeStepWaveform.Builder builder =
+                new SerializedAmplitudeStepWaveform.Builder();
+
+        for (int i = 0; i < segments.size(); i++) {
+            if (repeatingIndex == i) {
+                builder.setRepeatIndexToCurrentEntry();
+            }
+            try {
+                serializeStepSegment(segments.get(i), builder::addDurationAndAmplitude);
+            } catch (XmlSerializerException e) {
+                return null;
+            }
+        }
+
+        return new SerializedComposedEffect(builder.build());
+    }
+
+    private static SerializedComposedEffect serializeWaveformEffect(
+            List<VibrationEffectSegment> segments) throws XmlSerializerException {
+        SerializedAmplitudeStepWaveform.Builder builder =
+                new SerializedAmplitudeStepWaveform.Builder();
+        for (int i = 0; i < segments.size(); i++) {
+            serializeStepSegment(segments.get(i), builder::addDurationAndAmplitude);
+        }
+
+        return new SerializedComposedEffect(builder.build());
+    }
+
+    private static SerializedComposedEffect serializeWaveformEffectEntries(
+            List<VibrationEffectSegment> segments) throws XmlSerializerException {
+        SerializedWaveformEffectEntries.Builder builder =
+                new SerializedWaveformEffectEntries.Builder();
+        for (int i = 0; i < segments.size(); i++) {
+            serializeStepSegment(segments.get(i), builder::addDurationAndAmplitude);
+        }
+
+        return new SerializedComposedEffect(builder.build());
+    }
+
+    private static void serializeStepSegment(VibrationEffectSegment segment,
+            BiConsumer<Long, Integer> builder) throws XmlSerializerException {
+        XmlValidator.checkSerializerCondition(segment instanceof StepSegment,
+                "Unsupported segment for waveform effect %s", segment);
+
+        XmlValidator.checkSerializerCondition(
+                Float.compare(((StepSegment) segment).getFrequencyHz(), 0) == 0,
+                "Unsupported segment with non-default frequency %f",
+                ((StepSegment) segment).getFrequencyHz());
+
+        builder.accept(segment.getDuration(),
+                toAmplitudeInt(((StepSegment) segment).getAmplitude()));
+    }
+
+    private static SerializedPredefinedEffect serializePrebakedSegment(
+            VibrationEffectSegment segment, @XmlConstants.Flags int flags)
+            throws XmlSerializerException {
+        XmlValidator.checkSerializerCondition(segment instanceof PrebakedSegment,
+                "Unsupported segment for predefined effect %s", segment);
+
+        PrebakedSegment prebaked = (PrebakedSegment) segment;
+        XmlConstants.PredefinedEffectName effectName = XmlConstants.PredefinedEffectName.findById(
+                prebaked.getEffectId(), flags);
+
+        XmlValidator.checkSerializerCondition(effectName != null,
+                "Unsupported predefined effect id %s", prebaked.getEffectId());
+
+        if ((flags & XmlConstants.FLAG_ALLOW_HIDDEN_APIS) == 0) {
+            // Only allow effects with default fallback flag if using the public APIs schema.
+            XmlValidator.checkSerializerCondition(
+                    prebaked.shouldFallback() == PrebakedSegment.DEFAULT_SHOULD_FALLBACK,
+                    "Unsupported predefined effect with should fallback %s",
+                    prebaked.shouldFallback());
+        }
+
+        return new SerializedPredefinedEffect(effectName, prebaked.shouldFallback());
+    }
+
+    private static SerializedCompositionPrimitive serializePrimitiveSegment(
+            VibrationEffectSegment segment) throws XmlSerializerException {
+        XmlValidator.checkSerializerCondition(segment instanceof PrimitiveSegment,
+                "Unsupported segment for primitive composition %s", segment);
+
+        PrimitiveSegment primitive = (PrimitiveSegment) segment;
+        XmlConstants.PrimitiveEffectName primitiveName =
+                XmlConstants.PrimitiveEffectName.findById(primitive.getPrimitiveId());
+
+        XmlValidator.checkSerializerCondition(primitiveName != null,
+                "Unsupported primitive effect id %s", primitive.getPrimitiveId());
+
+        XmlConstants.PrimitiveDelayType delayType = null;
+
+        if (Flags.primitiveCompositionAbsoluteDelay()) {
+            delayType = XmlConstants.PrimitiveDelayType.findByType(primitive.getDelayType());
+            XmlValidator.checkSerializerCondition(delayType != null,
+                    "Unsupported primitive delay type %s", primitive.getDelayType());
+        } else {
+            XmlValidator.checkSerializerCondition(
+                    primitive.getDelayType() == PrimitiveSegment.DEFAULT_DELAY_TYPE,
+                    "Unsupported primitive delay type %s", primitive.getDelayType());
+        }
+
+        return new SerializedCompositionPrimitive(
+                primitiveName, primitive.getScale(), primitive.getDelay(), delayType);
+    }
+
+    private static int toAmplitudeInt(float amplitude) {
+        return Float.compare(amplitude, VibrationEffect.DEFAULT_AMPLITUDE) == 0
+                ? VibrationEffect.DEFAULT_AMPLITUDE
+                : Math.round(amplitude * VibrationEffect.MAX_AMPLITUDE);
+    }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
index 314bfe4..efd75fc 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
@@ -19,6 +19,7 @@
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PRIMITIVE_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_REPEATING_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VIBRATION_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_EFFECT;
@@ -120,6 +121,26 @@
  *     }
  * </pre>
  *
+ * * Repeating effects
+ *
+ * <pre>
+ *     {@code
+ *       <vibration-effect>
+ *          <repeating-effect>
+ *            <preamble>
+ *                <primitive-effect name="click" />
+ *            </preamble>
+ *            <repeating>
+ *              <basic-envelope-effect>
+ *                <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+ *                <control-point intensity="0.0" sharpness="0.5" durationMs="30" />
+ *              </basic-envelope-effect>
+ *            </repeating>
+ *          </repeating-effect>
+ *       </vibration-effect>
+ *     }
+ * </pre>
+ *
  * @hide
  */
 public class VibrationEffectXmlParser {
@@ -191,6 +212,12 @@
                             SerializedBasicEnvelopeEffect.Parser.parseNext(parser, flags));
                     break;
                 } // else fall through
+            case TAG_REPEATING_EFFECT:
+                if (Flags.normalizedPwleEffects()) {
+                    serializedVibration = new SerializedComposedEffect(
+                            SerializedRepeatingEffect.Parser.parseNext(parser, flags));
+                    break;
+                } // else fall through
             default:
                 throw new XmlParserException("Unexpected tag " + parser.getName()
                         + " in vibration tag " + vibrationTagName);
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
index df262cf..cc5c7cf 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
@@ -45,8 +45,10 @@
     public static final String TAG_WAVEFORM_ENVELOPE_EFFECT = "waveform-envelope-effect";
     public static final String TAG_BASIC_ENVELOPE_EFFECT = "basic-envelope-effect";
     public static final String TAG_WAVEFORM_EFFECT = "waveform-effect";
+    public static final String TAG_REPEATING_EFFECT = "repeating-effect";
     public static final String TAG_WAVEFORM_ENTRY = "waveform-entry";
     public static final String TAG_REPEATING = "repeating";
+    public static final String TAG_PREAMBLE = "preamble";
     public static final String TAG_CONTROL_POINT = "control-point";
 
     public static final String ATTRIBUTE_NAME = "name";
diff --git a/core/java/com/android/server/pm/pkg/AndroidPackage.java b/core/java/com/android/server/pm/pkg/AndroidPackage.java
index 70dd10f..5fa8125 100644
--- a/core/java/com/android/server/pm/pkg/AndroidPackage.java
+++ b/core/java/com/android/server/pm/pkg/AndroidPackage.java
@@ -722,7 +722,7 @@
      * The names of packages to adopt ownership of permissions from, parsed under {@link
      * ParsingPackageUtils#TAG_ADOPT_PERMISSIONS}.
      *
-     * @see R.styleable#AndroidManifestOriginalPackage_name
+     * @see R.styleable#AndroidManifestAdoptPermissions_name
      * @hide
      */
     @NonNull
diff --git a/core/jni/android_view_SurfaceControlActivePictureListener.cpp b/core/jni/android_view_SurfaceControlActivePictureListener.cpp
index 91849c1..15132db 100644
--- a/core/jni/android_view_SurfaceControlActivePictureListener.cpp
+++ b/core/jni/android_view_SurfaceControlActivePictureListener.cpp
@@ -106,12 +106,11 @@
     }
 
     status_t startListening() {
-        // TODO(b/337330263): Make SF multiple-listener capable
-        return SurfaceComposerClient::setActivePictureListener(this);
+        return SurfaceComposerClient::addActivePictureListener(this);
     }
 
     status_t stopListening() {
-        return SurfaceComposerClient::setActivePictureListener(nullptr);
+        return SurfaceComposerClient::removeActivePictureListener(this);
     }
 
 protected:
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 2e0fe9e..7e9d623 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -105,6 +105,7 @@
         optional SettingProto accessibility_gesture_targets = 57 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto display_daltonizer_saturation_level = 58 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto accessibility_key_gesture_targets = 59 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto hct_rect_prompt_status = 60 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
     }
     optional Accessibility accessibility = 2;
diff --git a/core/res/res/values-watch/styles_device_defaults.xml b/core/res/res/values-watch/styles_device_defaults.xml
index f3c85a9..d8d424a 100644
--- a/core/res/res/values-watch/styles_device_defaults.xml
+++ b/core/res/res/values-watch/styles_device_defaults.xml
@@ -85,4 +85,11 @@
         <item name="maxHeight">@dimen/progress_bar_height</item>
         <item name="mirrorForRtl">true</item>
     </style>
+
+    <style name="Widget.DeviceDefault.ProgressBar" parent="Widget.Material.ProgressBar">
+        <!-- Allow determinate option -->
+        <item name="indeterminateOnly">false</item>
+        <!-- Use Wear Material3 ring shape as default determinate drawable -->
+        <item name="progressDrawable">@drawable/progress_ring_watch</item>
+    </style>
 </resources>
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index a06d184..8c6fd1d 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -2892,6 +2892,17 @@
         <attr name="name" />
     </declare-styleable>
 
+    <!-- Private tag to declare the package name that the permissions of this package
+         is based on.  Only used for packages installed in the system image.  If
+         given, the permissions from the other package will be propagated into the
+         new package.
+
+         <p>This appears as a child tag of the root
+         {@link #AndroidManifest manifest} tag. -->
+    <declare-styleable name="AndroidManifestAdoptPermissions" parent="AndroidManifest">
+        <attr name="name" />
+    </declare-styleable>
+
     <!-- The <code>processes</code> tag specifies the processes the application will run code in
          and optionally characteristics of those processes.  This tag is optional; if not
          specified, components will simply run in the processes they specify.  If supplied,
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 3d023c3e..a9c0667 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1607,6 +1607,10 @@
          brightness value and will repeat for the following ramp if autobrightness is enabled. -->
     <bool name="config_skipScreenOnBrightnessRamp">false</bool>
 
+    <!-- Whether or not to skip a color fade transition to black when the display transitions to
+         STATE_OFF. Setting this to true will skip the color fade transition. -->
+    <bool name="config_skipScreenOffTransition">false</bool>
+
     <!-- Allow automatic adjusting of the screen brightness while dozing in low power state. -->
     <bool name="config_allowAutoBrightnessWhileDozing">false</bool>
 
diff --git a/core/res/res/values/config_battery_stats.xml b/core/res/res/values/config_battery_stats.xml
index 9498273..1b45373 100644
--- a/core/res/res/values/config_battery_stats.xml
+++ b/core/res/res/values/config_battery_stats.xml
@@ -53,4 +53,6 @@
     battery history, in bytes. -->
     <integer name="config_accumulatedBatteryUsageStatsSpanSize">32768</integer>
 
+    <!-- Size of storage allocated to battery history, in bytes -->
+    <integer name="config_batteryHistoryStorageSize">4194304</integer>
 </resources>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 20ae296..666f1cf 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -506,4 +506,10 @@
     <!-- Whether to allow TN scanning during satellite session. -->
     <bool name="config_satellite_allow_tn_scanning_during_satellite_session">true</bool>
     <java-symbol type="bool" name="config_satellite_allow_tn_scanning_during_satellite_session" />
+
+    <!-- List of integer tag Ids representing VZW satellite coverage. -->
+    <integer-array name="config_verizon_satellite_enabled_tagids">
+    </integer-array>
+    <java-symbol type="array" name="config_verizon_satellite_enabled_tagids" />
+
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 6c01994..3afb9d2 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2045,6 +2045,7 @@
   <java-symbol type="bool" name="config_unplugTurnsOnScreen" />
   <java-symbol type="bool" name="config_usbChargingMessage" />
   <java-symbol type="bool" name="config_skipScreenOnBrightnessRamp" />
+  <java-symbol type="bool" name="config_skipScreenOffTransition" />
   <java-symbol type="bool" name="config_allowAutoBrightnessWhileDozing" />
   <java-symbol type="bool" name="config_allowTheaterModeWakeFromUnplug" />
   <java-symbol type="bool" name="config_allowTheaterModeWakeFromGesture" />
@@ -5358,6 +5359,7 @@
   <java-symbol type="integer" name="config_powerStatsAggregationPeriod" />
   <java-symbol type="integer" name="config_aggregatedPowerStatsSpanDuration" />
   <java-symbol type="integer" name="config_accumulatedBatteryUsageStatsSpanSize" />
+  <java-symbol type="integer" name="config_batteryHistoryStorageSize" />
 
   <!--Dynamic Tokens-->
   <java-symbol name="materialColorBackground" type="color"/>
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index bd27337..e9dfdd8 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -16,7 +16,6 @@
 
 package android.app;
 
-import static android.app.Flags.FLAG_PIC_CACHE_NULLS;
 import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID;
 import static android.app.PropertyInvalidatedCache.NONCE_UNSET;
 import static android.app.PropertyInvalidatedCache.MODULE_BLUETOOTH;
@@ -711,7 +710,6 @@
         }
     }
 
-    @RequiresFlagsEnabled(FLAG_PIC_CACHE_NULLS)
     @Test
     public void testCachingNulls() {
         TestCache cache = new TestCache(new Args(MODULE_TEST)
diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
index bb8356f..fc04e64 100644
--- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java
+++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
@@ -16,7 +16,6 @@
 
 package android.os;
 
-import static android.app.Flags.FLAG_PIC_CACHE_NULLS;
 import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID;
 
 import static org.junit.Assert.assertEquals;
@@ -511,7 +510,6 @@
         IpcDataCache.setTestMode(true);
     }
 
-    @RequiresFlagsEnabled(FLAG_PIC_CACHE_NULLS)
     @Test
     public void testCachingNulls() {
         IpcDataCache.Config c =
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index 5f25e93..c058885 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -931,6 +931,545 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testRepeating_withWaveformEnvelopeEffect_allSucceed() throws Exception {
+        VibrationEffect preamble = new VibrationEffect.WaveformEnvelopeBuilder()
+                .addControlPoint(0.1f, 50f, 10)
+                .addControlPoint(0.2f, 60f, 20)
+                .build();
+        VibrationEffect repeating = new VibrationEffect.WaveformEnvelopeBuilder()
+                .setInitialFrequencyHz(70f)
+                .addControlPoint(0.3f, 80f, 25)
+                .addControlPoint(0.4f, 90f, 30)
+                .build();
+        VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+        String xml = """
+                <vibration-effect>
+                <repeating-effect>
+                    <preamble>
+                        <waveform-envelope-effect>
+                            <control-point amplitude="0.1" frequencyHz="50.0" durationMs="10"/>
+                            <control-point amplitude="0.2" frequencyHz="60.0" durationMs="20"/>
+                        </waveform-envelope-effect>
+                    </preamble>
+                    <repeating>
+                        <waveform-envelope-effect initialFrequencyHz="70.0">
+                            <control-point amplitude="0.3" frequencyHz="80.0" durationMs="25"/>
+                            <control-point amplitude="0.4" frequencyHz="90.0" durationMs="30"/>
+                        </waveform-envelope-effect>
+                    </repeating>
+                </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "0.1", "0.2", "0.3", "0.4", "50.0", "60.0",
+                "70.0", "80.0", "90.0", "10", "20", "25", "30");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "0.1", "0.2", "0.3", "0.4", "50.0", "60.0",
+                "70.0", "80.0", "90.0", "10", "20", "25", "30");
+        assertHiddenApisRoundTrip(effect);
+
+        effect = VibrationEffect.createRepeatingEffect(repeating);
+
+        xml = """
+                <vibration-effect>
+                <repeating-effect>
+                    <repeating>
+                        <waveform-envelope-effect initialFrequencyHz="70.0">
+                            <control-point amplitude="0.3" frequencyHz="80.0" durationMs="25"/>
+                            <control-point amplitude="0.4" frequencyHz="90.0" durationMs="30"/>
+                        </waveform-envelope-effect>
+                    </repeating>
+                </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "0.3", "0.4", "70.0", "80.0", "90.0", "25",
+                "30");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "0.3", "0.4", "70.0", "80.0", "90.0", "25",
+                "30");
+        assertHiddenApisRoundTrip(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testRepeating_withBasicEnvelopeEffect_allSucceed() throws Exception {
+        VibrationEffect preamble = new VibrationEffect.BasicEnvelopeBuilder()
+                .addControlPoint(0.1f, 0.1f, 10)
+                .addControlPoint(0.2f, 0.2f, 20)
+                .addControlPoint(0.0f, 0.2f, 20)
+                .build();
+        VibrationEffect repeating = new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(0.3f)
+                .addControlPoint(0.3f, 0.4f, 25)
+                .addControlPoint(0.4f, 0.6f, 30)
+                .addControlPoint(0.0f, 0.7f, 35)
+                .build();
+        VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+        String xml = """
+                <vibration-effect>
+                <repeating-effect>
+                    <preamble>
+                        <basic-envelope-effect>
+                            <control-point intensity="0.1" sharpness="0.1" durationMs="10" />
+                            <control-point intensity="0.2" sharpness="0.2" durationMs="20" />
+                            <control-point intensity="0.0" sharpness="0.2" durationMs="20" />
+                        </basic-envelope-effect>
+                    </preamble>
+                    <repeating>
+                        <basic-envelope-effect initialSharpness="0.3">
+                            <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+                            <control-point intensity="0.4" sharpness="0.6" durationMs="30" />
+                            <control-point intensity="0.0" sharpness="0.7" durationMs="35" />
+                        </basic-envelope-effect>
+                    </repeating>
+                </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "0.0", "0.1", "0.2", "0.3", "0.4", "0.1", "0.2",
+                "0.3", "0.4", "0.6", "0.7", "10", "20", "25", "30", "35");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "0.0", "0.1", "0.2", "0.3", "0.4", "0.1", "0.2",
+                "0.3", "0.4", "0.6", "0.7", "10", "20", "25", "30", "35");
+        assertHiddenApisRoundTrip(effect);
+
+        effect = VibrationEffect.createRepeatingEffect(repeating);
+
+        xml = """
+                <vibration-effect>
+                <repeating-effect>
+                    <repeating>
+                        <basic-envelope-effect initialSharpness="0.3">
+                            <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+                            <control-point intensity="0.4" sharpness="0.6" durationMs="30" />
+                            <control-point intensity="0.0" sharpness="0.7" durationMs="35" />
+                        </basic-envelope-effect>
+                    </repeating>
+                </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "0.3", "0.4", "0.0", "0.4", "0.6", "0.7", "25",
+                "30", "35");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "0.3", "0.4", "0.0", "0.4", "0.6", "0.7", "25",
+                "30", "35");
+        assertHiddenApisRoundTrip(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testRepeating_withPredefinedEffects_allSucceed() throws Exception {
+        for (Map.Entry<String, Integer> entry : createPublicPredefinedEffectsMap().entrySet()) {
+            VibrationEffect preamble = VibrationEffect.get(entry.getValue());
+            VibrationEffect repeating = VibrationEffect.get(entry.getValue());
+            VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+            String xml = String.format("""
+                    <vibration-effect>
+                        <repeating-effect>
+                            <preamble>
+                                <predefined-effect name="%s"/>
+                            </preamble>
+                            <repeating>
+                                <predefined-effect name="%s"/>
+                            </repeating>
+                        </repeating-effect>
+                    </vibration-effect>
+                    """,
+                    entry.getKey(), entry.getKey());
+
+            assertPublicApisParserSucceeds(xml, effect);
+            assertPublicApisSerializerSucceeds(effect, entry.getKey());
+            assertPublicApisRoundTrip(effect);
+
+            assertHiddenApisParserSucceeds(xml, effect);
+            assertHiddenApisSerializerSucceeds(effect, entry.getKey());
+            assertHiddenApisRoundTrip(effect);
+
+            effect = VibrationEffect.createRepeatingEffect(repeating);
+            xml = String.format("""
+                    <vibration-effect>
+                        <repeating-effect>
+                            <repeating>
+                                <predefined-effect name="%s"/>
+                            </repeating>
+                        </repeating-effect>
+                    </vibration-effect>
+                    """,
+                    entry.getKey());
+
+            assertPublicApisParserSucceeds(xml, effect);
+            assertPublicApisSerializerSucceeds(effect, entry.getKey());
+            assertPublicApisRoundTrip(effect);
+
+            assertHiddenApisParserSucceeds(xml, effect);
+            assertHiddenApisSerializerSucceeds(effect, entry.getKey());
+            assertHiddenApisRoundTrip(effect);
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testRepeating_withWaveformEntry_allSucceed() throws Exception {
+        VibrationEffect preamble = VibrationEffect.createWaveform(new long[]{123, 456, 789, 0},
+                new int[]{254, 1, 255, 0}, /* repeat= */ -1);
+        VibrationEffect repeating = VibrationEffect.createWaveform(new long[]{123, 456, 789, 0},
+                new int[]{254, 1, 255, 0}, /* repeat= */ -1);
+        VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+        String xml = """
+                <vibration-effect>
+                    <repeating-effect>
+                        <preamble>
+                            <waveform-entry durationMs="123" amplitude="254"/>
+                            <waveform-entry durationMs="456" amplitude="1"/>
+                            <waveform-entry durationMs="789" amplitude="255"/>
+                            <waveform-entry durationMs="0" amplitude="0"/>
+                        </preamble>
+                        <repeating>
+                            <waveform-entry durationMs="123" amplitude="254"/>
+                            <waveform-entry durationMs="456" amplitude="1"/>
+                            <waveform-entry durationMs="789" amplitude="255"/>
+                            <waveform-entry durationMs="0" amplitude="0"/>
+                        </repeating>
+                    </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
+        assertHiddenApisRoundTrip(effect);
+
+        xml = """
+                <vibration-effect>
+                    <repeating-effect>
+                        <repeating>
+                            <waveform-entry durationMs="123" amplitude="254"/>
+                            <waveform-entry durationMs="456" amplitude="1"/>
+                            <waveform-entry durationMs="789" amplitude="255"/>
+                            <waveform-entry durationMs="0" amplitude="0"/>
+                        </repeating>
+                    </repeating-effect>
+                </vibration-effect>
+                """;
+
+        effect = VibrationEffect.createRepeatingEffect(repeating);
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
+        assertHiddenApisRoundTrip(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testRepeating_withPrimitives_allSucceed() throws Exception {
+        VibrationEffect preamble = VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_CLICK)
+                .addPrimitive(PRIMITIVE_TICK, 0.2497f)
+                .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
+                .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
+                .compose();
+        VibrationEffect repeating = VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_CLICK)
+                .addPrimitive(PRIMITIVE_TICK, 0.2497f)
+                .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
+                .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
+                .compose();
+        VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+        String xml = """
+                <vibration-effect>
+                    <repeating-effect>
+                        <preamble>
+                            <primitive-effect name="click" />
+                            <primitive-effect name="tick" scale="0.2497" />
+                            <primitive-effect name="low_tick" delayMs="356" />
+                            <primitive-effect name="spin" scale="0.6364" delayMs="7" />
+                        </preamble>
+                        <repeating>
+                            <primitive-effect name="click" />
+                            <primitive-effect name="tick" scale="0.2497" />
+                            <primitive-effect name="low_tick" delayMs="356" />
+                            <primitive-effect name="spin" scale="0.6364" delayMs="7" />
+                        </repeating>
+                    </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
+        assertHiddenApisRoundTrip(effect);
+
+        repeating = VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_CLICK)
+                .addPrimitive(PRIMITIVE_TICK, 0.2497f)
+                .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
+                .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
+                .compose();
+        effect = VibrationEffect.createRepeatingEffect(repeating);
+
+        xml = """
+                <vibration-effect>
+                    <repeating-effect>
+                        <repeating>
+                            <primitive-effect name="click" />
+                            <primitive-effect name="tick" scale="0.2497" />
+                            <primitive-effect name="low_tick" delayMs="356" />
+                            <primitive-effect name="spin" scale="0.6364" delayMs="7" />
+                        </repeating>
+                    </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
+        assertHiddenApisRoundTrip(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testRepeating_withMixedVibrations_allSucceed() throws Exception {
+        VibrationEffect preamble = new VibrationEffect.WaveformEnvelopeBuilder()
+                .addControlPoint(0.1f, 50f, 10)
+                .build();
+        VibrationEffect repeating = VibrationEffect.get(VibrationEffect.EFFECT_TICK);
+        VibrationEffect effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+        String xml = """
+                    <vibration-effect>
+                        <repeating-effect>
+                            <preamble>
+                                <waveform-envelope-effect>
+                                <control-point amplitude="0.1" frequencyHz="50.0" durationMs="10"/>
+                                </waveform-envelope-effect>
+                            </preamble>
+                            <repeating>
+                                <predefined-effect name="tick"/>
+                            </repeating>
+                        </repeating-effect>
+                    </vibration-effect>
+                    """;
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "0.1", "50.0", "10", "tick");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "0.1", "50.0", "10", "tick");
+        assertHiddenApisRoundTrip(effect);
+
+        preamble = VibrationEffect.createWaveform(new long[]{123, 456},
+                new int[]{254, 1}, /* repeat= */ -1);
+        repeating = new VibrationEffect.BasicEnvelopeBuilder()
+                .addControlPoint(0.3f, 0.4f, 25)
+                .addControlPoint(0.0f, 0.5f, 30)
+                .build();
+        effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+        xml = """
+                <vibration-effect>
+                    <repeating-effect>
+                        <preamble>
+                            <waveform-entry durationMs="123" amplitude="254"/>
+                            <waveform-entry durationMs="456" amplitude="1"/>
+                        </preamble>
+                        <repeating>
+                        <basic-envelope-effect>
+                            <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+                            <control-point intensity="0.0" sharpness="0.5" durationMs="30" />
+                        </basic-envelope-effect>
+                        </repeating>
+                    </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "123", "456", "254", "1", "0.3", "0.0", "0.4",
+                "0.5", "25", "30");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "123", "456", "254", "1", "0.3", "0.0", "0.4",
+                "0.5", "25", "30");
+        assertHiddenApisRoundTrip(effect);
+
+        preamble = VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_CLICK)
+                .compose();
+        effect = VibrationEffect.createRepeatingEffect(preamble, repeating);
+
+        xml = """
+                <vibration-effect>
+                    <repeating-effect>
+                        <preamble>
+                            <primitive-effect name="click" />
+                        </preamble>
+                        <repeating>
+                            <basic-envelope-effect>
+                                <control-point intensity="0.3" sharpness="0.4" durationMs="25" />
+                                <control-point intensity="0.0" sharpness="0.5" durationMs="30" />
+                            </basic-envelope-effect>
+                        </repeating>
+                    </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, "click", "0.3", "0.4", "0.0", "0.5", "25", "30");
+        assertPublicApisRoundTrip(effect);
+
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, "click", "0.3", "0.4", "0.0", "0.5", "25", "30");
+        assertHiddenApisRoundTrip(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testRepeating_badXml_throwsException() throws IOException {
+        // Incomplete XML
+        assertParseElementFails("""
+                <vibration-effect>
+                <repeating-effect>
+                    <preamble>
+                        <primitive-effect name="click" />
+                    </preamble>
+                    <repeating>
+                        <primitive-effect name="click" />
+                """);
+
+        assertParseElementFails("""
+                <vibration-effect>
+                <repeating-effect>
+                        <primitive-effect name="click" />
+                    <repeating>
+                        <primitive-effect name="click" />
+                    </repeating>
+                </repeating-effect>
+                </vibration-effect>
+                """);
+
+        assertParseElementFails("""
+                <vibration-effect>
+                <repeating-effect>
+                    <preamble>
+                        <primitive-effect name="click" />
+                    </preamble>
+                        <primitive-effect name="click" />
+                </repeating-effect>
+                </vibration-effect>
+                """);
+
+        // Bad vibration XML
+        assertParseElementFails("""
+                <vibration-effect>
+                <repeating-effect>
+                    <repeating>
+                        <primitive-effect name="click" />
+                    </repeating>
+                    <preamble>
+                        <primitive-effect name="click" />
+                    </preamble>
+                </repeating-effect>
+                </vibration-effect>
+                """);
+
+        assertParseElementFails("""
+                <vibration-effect>
+                <repeating-effect>
+                    <repeating>
+                    <preamble>
+                        <primitive-effect name="click" />
+                    </preamble>
+                        <primitive-effect name="click" />
+                    </repeating>
+                </repeating-effect>
+                </vibration-effect>
+                """);
+
+        assertParseElementFails("""
+                <vibration-effect>
+                <repeating-effect>
+                    <preamble>
+                        <primitive-effect name="click" />
+                    <repeating>
+                        <primitive-effect name="click" />
+                    </repeating>
+                    </preamble>
+                </repeating-effect>
+                </vibration-effect>
+                """);
+
+        assertParseElementFails("""
+                <vibration-effect>
+                <repeating-effect>
+                    <primitive-effect name="click" />
+                    <primitive-effect name="click" />
+                </repeating-effect>
+                </vibration-effect>
+                """);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testRepeatingEffect_featureFlagDisabled_allFail() throws Exception {
+        VibrationEffect repeating = VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_CLICK)
+                .addPrimitive(PRIMITIVE_TICK, 0.2497f)
+                .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
+                .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
+                .compose();
+        VibrationEffect effect = VibrationEffect.createRepeatingEffect(repeating);
+
+        String xml = """
+                <vibration-effect>
+                    <repeating-effect>
+                        <repeating>
+                            <primitive-effect name="click" />
+                            <primitive-effect name="tick" scale="0.2497" />
+                            <primitive-effect name="low_tick" delayMs="356" />
+                            <primitive-effect name="spin" scale="0.6364" delayMs="7" />
+                        </repeating>
+                    </repeating-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserFails(xml);
+        assertPublicApisSerializerFails(effect);
+        assertHiddenApisParserFails(xml);
+        assertHiddenApisSerializerFails(effect);
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
     public void testVendorEffect_allSucceed() throws Exception {
         PersistableBundle vendorData = new PersistableBundle();
diff --git a/core/xsd/vibrator/vibration/schema/current.txt b/core/xsd/vibrator/vibration/schema/current.txt
index 29f8d19..89ca044 100644
--- a/core/xsd/vibrator/vibration/schema/current.txt
+++ b/core/xsd/vibrator/vibration/schema/current.txt
@@ -62,17 +62,41 @@
     enum_constant public static final com.android.internal.vibrator.persistence.PrimitiveEffectName tick;
   }
 
+  public class RepeatingEffect {
+    ctor public RepeatingEffect();
+    method public com.android.internal.vibrator.persistence.RepeatingEffectEntry getPreamble();
+    method public com.android.internal.vibrator.persistence.RepeatingEffectEntry getRepeating();
+    method public void setPreamble(com.android.internal.vibrator.persistence.RepeatingEffectEntry);
+    method public void setRepeating(com.android.internal.vibrator.persistence.RepeatingEffectEntry);
+  }
+
+  public class RepeatingEffectEntry {
+    ctor public RepeatingEffectEntry();
+    method public com.android.internal.vibrator.persistence.BasicEnvelopeEffect getBasicEnvelopeEffect_optional();
+    method public com.android.internal.vibrator.persistence.PredefinedEffect getPredefinedEffect_optional();
+    method public com.android.internal.vibrator.persistence.PrimitiveEffect getPrimitiveEffect_optional();
+    method public com.android.internal.vibrator.persistence.WaveformEntry getWaveformEntry_optional();
+    method public com.android.internal.vibrator.persistence.WaveformEnvelopeEffect getWaveformEnvelopeEffect_optional();
+    method public void setBasicEnvelopeEffect_optional(com.android.internal.vibrator.persistence.BasicEnvelopeEffect);
+    method public void setPredefinedEffect_optional(com.android.internal.vibrator.persistence.PredefinedEffect);
+    method public void setPrimitiveEffect_optional(com.android.internal.vibrator.persistence.PrimitiveEffect);
+    method public void setWaveformEntry_optional(com.android.internal.vibrator.persistence.WaveformEntry);
+    method public void setWaveformEnvelopeEffect_optional(com.android.internal.vibrator.persistence.WaveformEnvelopeEffect);
+  }
+
   public class VibrationEffect {
     ctor public VibrationEffect();
     method public com.android.internal.vibrator.persistence.BasicEnvelopeEffect getBasicEnvelopeEffect_optional();
     method public com.android.internal.vibrator.persistence.PredefinedEffect getPredefinedEffect_optional();
     method public com.android.internal.vibrator.persistence.PrimitiveEffect getPrimitiveEffect_optional();
+    method public com.android.internal.vibrator.persistence.RepeatingEffect getRepeatingEffect_optional();
     method public byte[] getVendorEffect_optional();
     method public com.android.internal.vibrator.persistence.WaveformEffect getWaveformEffect_optional();
     method public com.android.internal.vibrator.persistence.WaveformEnvelopeEffect getWaveformEnvelopeEffect_optional();
     method public void setBasicEnvelopeEffect_optional(com.android.internal.vibrator.persistence.BasicEnvelopeEffect);
     method public void setPredefinedEffect_optional(com.android.internal.vibrator.persistence.PredefinedEffect);
     method public void setPrimitiveEffect_optional(com.android.internal.vibrator.persistence.PrimitiveEffect);
+    method public void setRepeatingEffect_optional(com.android.internal.vibrator.persistence.RepeatingEffect);
     method public void setVendorEffect_optional(byte[]);
     method public void setWaveformEffect_optional(com.android.internal.vibrator.persistence.WaveformEffect);
     method public void setWaveformEnvelopeEffect_optional(com.android.internal.vibrator.persistence.WaveformEnvelopeEffect);
diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
index b4df2d1..57bcde7 100644
--- a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
+++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
@@ -60,6 +60,30 @@
             <!-- Basic envelope effect -->
             <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
 
+            <!-- Repeating vibration effect -->
+            <xs:element name="repeating-effect" type="RepeatingEffect"/>
+
+        </xs:choice>
+    </xs:complexType>
+
+    <xs:complexType name="RepeatingEffect">
+        <xs:sequence>
+            <xs:element name="preamble" maxOccurs="1" minOccurs="0" type="RepeatingEffectEntry" />
+            <xs:element name="repeating" maxOccurs="1" minOccurs="1" type="RepeatingEffectEntry" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="RepeatingEffectEntry">
+        <xs:choice>
+            <xs:element name="predefined-effect" type="PredefinedEffect" />
+            <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect" />
+            <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect" />
+            <xs:sequence>
+                <xs:element name="waveform-entry" type="WaveformEntry" />
+            </xs:sequence>
+            <xs:sequence>
+                <xs:element name="primitive-effect" type="PrimitiveEffect" />
+            </xs:sequence>
         </xs:choice>
     </xs:complexType>
 
diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd
index fba966f..c11fb66 100644
--- a/core/xsd/vibrator/vibration/vibration.xsd
+++ b/core/xsd/vibrator/vibration/vibration.xsd
@@ -58,9 +58,34 @@
             <!-- Basic envelope effect -->
             <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
 
+            <!-- Repeating vibration effect -->
+            <xs:element name="repeating-effect" type="RepeatingEffect"/>
+
         </xs:choice>
     </xs:complexType>
 
+    <xs:complexType name="RepeatingEffect">
+        <xs:sequence>
+            <xs:element name="preamble" maxOccurs="1" minOccurs="0" type="RepeatingEffectEntry" />
+            <xs:element name="repeating" maxOccurs="1" minOccurs="1" type="RepeatingEffectEntry" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="RepeatingEffectEntry">
+        <xs:choice>
+            <xs:element name="predefined-effect" type="PredefinedEffect" />
+            <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect" />
+            <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect" />
+            <xs:sequence>
+                <xs:element name="waveform-entry" type="WaveformEntry" />
+            </xs:sequence>
+            <xs:sequence>
+                <xs:element name="primitive-effect" type="PrimitiveEffect" />
+            </xs:sequence>
+        </xs:choice>
+    </xs:complexType>
+
+
     <xs:complexType name="WaveformEffect">
         <xs:sequence>
 
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 2c542ec..2398e71 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -679,4 +679,8 @@
         <permission name="android.permission.BATTERY_STATS"/>
         <permission name="android.permission.ENTER_TRADE_IN_MODE"/>
     </privapp-permissions>
+
+    <privapp-permissions package="com.android.wm.shell">
+        <permission name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" />
+    </privapp-permissions>
 </permissions>
diff --git a/framework-jarjar-rules.txt b/framework-jarjar-rules.txt
index 087378b..3da69e8 100644
--- a/framework-jarjar-rules.txt
+++ b/framework-jarjar-rules.txt
@@ -10,3 +10,6 @@
 
 # For Perfetto proto dependencies
 rule perfetto.protos.** android.internal.perfetto.protos.@1
+
+# For aconfig storage classes
+rule android.aconfig.storage.** android.internal.aconfig.storage.@1
diff --git a/libs/WindowManager/Shell/res/layout/desktop_header_maximize_menu_button_progress_indicator_layout.xml b/libs/WindowManager/Shell/res/layout/desktop_header_maximize_menu_button_progress_indicator_layout.xml
new file mode 100644
index 0000000..5a39c83
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_header_maximize_menu_button_progress_indicator_layout.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="44dp"
+    android:layout_height="40dp"
+    android:importantForAccessibility="noHideDescendants">
+    <ProgressBar
+        android:id="@+id/progress_bar"
+        style="?android:attr/progressBarStyleHorizontal"
+        android:progressDrawable="@drawable/circular_progress"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:indeterminate="false"
+        android:layout_marginHorizontal="6dp"
+        android:layout_marginVertical="4dp"
+        android:visibility="invisible"/>
+</FrameLayout>
diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
index b734d2d..059e9e1 100644
--- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
+++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
@@ -17,21 +17,14 @@
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
 
-    <FrameLayout
+    <ViewStub
+        android:id="@+id/stub_progress_bar_container"
+        android:inflatedId="@+id/inflatedProgressBarContainer"
+        android:layout="@layout/desktop_header_maximize_menu_button_progress_indicator_layout"
         android:layout_width="44dp"
         android:layout_height="40dp"
-        android:importantForAccessibility="noHideDescendants">
-        <ProgressBar
-            android:id="@+id/progress_bar"
-            style="?android:attr/progressBarStyleHorizontal"
-            android:progressDrawable="@drawable/circular_progress"
-            android:layout_width="32dp"
-            android:layout_height="32dp"
-            android:indeterminate="false"
-            android:layout_marginHorizontal="6dp"
-            android:layout_marginVertical="4dp"
-            android:visibility="invisible"/>
-    </FrameLayout>
+        android:importantForAccessibility="noHideDescendants"
+        />
 
     <ImageButton
         android:id="@+id/maximize_window"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
index 68c42d6..06a55d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
@@ -24,7 +24,6 @@
 import android.content.Intent
 import android.content.Intent.ACTION_VIEW
 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.content.Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER
 import android.content.pm.PackageManager
 import android.content.pm.verify.domain.DomainVerificationManager
 import android.content.pm.verify.domain.DomainVerificationUserState
@@ -60,13 +59,15 @@
  * Returns intent if there is a browser application available to handle the uri. Otherwise, returns
  * null.
  */
-fun getBrowserIntent(uri: Uri, packageManager: PackageManager): Intent? {
+fun getBrowserIntent(uri: Uri, packageManager: PackageManager, userId: Int): Intent? {
     val intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER)
         .setData(uri)
         .addFlags(FLAG_ACTIVITY_NEW_TASK)
-    // If there is no browser application available to handle intent, return null
-    val component = intent.resolveActivity(packageManager) ?: return null
-    intent.setComponent(component)
+    // If there is a browser application available to handle the intent, return the intent.
+    // Otherwise, return null.
+    val resolveInfo = packageManager.resolveActivityAsUser(intent, /* flags= */ 0, userId)
+        ?: return null
+    intent.setComponent(resolveInfo.componentInfo.componentName)
     return intent
 }
 
@@ -74,14 +75,17 @@
  * Returns intent if there is a non-browser application available to handle the uri. Otherwise,
  * returns null.
  */
-fun getAppIntent(uri: Uri, packageManager: PackageManager): Intent? {
-    val intent = Intent(ACTION_VIEW, uri).apply {
-        flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_REQUIRE_NON_BROWSER
+fun getAppIntent(uri: Uri, packageManager: PackageManager, userId: Int): Intent? {
+    val intent = Intent(ACTION_VIEW, uri).addFlags(FLAG_ACTIVITY_NEW_TASK)
+    val resolveInfo = packageManager.resolveActivityAsUser(intent, /* flags= */ 0, userId)
+        ?: return null
+    // If there is a non-browser application available to handle the intent, return the intent.
+    // Otherwise, return null.
+     if (resolveInfo.activityInfo != null && !resolveInfo.handleAllWebDataURI) {
+        intent.setComponent(resolveInfo.componentInfo.componentName)
+        return intent
     }
-    // If there is no application available to handle intent, return null
-    val component = intent.resolveActivity(packageManager) ?: return null
-    intent.setComponent(component)
-    return intent
+    return null
 }
 
 /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index bec73a1..9aba3aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -2072,10 +2072,7 @@
 
         @Override
         public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
-            if (mLayerView != null) {
-                // TODO (b/273316505) handle suppression changes, although might not need to
-                //  to do anything on the layerview side for this...
-            }
+            // Nothing to do for our views, handled by launcher / in the bubble bar.
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 10054a1..94a6e58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -115,6 +115,7 @@
             PipTransitionState pipTransitionState,
             PipTouchHandler pipTouchHandler,
             PipAppOpsListener pipAppOpsListener,
+            PhonePipMenuController pipMenuController,
             @ShellMainThread ShellExecutor mainExecutor) {
         if (!PipUtils.isPip2ExperimentEnabled()) {
             return Optional.empty();
@@ -123,7 +124,8 @@
                     context, shellInit, shellCommandHandler, shellController, displayController,
                     displayInsetsController, pipBoundsState, pipBoundsAlgorithm,
                     pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer,
-                    pipTransitionState, pipTouchHandler, pipAppOpsListener, mainExecutor));
+                    pipTransitionState, pipTouchHandler, pipAppOpsListener, pipMenuController,
+                    mainExecutor));
         }
     }
 
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 3cc4776..9c3e815 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
@@ -781,6 +781,7 @@
                 // cancel any running animator, as it is using stale display layout information
                 animator.cancel();
             }
+            mMenuController.hideMenu();
             onDisplayChangedUncheck(layout, saveRestoreSnapFraction);
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index a849b9d..8c6d5f5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -97,6 +97,7 @@
     private final PipTransitionState mPipTransitionState;
     private final PipTouchHandler mPipTouchHandler;
     private final PipAppOpsListener mPipAppOpsListener;
+    private final PhonePipMenuController mPipMenuController;
     private final ShellExecutor mMainExecutor;
     private final PipImpl mImpl;
     private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>();
@@ -141,6 +142,7 @@
             PipTransitionState pipTransitionState,
             PipTouchHandler pipTouchHandler,
             PipAppOpsListener pipAppOpsListener,
+            PhonePipMenuController pipMenuController,
             ShellExecutor mainExecutor) {
         mContext = context;
         mShellCommandHandler = shellCommandHandler;
@@ -157,6 +159,7 @@
         mPipTransitionState.addPipTransitionStateChangedListener(this);
         mPipTouchHandler = pipTouchHandler;
         mPipAppOpsListener = pipAppOpsListener;
+        mPipMenuController = pipMenuController;
         mMainExecutor = mainExecutor;
         mImpl = new PipImpl();
 
@@ -183,6 +186,7 @@
             PipTransitionState pipTransitionState,
             PipTouchHandler pipTouchHandler,
             PipAppOpsListener pipAppOpsListener,
+            PhonePipMenuController pipMenuController,
             ShellExecutor mainExecutor) {
         if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -192,7 +196,8 @@
         return new PipController(context, shellInit, shellCommandHandler, shellController,
                 displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm,
                 pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer,
-                pipTransitionState, pipTouchHandler, pipAppOpsListener, mainExecutor);
+                pipTransitionState, pipTouchHandler, pipAppOpsListener, pipMenuController,
+                mainExecutor);
     }
 
     public PipImpl getPipImpl() {
@@ -329,6 +334,7 @@
         }
 
         mPipTouchHandler.updateMinMaxSize(mPipBoundsState.getAspectRatio());
+        mPipMenuController.hideMenu();
 
         if (mPipTransitionState.isInFixedRotation()) {
             // Do not change the bounds when in fixed rotation, but do update the movement bounds
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 5dd49f0..39ed206 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -302,7 +302,8 @@
                 mTaskOrganizer, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider,
                 mMainExecutor, mMainHandler, mBgExecutor, mRecentTasksOptional,
-                mLaunchAdjacentController, mWindowDecorViewModel, mSplitState);
+                mLaunchAdjacentController, mWindowDecorViewModel, mSplitState,
+                mDesktopTasksController);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index d0c21c9..246760e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -142,6 +142,7 @@
 import com.android.wm.shell.common.split.SplitScreenUtils;
 import com.android.wm.shell.common.split.SplitState;
 import com.android.wm.shell.common.split.SplitWindowManager;
+import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.shared.TransactionPool;
@@ -222,6 +223,7 @@
     private final Optional<RecentTasksController> mRecentTasks;
     private final LaunchAdjacentController mLaunchAdjacentController;
     private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
+    private final Optional<DesktopTasksController> mDesktopTasksController;
     /** Singleton source of truth for the current state of split screen on this device. */
     private final SplitState mSplitState;
 
@@ -358,7 +360,8 @@
             ShellExecutor bgExecutor,
             Optional<RecentTasksController> recentTasks,
             LaunchAdjacentController launchAdjacentController,
-            Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState) {
+            Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState,
+            Optional<DesktopTasksController> desktopTasksController) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
@@ -371,6 +374,7 @@
         mLaunchAdjacentController = launchAdjacentController;
         mWindowDecorViewModel = windowDecorViewModel;
         mSplitState = splitState;
+        mDesktopTasksController = desktopTasksController;
 
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */);
 
@@ -443,7 +447,8 @@
             ShellExecutor bgExecutor,
             Optional<RecentTasksController> recentTasks,
             LaunchAdjacentController launchAdjacentController,
-            Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState) {
+            Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState,
+            Optional<DesktopTasksController> desktopTasksController) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
@@ -465,6 +470,7 @@
         mLaunchAdjacentController = launchAdjacentController;
         mWindowDecorViewModel = windowDecorViewModel;
         mSplitState = splitState;
+        mDesktopTasksController = desktopTasksController;
 
         mDisplayController.addDisplayWindowListener(this);
         transitions.addHandler(this);
@@ -2768,11 +2774,17 @@
         final @WindowManager.TransitionType int type = request.getType();
         final boolean isOpening = isOpeningType(type);
         final boolean inFullscreen = triggerTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
-        final boolean inDesktopMode = DesktopModeStatus.canEnterDesktopMode(mContext)
-                && triggerTask.getWindowingMode() == WINDOWING_MODE_FREEFORM;
+        final boolean inDesktopMode = mDesktopTasksController.isPresent()
+                && mDesktopTasksController.get().isDesktopModeShowing(mDisplayId);
+        final boolean isLaunchingDesktopTask = isOpening && DesktopModeStatus.canEnterDesktopMode(
+                mContext) && triggerTask.getWindowingMode() == WINDOWING_MODE_FREEFORM;
         final StageTaskListener stage = getStageOfTask(triggerTask);
 
-        if (isOpening && inFullscreen) {
+        if (inDesktopMode || isLaunchingDesktopTask) {
+            // Don't handle request when desktop mode is showing (since they don't coexist), or
+            // when launching a desktop task (defer to DesktopTasksController)
+            return null;
+        } else if (isOpening && inFullscreen) {
             // One task is opening into fullscreen mode, remove the corresponding split record.
             mRecentTasks.ifPresent(recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId));
             logExit(EXIT_REASON_FULLSCREEN_REQUEST);
@@ -2824,12 +2836,6 @@
                     mSplitTransitions.setDismissTransition(transition, stageType,
                             EXIT_REASON_FULLSCREEN_REQUEST);
                 }
-            } else if (isOpening && inDesktopMode) {
-                // If the app being opened is in Desktop mode, set it to full screen and dismiss
-                // split screen stage.
-                prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out);
-                out.setWindowingMode(triggerTask.token, WINDOWING_MODE_UNDEFINED)
-                        .setBounds(triggerTask.token, null);
             } else if (isOpening && inFullscreen) {
                 final int activityType = triggerTask.getActivityType();
                 if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) {
@@ -2999,7 +3005,8 @@
                     mSplitLayout.update(startTransaction, false /* resetImePosition */);
                 }
 
-                if (mMixedHandler.isEnteringPip(change, transitType)) {
+                if (mMixedHandler.isEnteringPip(change, transitType)
+                        && getSplitItemStage(change.getLastParent()) != STAGE_TYPE_UNDEFINED) {
                     pipChange = change;
                 }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
index a318bcf..9d85bea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
@@ -59,7 +59,7 @@
         super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController,
                 displayInsetsController, transitions, transactionPool, iconProvider,
                 mainExecutor, mainHandler, bgExecutor, recentTasks, launchAdjacentController,
-                Optional.empty(), splitState);
+                Optional.empty(), splitState, Optional.empty());
 
         mTvSplitMenuController = new TvSplitMenuController(context, this,
                 systemWindows, mainHandler);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index c385f9a..1c7e62b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -76,8 +76,8 @@
                 if (Flags.migratePredictiveBackTransition()) {
                     final boolean gestureToHomeTransition = isBackGesture
                             && TransitionUtil.isClosingType(info.getType());
-                    if (gestureToHomeTransition
-                            || (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode))) {
+                    if (gestureToHomeTransition || TransitionUtil.isClosingMode(mode)
+                            || (!isBackGesture && TransitionUtil.isOpeningMode(mode))) {
                         notifyHomeVisibilityChanged(gestureToHomeTransition
                                 || TransitionUtil.isOpeningType(mode));
                     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 0b91966..792f5ca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -468,7 +468,7 @@
                 case MotionEvent.ACTION_DOWN: {
                     mDragPointerId = e.getPointerId(0);
                     mDragPositioningCallback.onDragPositioningStart(
-                            0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
+                            0 /* ctrlType */, e.getDisplayId(), e.getRawX(0), e.getRawY(0));
                     mIsDragging = false;
                     return false;
                 }
@@ -481,6 +481,7 @@
                     if (decoration.isHandlingDragResize()) break;
                     final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                     mDragPositioningCallback.onDragPositioningMove(
+                            e.getDisplayId(),
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     mIsDragging = true;
                     return true;
@@ -492,6 +493,7 @@
                     }
                     final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                     final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd(
+                            e.getDisplayId(),
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(newTaskBounds,
                             mWindowDecorByTaskId.get(mTaskId).calculateValidDragArea());
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 5a05861..7928e5e 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
@@ -1140,7 +1140,7 @@
                     if (dragAllowed) {
                         mDragPointerId = e.getPointerId(0);
                         final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart(
-                                0 /* ctrlType */, e.getRawX(0),
+                                0 /* ctrlType */, e.getDisplayId(), e.getRawX(0),
                                 e.getRawY(0));
                         updateDragStatus(e.getActionMasked());
                         mOnDragStartInitialBounds.set(initialBounds);
@@ -1161,6 +1161,7 @@
                     }
                     final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                     final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningMove(
+                            e.getDisplayId(),
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     mDesktopTasksController.onDragPositioningMove(taskInfo,
                             decoration.mTaskSurface,
@@ -1191,6 +1192,7 @@
                             (int) (e.getRawX(dragPointerIdx) - e.getX(dragPointerIdx)),
                             (int) (e.getRawY(dragPointerIdx) - e.getY(dragPointerIdx)));
                     final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd(
+                            e.getDisplayId(),
                             e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                     // Tasks bounds haven't actually been updated (only its leash), so pass to
                     // DesktopTasksController to allow secondary transformations (i.e. snap resizing
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 febf566..0d1960a 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
@@ -617,14 +617,16 @@
         }
 
         if (browserLink == null) return null;
-        return AppToWebUtils.getBrowserIntent(browserLink, mContext.getPackageManager());
+        return AppToWebUtils.getBrowserIntent(browserLink, mContext.getPackageManager(),
+                mUserContext.getUserId());
 
     }
 
     @Nullable
     private Intent getAppLink() {
         return mWebUri == null ? null
-                : AppToWebUtils.getAppIntent(mWebUri, mContext.getPackageManager());
+                : AppToWebUtils.getAppIntent(mWebUri, mContext.getPackageManager(),
+                        mUserContext.getUserId());
     }
 
     private boolean isBrowserApp() {
@@ -779,12 +781,17 @@
         final Point position = new Point(mResult.mCaptionX, 0);
         if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId)
                 == SPLIT_POSITION_BOTTOM_OR_RIGHT
-                && mDisplayController.getDisplayLayout(mTaskInfo.displayId).isLandscape()
         ) {
-            // If this is the right split task, add left stage's width.
-            final Rect leftStageBounds = new Rect();
-            mSplitScreenController.getStageBounds(leftStageBounds, new Rect());
-            position.x += leftStageBounds.width();
+            if (mSplitScreenController.isLeftRightSplit()) {
+                // If this is the right split task, add left stage's width.
+                final Rect leftStageBounds = new Rect();
+                mSplitScreenController.getStageBounds(leftStageBounds, new Rect());
+                position.x += leftStageBounds.width();
+            } else {
+                final Rect bottomStageBounds = new Rect();
+                mSplitScreenController.getRefStageBounds(new Rect(), bottomStageBounds);
+                position.y += bottomStageBounds.top;
+            }
         }
         return position;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
index 421ffd9..3eebdb04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
@@ -41,25 +41,30 @@
      *
      * @param ctrlType {@link CtrlType} indicating the direction of resizing, use
      *                 {@code 0} to indicate it's a move
+     * @param displayId the ID of the display where the drag starts
      * @param x x coordinate in window decoration coordinate system where the drag starts
      * @param y y coordinate in window decoration coordinate system where the drag starts
      * @return the starting task bounds
      */
-    Rect onDragPositioningStart(@CtrlType int ctrlType, float x, float y);
+    Rect onDragPositioningStart(@CtrlType int ctrlType, int displayId, float x, float y);
 
     /**
      * Called when the pointer moves during a drag-resize or drag-move.
+     *
+     * @param displayId the ID of the display where the pointer is currently located
      * @param x x coordinate in window decoration coordinate system of the new pointer location
      * @param y y coordinate in window decoration coordinate system of the new pointer location
      * @return the updated task bounds
      */
-    Rect onDragPositioningMove(float x, float y);
+    Rect onDragPositioningMove(int displayId, float x, float y);
 
     /**
      * Called when a drag-resize or drag-move stops.
+     *
+     * @param displayId the ID of the display where the pointer is located when drag stops
      * @param x x coordinate in window decoration coordinate system where the drag resize stops
      * @param y y coordinate in window decoration coordinate system where the drag resize stops
      * @return the final bounds for the dragged task
      */
-    Rect onDragPositioningEnd(float x, float y);
+    Rect onDragPositioningEnd(int displayId, float x, float y);
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index a6d503d..7d1471f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -454,7 +454,7 @@
                         ProtoLog.d(WM_SHELL_DESKTOP_MODE,
                                 "%s: Handling action down, update ctrlType to %d", TAG, ctrlType);
                         mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType,
-                                rawX, rawY);
+                                e.getDisplayId(), rawX, rawY);
                         mLastMotionEventOnDown = e;
                         mResizeTrigger = (ctrlType == CTRL_TYPE_BOTTOM || ctrlType == CTRL_TYPE_TOP
                                 || ctrlType == CTRL_TYPE_RIGHT || ctrlType == CTRL_TYPE_LEFT)
@@ -489,7 +489,8 @@
                     }
                     final float rawX = e.getRawX(dragPointerIndex);
                     final float rawY = e.getRawY(dragPointerIndex);
-                    final Rect taskBounds = mCallback.onDragPositioningMove(rawX, rawY);
+                    final Rect taskBounds = mCallback.onDragPositioningMove(e.getDisplayId(),
+                            rawX, rawY);
                     updateInputSinkRegionForDrag(taskBounds);
                     result = true;
                     break;
@@ -505,7 +506,7 @@
                                     TAG, e.getActionMasked());
                             break;
                         }
-                        final Rect taskBounds = mCallback.onDragPositioningEnd(
+                        final Rect taskBounds = mCallback.onDragPositioningEnd(e.getDisplayId(),
                                 e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
                         // If taskBounds has changed, setGeometry will be called and update the
                         // sink region. Otherwise, we should revert it here.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index c8aff78..5b027f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -168,7 +168,10 @@
         return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
     }
 
-    static boolean isEdgeResizePermitted(@NonNull MotionEvent e) {
+    /**
+     * Whether resizing a window from the edge is permitted based on the motion event.
+     */
+    public static boolean isEdgeResizePermitted(@NonNull MotionEvent e) {
         if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) {
             return e.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
                     || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt
index 3885761..ab30d61 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt
@@ -47,10 +47,11 @@
     private var startingAspectRatio = 0f
     private var isTaskPortrait = false
 
-    override fun onDragPositioningStart(@CtrlType ctrlType: Int, x: Float, y: Float): Rect {
+    override fun onDragPositioningStart(
+        @CtrlType ctrlType: Int, displayId: Int, x: Float, y: Float): Rect {
         originalCtrlType = ctrlType
         if (!requiresFixedAspectRatio()) {
-            return super.onDragPositioningStart(originalCtrlType, x, y)
+            return super.onDragPositioningStart(originalCtrlType, displayId, x, y)
         }
 
         lastRepositionedBounds.set(getBounds(windowDecoration.mTaskInfo))
@@ -72,27 +73,27 @@
                     val verticalMidPoint = lastRepositionedBounds.top + (startingBoundHeight / 2)
                     edgeResizeCtrlType = originalCtrlType +
                             if (y < verticalMidPoint) CTRL_TYPE_TOP else CTRL_TYPE_BOTTOM
-                    super.onDragPositioningStart(edgeResizeCtrlType, x, y)
+                    super.onDragPositioningStart(edgeResizeCtrlType, displayId, x, y)
                 }
                 CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> {
                     val horizontalMidPoint = lastRepositionedBounds.left + (startingBoundWidth / 2)
                     edgeResizeCtrlType = originalCtrlType +
                             if (x < horizontalMidPoint) CTRL_TYPE_LEFT else CTRL_TYPE_RIGHT
-                    super.onDragPositioningStart(edgeResizeCtrlType, x, y)
+                    super.onDragPositioningStart(edgeResizeCtrlType, displayId, x, y)
                 }
                 // If resize is corner resize, no alteration to the ctrlType needs to be made.
                 else -> {
                     edgeResizeCtrlType = CTRL_TYPE_UNDEFINED
-                    super.onDragPositioningStart(originalCtrlType, x, y)
+                    super.onDragPositioningStart(originalCtrlType, displayId, x, y)
                 }
             }
         )
         return lastRepositionedBounds
     }
 
-    override fun onDragPositioningMove(x: Float, y: Float): Rect {
+    override fun onDragPositioningMove(displayId: Int, x: Float, y: Float): Rect {
         if (!requiresFixedAspectRatio()) {
-            return super.onDragPositioningMove(x, y)
+            return super.onDragPositioningMove(displayId, x, y)
         }
 
         val diffX = x - lastValidPoint.x
@@ -103,7 +104,7 @@
                     // Drag coordinate falls within valid region (90 - 180 degrees or 270- 360
                     // degrees from the corner the previous valid point). Allow resize with adjusted
                     // coordinates to maintain aspect ratio.
-                    lastRepositionedBounds.set(dragAdjustedMove(x, y))
+                    lastRepositionedBounds.set(dragAdjustedMove(displayId, x, y))
                 }
             }
             CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, CTRL_TYPE_TOP + CTRL_TYPE_RIGHT -> {
@@ -111,28 +112,28 @@
                     // Drag coordinate falls within valid region (180 - 270 degrees or 0 - 90
                     // degrees from the corner the previous valid point). Allow resize with adjusted
                     // coordinates to maintain aspect ratio.
-                    lastRepositionedBounds.set(dragAdjustedMove(x, y))
+                    lastRepositionedBounds.set(dragAdjustedMove(displayId, x, y))
                 }
             }
             CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> {
                 // If resize is on left or right edge, always adjust the y coordinate.
                 val adjustedY = getScaledChangeForY(x)
                 lastValidPoint.set(x, adjustedY)
-                lastRepositionedBounds.set(super.onDragPositioningMove(x, adjustedY))
+                lastRepositionedBounds.set(super.onDragPositioningMove(displayId, x, adjustedY))
             }
             CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> {
                 // If resize is on top or bottom edge, always adjust the x coordinate.
                 val adjustedX = getScaledChangeForX(y)
                 lastValidPoint.set(adjustedX, y)
-                lastRepositionedBounds.set(super.onDragPositioningMove(adjustedX, y))
+                lastRepositionedBounds.set(super.onDragPositioningMove(displayId, adjustedX, y))
             }
         }
         return lastRepositionedBounds
     }
 
-    override fun onDragPositioningEnd(x: Float, y: Float): Rect {
+    override fun onDragPositioningEnd(displayId: Int, x: Float, y: Float): Rect {
         if (!requiresFixedAspectRatio()) {
-            return super.onDragPositioningEnd(x, y)
+            return super.onDragPositioningEnd(displayId, x, y)
         }
 
         val diffX = x - lastValidPoint.x
@@ -144,55 +145,55 @@
                     // Drag coordinate falls within valid region (90 - 180 degrees or 270- 360
                     // degrees from the corner the previous valid point). End resize with adjusted
                     // coordinates to maintain aspect ratio.
-                    return dragAdjustedEnd(x, y)
+                    return dragAdjustedEnd(displayId, x, y)
                 }
                 // If end of resize is not within valid region, end resize from last valid
                 // coordinates.
-                return super.onDragPositioningEnd(lastValidPoint.x, lastValidPoint.y)
+                return super.onDragPositioningEnd(displayId, lastValidPoint.x, lastValidPoint.y)
             }
             CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, CTRL_TYPE_TOP + CTRL_TYPE_RIGHT -> {
                 if ((diffX > 0 && diffY < 0) || (diffX < 0 && diffY > 0)) {
                     // Drag coordinate falls within valid region (180 - 260 degrees or 0 - 90
                     // degrees from the corner the previous valid point). End resize with adjusted
                     // coordinates to maintain aspect ratio.
-                    return dragAdjustedEnd(x, y)
+                    return dragAdjustedEnd(displayId, x, y)
                 }
                 // If end of resize is not within valid region, end resize from last valid
                 // coordinates.
-                return super.onDragPositioningEnd(lastValidPoint.x, lastValidPoint.y)
+                return super.onDragPositioningEnd(displayId, lastValidPoint.x, lastValidPoint.y)
             }
             CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> {
                 // If resize is on left or right edge, always adjust the y coordinate.
-                return super.onDragPositioningEnd(x, getScaledChangeForY(x))
+                return super.onDragPositioningEnd(displayId, x, getScaledChangeForY(x))
             }
             CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> {
                 // If resize is on top or bottom edge, always adjust the x coordinate.
-                return super.onDragPositioningEnd(getScaledChangeForX(y), y)
+                return super.onDragPositioningEnd(displayId, getScaledChangeForX(y), y)
             }
             else -> {
-                return super.onDragPositioningEnd(x, y)
+                return super.onDragPositioningEnd(displayId, x, y)
             }
         }
     }
 
-    private fun dragAdjustedMove(x: Float, y: Float): Rect {
+    private fun dragAdjustedMove(displayId: Int, x: Float, y: Float): Rect {
         val absDiffX = abs(x - lastValidPoint.x)
         val absDiffY = abs(y - lastValidPoint.y)
         if (absDiffY < absDiffX) {
             lastValidPoint.set(getScaledChangeForX(y), y)
-            return super.onDragPositioningMove(getScaledChangeForX(y), y)
+            return super.onDragPositioningMove(displayId, getScaledChangeForX(y), y)
         }
         lastValidPoint.set(x, getScaledChangeForY(x))
-        return super.onDragPositioningMove(x, getScaledChangeForY(x))
+        return super.onDragPositioningMove(displayId, x, getScaledChangeForY(x))
     }
 
-    private fun dragAdjustedEnd(x: Float, y: Float): Rect {
+    private fun dragAdjustedEnd(displayId: Int, x: Float, y: Float): Rect {
         val absDiffX = abs(x - lastValidPoint.x)
         val absDiffY = abs(y - lastValidPoint.y)
         if (absDiffY < absDiffX) {
-            return super.onDragPositioningEnd(getScaledChangeForX(y), y)
+            return super.onDragPositioningEnd(displayId, getScaledChangeForX(y), y)
         }
-        return super.onDragPositioningEnd(x, getScaledChangeForY(x))
+        return super.onDragPositioningEnd(displayId, x, getScaledChangeForY(x))
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index 3efae9d..2d6f745 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -91,7 +91,7 @@
     }
 
     @Override
-    public Rect onDragPositioningStart(int ctrlType, float x, float y) {
+    public Rect onDragPositioningStart(int ctrlType, int displayId, float x, float y) {
         mCtrlType = ctrlType;
         mTaskBoundsAtDragStart.set(
                 mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
@@ -117,7 +117,7 @@
     }
 
     @Override
-    public Rect onDragPositioningMove(float x, float y) {
+    public Rect onDragPositioningMove(int displayId, float x, float y) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint);
         if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType,
@@ -147,7 +147,7 @@
     }
 
     @Override
-    public Rect onDragPositioningEnd(float x, float y) {
+    public Rect onDragPositioningEnd(int displayId, float x, float y) {
         // If task has been resized or task was dragged into area outside of
         // mDisallowedAreaForEndBounds, apply WCT to finish it.
         if (isResizing() && mHasDragResized) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 159759e..bb19a2c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -694,7 +694,7 @@
                 setTextColor(style.textColor)
                 compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
             }
-
+            openByDefaultBtn.isGone = isBrowserApp
             openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor)
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
index 376cd2a..e23ebe6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
@@ -26,6 +26,7 @@
 import android.util.AttributeSet
 import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewStub
 import android.widget.FrameLayout
 import android.widget.ImageButton
 import android.widget.ProgressBar
@@ -46,13 +47,17 @@
     private val hoverProgressAnimatorSet = AnimatorSet()
     var hoverDisabled = false
 
-    private val progressBar: ProgressBar
+    private lateinit var stubProgressBarContainer: ViewStub
     private val maximizeWindow: ImageButton
+    private val progressBar: ProgressBar by lazy {
+        (stubProgressBarContainer.inflate() as FrameLayout)
+            .requireViewById(R.id.progress_bar)
+    }
 
     init {
         LayoutInflater.from(context).inflate(R.layout.maximize_menu_button, this, true)
 
-        progressBar = requireViewById(R.id.progress_bar)
+        stubProgressBarContainer = requireViewById(R.id.stub_progress_bar_container)
         maximizeWindow = requireViewById(R.id.maximize_window)
     }
 
@@ -115,21 +120,34 @@
             requireNotNull(rippleDrawable) { "Ripple drawable must be non-null" }
             maximizeWindow.imageTintList = iconForegroundColor
             maximizeWindow.background = rippleDrawable
-            progressBar.progressTintList = ColorStateList.valueOf(baseForegroundColor)
-                .withAlpha(OPACITY_15)
-            progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT)
-        } else {
-            if (darkMode) {
-                progressBar.progressTintList = ColorStateList.valueOf(
-                    resources.getColor(R.color.desktop_mode_maximize_menu_progress_dark))
-                maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
-                    R.color.desktop_mode_caption_button_color_selector_dark))
-            } else {
-                progressBar.progressTintList = ColorStateList.valueOf(
-                    resources.getColor(R.color.desktop_mode_maximize_menu_progress_light))
-                maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
-                    R.color.desktop_mode_caption_button_color_selector_light))
+            stubProgressBarContainer.setOnInflateListener { _, inflated ->
+                val progressBar = (inflated as FrameLayout)
+                    .requireViewById(R.id.progress_bar) as ProgressBar
+                progressBar.progressTintList = ColorStateList.valueOf(baseForegroundColor)
+                    .withAlpha(OPACITY_15)
+                progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT)
             }
+        } else {
+            val progressTint = if (darkMode) {
+                ColorStateList.valueOf(
+                    resources.getColor(R.color.desktop_mode_maximize_menu_progress_dark))
+            } else {
+                ColorStateList.valueOf(
+                    resources.getColor(R.color.desktop_mode_maximize_menu_progress_light))
+            }
+            val backgroundTint = if (darkMode) {
+                ContextCompat.getColorStateList(context,
+                    R.color.desktop_mode_caption_button_color_selector_dark)
+            } else {
+                ContextCompat.getColorStateList(context,
+                    R.color.desktop_mode_caption_button_color_selector_light)
+            }
+            stubProgressBarContainer.setOnInflateListener { _, inflated ->
+                val progressBar = (inflated as FrameLayout)
+                    .requireViewById(R.id.progress_bar) as ProgressBar
+                progressBar.progressTintList = progressTint
+            }
+            maximizeWindow.background?.setTintList(backgroundTint)
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 1f03d75..e011cc0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -104,7 +104,7 @@
     }
 
     @Override
-    public Rect onDragPositioningStart(int ctrlType, float x, float y) {
+    public Rect onDragPositioningStart(int ctrlType, int displayId, float x, float y) {
         mCtrlType = ctrlType;
         mTaskBoundsAtDragStart.set(
                 mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
@@ -136,7 +136,7 @@
     }
 
     @Override
-    public Rect onDragPositioningMove(float x, float y) {
+    public Rect onDragPositioningMove(int displayId, float x, float y) {
         if (Looper.myLooper() != mHandler.getLooper()) {
             // This method must run on the shell main thread to use the correct Choreographer
             // instance below.
@@ -170,7 +170,7 @@
     }
 
     @Override
-    public Rect onDragPositioningEnd(float x, float y) {
+    public Rect onDragPositioningEnd(int displayId, float x, float y) {
         PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y,
                 mRepositionStartPoint);
         if (isResizing()) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt
index d52fd4f..3ab7b34 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt
@@ -20,7 +20,6 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.internal.R
-import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -35,7 +34,7 @@
  */
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
-class AppCompatUtilsTest : ShellTestCase() {
+class AppCompatUtilsTest : CompatUIShellTestCase() {
     @Test
     fun testIsTopActivityExemptFromDesktopWindowing_onlyTransparentActivitiesInStack() {
         assertTrue(isTopActivityExemptFromDesktopWindowing(mContext,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 67573da..ecf766d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -39,9 +39,6 @@
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.view.InsetsSource;
 import android.view.InsetsState;
@@ -52,7 +49,6 @@
 
 import com.android.window.flags.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
@@ -70,11 +66,8 @@
 
 import dagger.Lazy;
 
-import java.util.Optional;
-
 import org.junit.Assert;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -82,25 +75,20 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 /**
  * Tests for {@link CompatUIController}.
  *
  * Build/Install/Run:
- *  atest WMShellUnitTests:CompatUIControllerTest
+ * atest WMShellUnitTests:CompatUIControllerTest
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class CompatUIControllerTest extends ShellTestCase {
+public class CompatUIControllerTest extends CompatUIShellTestCase {
     private static final int DISPLAY_ID = 0;
     private static final int TASK_ID = 12;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
-    @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
     private CompatUIController mController;
     private ShellInit mShellInit;
     @Mock
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index e5d1919..2117b06 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -26,8 +26,6 @@
 import android.app.TaskInfo;
 import android.graphics.Rect;
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
 import android.view.LayoutInflater;
@@ -40,7 +38,6 @@
 import com.android.window.flags.Flags;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
@@ -49,7 +46,6 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -66,14 +62,10 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class CompatUILayoutTest extends ShellTestCase {
+public class CompatUILayoutTest extends CompatUIShellTestCase {
 
     private static final int TASK_ID = 1;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @Mock private SyncTransactionQueue mSyncTransactionQueue;
     @Mock private Consumer<CompatUIEvent> mCallback;
     @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java
new file mode 100644
index 0000000..5a49d01
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java
@@ -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.wm.shell.compatui;
+
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.wm.shell.ShellTestCase;
+
+import org.junit.Rule;
+
+/**
+ * Base class for CompatUI tests.
+ */
+public class CompatUIShellTestCase extends ShellTestCase {
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java
index 8fd7c0e..0b37648 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java
@@ -27,8 +27,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.wm.shell.ShellTestCase;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -44,7 +42,7 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class CompatUIStatusManagerTest extends ShellTestCase {
+public class CompatUIStatusManagerTest extends CompatUIShellTestCase {
 
     private FakeCompatUIStatusManagerTest mTestState;
     private CompatUIStatusManager mStatusManager;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index 1c01756..61b6d80 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.compatui;
 
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
 
@@ -40,9 +39,6 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
 import android.view.DisplayInfo;
@@ -56,7 +52,6 @@
 
 import com.android.window.flags.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
@@ -65,7 +60,6 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -82,13 +76,7 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class CompatUIWindowManagerTest extends ShellTestCase {
-    @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
+public class CompatUIWindowManagerTest extends CompatUIShellTestCase {
 
     private static final int TASK_ID = 1;
     private static final int TASK_WIDTH = 2000;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java
index e8191db..e786fef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java
@@ -25,8 +25,6 @@
 import static org.mockito.Mockito.verify;
 
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -34,10 +32,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTestCase;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -51,7 +47,7 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class LetterboxEduDialogLayoutTest extends ShellTestCase {
+public class LetterboxEduDialogLayoutTest extends CompatUIShellTestCase {
 
     @Mock
     private Runnable mDismissCallback;
@@ -60,10 +56,6 @@
     private View mDismissButton;
     private View mDialogContainer;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
index 4c97c76..09fc082 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
@@ -46,9 +46,6 @@
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
 import android.view.DisplayCutout;
@@ -65,7 +62,6 @@
 import com.android.window.flags.Flags;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.DockStateReader;
@@ -75,7 +71,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -95,7 +90,7 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class LetterboxEduWindowManagerTest extends ShellTestCase {
+public class LetterboxEduWindowManagerTest extends CompatUIShellTestCase {
 
     private static final int USER_ID_1 = 1;
     private static final int USER_ID_2 = 2;
@@ -128,18 +123,11 @@
     @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnDismissCallback;
     @Mock private DockStateReader mDockStateReader;
 
-    @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
     private CompatUIConfiguration mCompatUIConfiguration;
     private TestShellExecutor mExecutor;
     private FakeCompatUIStatusManagerTest mCompatUIStatus;
     private CompatUIStatusManager mCompatUIStatusManager;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java
index 0da14d6..02c099b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java
@@ -26,8 +26,6 @@
 
 import android.app.TaskInfo;
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.LayoutInflater;
@@ -36,10 +34,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTestCase;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -54,7 +50,7 @@
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class ReachabilityEduLayoutTest extends ShellTestCase {
+public class ReachabilityEduLayoutTest extends CompatUIShellTestCase {
 
     private ReachabilityEduLayout mLayout;
     private View mMoveUpButton;
@@ -68,10 +64,6 @@
     @Mock
     private TaskInfo mTaskInfo;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
index eafb414..fa04e07 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
@@ -25,14 +25,11 @@
 import android.app.TaskInfo;
 import android.content.res.Configuration;
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -40,7 +37,6 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -56,7 +52,7 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class ReachabilityEduWindowManagerTest extends ShellTestCase {
+public class ReachabilityEduWindowManagerTest extends CompatUIShellTestCase {
     @Mock
     private SyncTransactionQueue mSyncTransactionQueue;
     @Mock
@@ -71,10 +67,6 @@
     private TaskInfo mTaskInfo;
     private ReachabilityEduWindowManager mWindowManager;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
index 6b0c5dd..2cded9d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
@@ -26,8 +26,6 @@
 import static org.mockito.Mockito.verify;
 
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -36,10 +34,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTestCase;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -55,7 +51,7 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class RestartDialogLayoutTest extends ShellTestCase  {
+public class RestartDialogLayoutTest extends CompatUIShellTestCase  {
 
     @Mock private Runnable mDismissCallback;
     @Mock private Consumer<Boolean> mRestartCallback;
@@ -66,10 +62,6 @@
     private View mDialogContainer;
     private CheckBox mDontRepeatCheckBox;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
index cfeef90..ebd0f41 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
@@ -22,15 +22,12 @@
 import android.app.TaskInfo;
 import android.content.res.Configuration;
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.transition.Transitions;
@@ -38,7 +35,6 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -54,7 +50,7 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class RestartDialogWindowManagerTest extends ShellTestCase {
+public class RestartDialogWindowManagerTest extends CompatUIShellTestCase {
 
     @Mock
     private SyncTransactionQueue mSyncTransactionQueue;
@@ -66,10 +62,6 @@
     private RestartDialogWindowManager mWindowManager;
     private TaskInfo mTaskInfo;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
index e8e68bd..c6532e1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
@@ -27,8 +27,6 @@
 import android.app.TaskInfo;
 import android.content.ComponentName;
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
 import android.view.LayoutInflater;
@@ -40,7 +38,6 @@
 
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -48,7 +45,6 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -66,7 +62,7 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-public class UserAspectRatioSettingsLayoutTest extends ShellTestCase {
+public class UserAspectRatioSettingsLayoutTest extends CompatUIShellTestCase {
 
     private static final int TASK_ID = 1;
 
@@ -88,10 +84,6 @@
     private UserAspectRatioSettingsLayout mLayout;
     private TaskInfo mTaskInfo;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
index 9f86d49..096e900 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
@@ -41,8 +41,6 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.Pair;
@@ -56,7 +54,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -65,7 +62,6 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -87,7 +83,7 @@
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 @SmallTest
-public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase {
+public class UserAspectRatioSettingsWindowManagerTest extends CompatUIShellTestCase {
 
     private static final int TASK_ID = 1;
 
@@ -112,10 +108,6 @@
 
     private TestShellExecutor mExecutor;
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
index 9b4cc17..db00f41 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
@@ -64,7 +64,7 @@
                 context,
                 testExecutor,
                 testExecutor,
-                transactionSupplier
+                transactionSupplier,
             )
     }
 
@@ -81,11 +81,11 @@
                 info =
                     createTransitionInfo(
                         type = WindowManager.TRANSIT_OPEN,
-                        task = createTask(WINDOWING_MODE_FREEFORM)
+                        task = createTask(WINDOWING_MODE_FREEFORM),
                     ),
                 startTransaction = mock(),
                 finishTransaction = mock(),
-                finishCallback = {}
+                finishCallback = {},
             )
 
         assertFalse("Should not animate open transition", animates)
@@ -99,7 +99,7 @@
                 info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)),
                 startTransaction = mock(),
                 finishTransaction = mock(),
-                finishCallback = {}
+                finishCallback = {},
             )
 
         assertFalse("Should not animate fullscreen task close transition", animates)
@@ -113,11 +113,11 @@
                 info =
                     createTransitionInfo(
                         changeMode = WindowManager.TRANSIT_OPEN,
-                        task = createTask(WINDOWING_MODE_FREEFORM)
+                        task = createTask(WINDOWING_MODE_FREEFORM),
                     ),
                 startTransaction = mock(),
                 finishTransaction = mock(),
-                finishCallback = {}
+                finishCallback = {},
             )
 
         assertFalse("Should not animate opening freeform task close transition", animates)
@@ -131,7 +131,7 @@
                 info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)),
                 startTransaction = mock(),
                 finishTransaction = mock(),
-                finishCallback = {}
+                finishCallback = {},
             )
 
         assertTrue("Should animate closing freeform task close transition", animates)
@@ -140,7 +140,7 @@
     private fun createTransitionInfo(
         type: Int = WindowManager.TRANSIT_CLOSE,
         changeMode: Int = WindowManager.TRANSIT_CLOSE,
-        task: RunningTaskInfo
+        task: RunningTaskInfo,
     ): TransitionInfo =
         TransitionInfo(type, 0 /* flags */).apply {
             addChange(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
index 4cc641c..ecad521 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
@@ -36,7 +36,6 @@
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
 import com.android.window.flags.Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE
-import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.common.ShellExecutor
@@ -47,6 +46,7 @@
 import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
 import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
 import junit.framework.Assert.assertEquals
@@ -129,16 +129,22 @@
                 persistentRepository,
                 repositoryInitializer,
                 testScope,
-                userManager
+                userManager,
             )
         whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
         whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
-        whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn(
-            Desktop.getDefaultInstance()
-        )
+        whenever(runBlocking { persistentRepository.readDesktop(any(), any()) })
+            .thenReturn(Desktop.getDefaultInstance())
 
-        handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer,
-            taskStackListener, resizeTransitionHandler, userRepositories)
+        handler =
+            DesktopActivityOrientationChangeHandler(
+                context,
+                shellInit,
+                shellTaskOrganizer,
+                taskStackListener,
+                resizeTransitionHandler,
+                userRepositories,
+            )
 
         shellInit.init()
     }
@@ -161,19 +167,28 @@
         whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
         clearInvocations(shellInit)
 
-        handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer,
-            taskStackListener, resizeTransitionHandler, userRepositories)
+        handler =
+            DesktopActivityOrientationChangeHandler(
+                context,
+                shellInit,
+                shellTaskOrganizer,
+                taskStackListener,
+                resizeTransitionHandler,
+                userRepositories,
+            )
 
-        verify(shellInit, never()).addInitCallback(any(),
-            any<DesktopActivityOrientationChangeHandler>())
+        verify(shellInit, never())
+            .addInitCallback(any(), any<DesktopActivityOrientationChangeHandler>())
     }
 
     @Test
     fun handleActivityOrientationChange_resizeable_doNothing() {
         val task = setUpFreeformTask()
 
-        taskStackListener.onActivityRequestedOrientationChanged(task.taskId,
-            SCREEN_ORIENTATION_LANDSCAPE)
+        taskStackListener.onActivityRequestedOrientationChanged(
+            task.taskId,
+            SCREEN_ORIENTATION_LANDSCAPE,
+        )
 
         verify(resizeTransitionHandler, never()).startTransition(any(), any())
     }
@@ -189,8 +204,10 @@
         userRepositories.current.addTask(DEFAULT_DISPLAY, task.taskId, isVisible = true)
         runningTasks.add(task)
 
-        taskStackListener.onActivityRequestedOrientationChanged(task.taskId,
-            SCREEN_ORIENTATION_LANDSCAPE)
+        taskStackListener.onActivityRequestedOrientationChanged(
+            task.taskId,
+            SCREEN_ORIENTATION_LANDSCAPE,
+        )
 
         verify(resizeTransitionHandler, never()).startTransition(any(), any())
     }
@@ -198,8 +215,11 @@
     @Test
     fun handleActivityOrientationChange_nonResizeablePortrait_requestSameOrientation_doNothing() {
         val task = setUpFreeformTask(isResizeable = false)
-        val newTask = setUpFreeformTask(isResizeable = false,
-            orientation = SCREEN_ORIENTATION_SENSOR_PORTRAIT)
+        val newTask =
+            setUpFreeformTask(
+                isResizeable = false,
+                orientation = SCREEN_ORIENTATION_SENSOR_PORTRAIT,
+            )
 
         handler.handleActivityOrientationChange(task, newTask)
 
@@ -211,8 +231,10 @@
         val task = setUpFreeformTask(isResizeable = false)
         userRepositories.current.updateTask(task.displayId, task.taskId, isVisible = false)
 
-        taskStackListener.onActivityRequestedOrientationChanged(task.taskId,
-            SCREEN_ORIENTATION_LANDSCAPE)
+        taskStackListener.onActivityRequestedOrientationChanged(
+            task.taskId,
+            SCREEN_ORIENTATION_LANDSCAPE,
+        )
 
         verify(resizeTransitionHandler, never()).startTransition(any(), any())
     }
@@ -221,8 +243,8 @@
     fun handleActivityOrientationChange_nonResizeablePortrait_respectLandscapeRequest() {
         val task = setUpFreeformTask(isResizeable = false)
         val oldBounds = task.configuration.windowConfiguration.bounds
-        val newTask = setUpFreeformTask(isResizeable = false,
-            orientation = SCREEN_ORIENTATION_LANDSCAPE)
+        val newTask =
+            setUpFreeformTask(isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE)
 
         handler.handleActivityOrientationChange(task, newTask)
 
@@ -242,9 +264,12 @@
     @Test
     fun handleActivityOrientationChange_nonResizeableLandscape_respectPortraitRequest() {
         val oldBounds = Rect(0, 0, 500, 200)
-        val task = setUpFreeformTask(
-            isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE, bounds = oldBounds
-        )
+        val task =
+            setUpFreeformTask(
+                isResizeable = false,
+                orientation = SCREEN_ORIENTATION_LANDSCAPE,
+                bounds = oldBounds,
+            )
         val newTask = setUpFreeformTask(isResizeable = false, bounds = oldBounds)
 
         handler.handleActivityOrientationChange(task, newTask)
@@ -266,7 +291,7 @@
         displayId: Int = DEFAULT_DISPLAY,
         isResizeable: Boolean = true,
         orientation: Int = SCREEN_ORIENTATION_PORTRAIT,
-        bounds: Rect? = Rect(0, 0, 200, 500)
+        bounds: Rect? = Rect(0, 0, 200, 500),
     ): RunningTaskInfo {
         val task = createFreeformTask(displayId, bounds)
         val activityInfo = ActivityInfo()
@@ -291,4 +316,4 @@
 
     private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
         wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
index 6df8d6f..d14c640 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
@@ -56,11 +56,7 @@
     @Before
     fun setUp() {
         handler =
-            DesktopBackNavigationTransitionHandler(
-                testExecutor,
-                testExecutor,
-                displayController
-            )
+            DesktopBackNavigationTransitionHandler(testExecutor, testExecutor, displayController)
         whenever(displayController.getDisplayContext(any())).thenReturn(mContext)
     }
 
@@ -75,13 +71,13 @@
             handler.startAnimation(
                 transition = mock(),
                 info =
-                createTransitionInfo(
-                    type = WindowManager.TRANSIT_OPEN,
-                    task = createTask(WINDOWING_MODE_FREEFORM)
-                ),
+                    createTransitionInfo(
+                        type = WindowManager.TRANSIT_OPEN,
+                        task = createTask(WINDOWING_MODE_FREEFORM),
+                    ),
                 startTransaction = mock(),
                 finishTransaction = mock(),
-                finishCallback = {}
+                finishCallback = {},
             )
 
         assertFalse("Should not animate open transition", animates)
@@ -95,7 +91,7 @@
                 info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)),
                 startTransaction = mock(),
                 finishTransaction = mock(),
-                finishCallback = {}
+                finishCallback = {},
             )
 
         assertFalse("Should not animate fullscreen task to back transition", animates)
@@ -107,13 +103,13 @@
             handler.startAnimation(
                 transition = mock(),
                 info =
-                createTransitionInfo(
-                    changeMode = WindowManager.TRANSIT_OPEN,
-                    task = createTask(WINDOWING_MODE_FREEFORM)
-                ),
+                    createTransitionInfo(
+                        changeMode = WindowManager.TRANSIT_OPEN,
+                        task = createTask(WINDOWING_MODE_FREEFORM),
+                    ),
                 startTransaction = mock(),
                 finishTransaction = mock(),
-                finishCallback = {}
+                finishCallback = {},
             )
 
         assertFalse("Should not animate opening freeform task to back transition", animates)
@@ -127,7 +123,7 @@
                 info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)),
                 startTransaction = mock(),
                 finishTransaction = mock(),
-                finishCallback = {}
+                finishCallback = {},
             )
 
         assertTrue("Should animate going to back freeform task close transition", animates)
@@ -138,22 +134,24 @@
         val animates =
             handler.startAnimation(
                 transition = mock(),
-                info = createTransitionInfo(
-                    type = TRANSIT_CLOSE,
-                    changeMode = TRANSIT_CLOSE,
-                    task = createTask(WINDOWING_MODE_FREEFORM)
-                ),
+                info =
+                    createTransitionInfo(
+                        type = TRANSIT_CLOSE,
+                        changeMode = TRANSIT_CLOSE,
+                        task = createTask(WINDOWING_MODE_FREEFORM),
+                    ),
                 startTransaction = mock(),
                 finishTransaction = mock(),
-                finishCallback = {}
+                finishCallback = {},
             )
 
         assertTrue("Should animate going to back freeform task close transition", animates)
     }
+
     private fun createTransitionInfo(
         type: Int = WindowManager.TRANSIT_TO_BACK,
         changeMode: Int = WindowManager.TRANSIT_TO_BACK,
-        task: RunningTaskInfo
+        task: RunningTaskInfo,
     ): TransitionInfo =
         TransitionInfo(type, 0 /* flags */).apply {
             addChange(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
index fea8236..6a37174 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
@@ -63,109 +63,115 @@
 @RunWith(AndroidTestingRunner::class)
 class DesktopDisplayEventHandlerTest : ShellTestCase() {
 
-  @Mock lateinit var testExecutor: ShellExecutor
-  @Mock lateinit var transitions: Transitions
-  @Mock lateinit var displayController: DisplayController
-  @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
-  @Mock private lateinit var mockWindowManager: IWindowManager
+    @Mock lateinit var testExecutor: ShellExecutor
+    @Mock lateinit var transitions: Transitions
+    @Mock lateinit var displayController: DisplayController
+    @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+    @Mock private lateinit var mockWindowManager: IWindowManager
 
-  private lateinit var shellInit: ShellInit
-  private lateinit var handler: DesktopDisplayEventHandler
+    private lateinit var shellInit: ShellInit
+    private lateinit var handler: DesktopDisplayEventHandler
 
-  @Before
-  fun setUp() {
-    shellInit = spy(ShellInit(testExecutor))
-    whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
-    val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
-    whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
-    handler =
-        DesktopDisplayEventHandler(
-            context,
-            shellInit,
-            transitions,
-            displayController,
-            rootTaskDisplayAreaOrganizer,
-            mockWindowManager,
+    @Before
+    fun setUp() {
+        shellInit = spy(ShellInit(testExecutor))
+        whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
+        val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
+        handler =
+            DesktopDisplayEventHandler(
+                context,
+                shellInit,
+                transitions,
+                displayController,
+                rootTaskDisplayAreaOrganizer,
+                mockWindowManager,
+            )
+        shellInit.init()
+    }
+
+    private fun testDisplayWindowingModeSwitch(
+        defaultWindowingMode: Int,
+        extendedDisplayEnabled: Boolean,
+        expectTransition: Boolean,
+    ) {
+        val externalDisplayId = 100
+        val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java)
+        verify(displayController).addDisplayWindowListener(captor.capture())
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = defaultWindowingMode
+        whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode }
+        val settingsSession =
+            ExtendedDisplaySettingsSession(
+                context.contentResolver,
+                if (extendedDisplayEnabled) 1 else 0,
+            )
+
+        settingsSession.use {
+            // The external display connected.
+            whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
+                .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId))
+            captor.value.onDisplayAdded(externalDisplayId)
+            tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+            // The external display disconnected.
+            whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
+                .thenReturn(intArrayOf(DEFAULT_DISPLAY))
+            captor.value.onDisplayRemoved(externalDisplayId)
+
+            if (expectTransition) {
+                val arg = argumentCaptor<WindowContainerTransaction>()
+                verify(transitions, times(2))
+                    .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
+                assertThat(arg.firstValue.changes[tda.token.asBinder()]?.windowingMode)
+                    .isEqualTo(WINDOWING_MODE_FREEFORM)
+                assertThat(arg.secondValue.changes[tda.token.asBinder()]?.windowingMode)
+                    .isEqualTo(defaultWindowingMode)
+            } else {
+                verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull())
+            }
+        }
+    }
+
+    @Test
+    fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() {
+        testDisplayWindowingModeSwitch(
+            defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            extendedDisplayEnabled = false,
+            expectTransition = false,
         )
-    shellInit.init()
-  }
-
-  private fun testDisplayWindowingModeSwitch(
-    defaultWindowingMode: Int,
-    extendedDisplayEnabled: Boolean,
-    expectTransition: Boolean
-  ) {
-    val externalDisplayId = 100
-    val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java)
-    verify(displayController).addDisplayWindowListener(captor.capture())
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = defaultWindowingMode
-    whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode }
-    val settingsSession = ExtendedDisplaySettingsSession(
-      context.contentResolver, if (extendedDisplayEnabled) 1 else 0)
-
-    settingsSession.use {
-      // The external display connected.
-      whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
-        .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId))
-      captor.value.onDisplayAdded(externalDisplayId)
-      tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
-      // The external display disconnected.
-      whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
-        .thenReturn(intArrayOf(DEFAULT_DISPLAY))
-      captor.value.onDisplayRemoved(externalDisplayId)
-
-      if (expectTransition) {
-        val arg = argumentCaptor<WindowContainerTransaction>()
-        verify(transitions, times(2)).startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
-        assertThat(arg.firstValue.changes[tda.token.asBinder()]?.windowingMode)
-          .isEqualTo(WINDOWING_MODE_FREEFORM)
-        assertThat(arg.secondValue.changes[tda.token.asBinder()]?.windowingMode)
-          .isEqualTo(defaultWindowingMode)
-      } else {
-        verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull())
-      }
     }
-  }
 
-  @Test
-  fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() {
-    testDisplayWindowingModeSwitch(
-      defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
-      extendedDisplayEnabled = false,
-      expectTransition = false
-    )
-  }
-
-  @Test
-  fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() {
-    testDisplayWindowingModeSwitch(
-      defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
-      extendedDisplayEnabled = true,
-      expectTransition = true
-    )
-  }
-
-  @Test
-  fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() {
-    testDisplayWindowingModeSwitch(
-      defaultWindowingMode = WINDOWING_MODE_FREEFORM,
-      extendedDisplayEnabled = true,
-      expectTransition = false
-    )
-  }
-
-  private class ExtendedDisplaySettingsSession(
-    private val contentResolver: ContentResolver,
-      private val overrideValue: Int
-  ) : AutoCloseable {
-    private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
-    private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0)
-
-    init { Settings.Global.putInt(contentResolver, settingName, overrideValue) }
-
-    override fun close() {
-      Settings.Global.putInt(contentResolver, settingName, initialValue)
+    @Test
+    fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() {
+        testDisplayWindowingModeSwitch(
+            defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            extendedDisplayEnabled = true,
+            expectTransition = true,
+        )
     }
-  }
-}
\ No newline at end of file
+
+    @Test
+    fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() {
+        testDisplayWindowingModeSwitch(
+            defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+            extendedDisplayEnabled = true,
+            expectTransition = false,
+        )
+    }
+
+    private class ExtendedDisplaySettingsSession(
+        private val contentResolver: ContentResolver,
+        private val overrideValue: Int,
+    ) : AutoCloseable {
+        private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
+        private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0)
+
+        init {
+            Settings.Global.putInt(contentResolver, settingName, overrideValue)
+        }
+
+        override fun close() {
+            Settings.Global.putInt(contentResolver, settingName, initialValue)
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
index b87f200..47d133b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
@@ -88,9 +88,16 @@
 
     @Before
     fun setUp() {
-        userRepositories = DesktopUserRepositories(
-            context, ShellInit(TestShellExecutor()), mock(), mock(), mock(), mock(), mock()
-        )
+        userRepositories =
+            DesktopUserRepositories(
+                context,
+                ShellInit(TestShellExecutor()),
+                mock(),
+                mock(),
+                mock(),
+                mock(),
+                mock(),
+            )
         whenever(mockDisplayController.getDisplayLayout(DEFAULT_DISPLAY))
             .thenReturn(mockDisplayLayout)
         whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { invocation ->
@@ -98,15 +105,16 @@
         }
         whenever(mockDisplayLayout.width()).thenReturn(DISPLAY_BOUNDS.width())
         whenever(mockDisplayLayout.height()).thenReturn(DISPLAY_BOUNDS.height())
-        controller = DesktopImmersiveController(
-            shellInit = mock(),
-            transitions = mockTransitions,
-            desktopUserRepositories = userRepositories,
-            displayController = mockDisplayController,
-            shellTaskOrganizer = mockShellTaskOrganizer,
-            shellCommandHandler = mock(),
-            transactionSupplier = transactionSupplier,
-        )
+        controller =
+            DesktopImmersiveController(
+                shellInit = mock(),
+                transitions = mockTransitions,
+                desktopUserRepositories = userRepositories,
+                displayController = mockDisplayController,
+                shellTaskOrganizer = mockShellTaskOrganizer,
+                shellCommandHandler = mock(),
+                transactionSupplier = transactionSupplier,
+            )
         desktopRepository = userRepositories.current
     }
 
@@ -119,15 +127,13 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = false
+            immersive = false,
         )
 
         controller.moveTaskToImmersive(task)
         controller.onTransitionReady(
             transition = mockBinder,
-            info = createTransitionInfo(
-                changes = listOf(createChange(task))
-            ),
+            info = createTransitionInfo(changes = listOf(createChange(task))),
             startTransaction = SurfaceControl.Transaction(),
             finishTransaction = SurfaceControl.Transaction(),
         )
@@ -145,16 +151,14 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = false
+            immersive = false,
         )
         assertThat(desktopRepository.removeBoundsBeforeFullImmersive(task.taskId)).isNull()
 
         controller.moveTaskToImmersive(task)
         controller.onTransitionReady(
             transition = mockBinder,
-            info = createTransitionInfo(
-                changes = listOf(createChange(task))
-            ),
+            info = createTransitionInfo(changes = listOf(createChange(task))),
             startTransaction = SurfaceControl.Transaction(),
             finishTransaction = SurfaceControl.Transaction(),
         )
@@ -171,15 +175,13 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
         controller.moveTaskToNonImmersive(task, USER_INTERACTION)
         controller.onTransitionReady(
             transition = mockBinder,
-            info = createTransitionInfo(
-                changes = listOf(createChange(task))
-            ),
+            info = createTransitionInfo(changes = listOf(createChange(task))),
             startTransaction = SurfaceControl.Transaction(),
             finishTransaction = SurfaceControl.Transaction(),
         )
@@ -197,16 +199,14 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600))
 
         controller.moveTaskToNonImmersive(task, USER_INTERACTION)
         controller.onTransitionReady(
             transition = mockBinder,
-            info = createTransitionInfo(
-                changes = listOf(createChange(task))
-            ),
+            info = createTransitionInfo(changes = listOf(createChange(task))),
             startTransaction = SurfaceControl.Transaction(),
             finishTransaction = SurfaceControl.Transaction(),
         )
@@ -220,16 +220,23 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
         controller.onTransitionReady(
             transition = mock(IBinder::class.java),
-            info = createTransitionInfo(
-                changes = listOf(createChange(task).apply {
-                    setRotation(/* start= */ Surface.ROTATION_0, /* end= */ Surface.ROTATION_90)
-                })
-            ),
+            info =
+                createTransitionInfo(
+                    changes =
+                        listOf(
+                            createChange(task).apply {
+                                setRotation(
+                                    /* start= */ Surface.ROTATION_0,
+                                    /* end= */ Surface.ROTATION_90,
+                                )
+                            }
+                        )
+                ),
             startTransaction = SurfaceControl.Transaction(),
             finishTransaction = SurfaceControl.Transaction(),
         )
@@ -247,8 +254,7 @@
         controller.moveTaskToImmersive(task)
         controller.moveTaskToImmersive(task)
 
-        verify(mockTransitions, times(1))
-            .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))
+        verify(mockTransitions, times(1)).startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))
     }
 
     @Test
@@ -261,8 +267,7 @@
         controller.moveTaskToNonImmersive(task, USER_INTERACTION)
         controller.moveTaskToNonImmersive(task, USER_INTERACTION)
 
-        verify(mockTransitions, times(1))
-            .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))
+        verify(mockTransitions, times(1)).startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))
     }
 
     @Test
@@ -275,7 +280,7 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
         controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
@@ -284,7 +289,7 @@
             transition = transition,
             taskId = task.taskId,
             direction = Direction.EXIT,
-            animate = false
+            animate = false,
         )
     }
 
@@ -298,7 +303,7 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = false
+            immersive = false,
         )
 
         controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
@@ -307,7 +312,7 @@
             transition = transition,
             taskId = task.taskId,
             direction = Direction.EXIT,
-            animate = false
+            animate = false,
         )
     }
 
@@ -321,7 +326,7 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
         controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
@@ -339,7 +344,7 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = false
+            immersive = false,
         )
 
         controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
@@ -357,21 +362,25 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
-        controller.exitImmersiveIfApplicable(
-            wct = wct,
-            displayId = DEFAULT_DISPLAY,
-            excludeTaskId = task.taskId,
-            reason = USER_INTERACTION,
-        ).asExit()?.runOnTransitionStart?.invoke(transition)
+        controller
+            .exitImmersiveIfApplicable(
+                wct = wct,
+                displayId = DEFAULT_DISPLAY,
+                excludeTaskId = task.taskId,
+                reason = USER_INTERACTION,
+            )
+            .asExit()
+            ?.runOnTransitionStart
+            ?.invoke(transition)
 
         assertTransitionNotPending(
             transition = transition,
             taskId = task.taskId,
             animate = false,
-            direction = Direction.EXIT
+            direction = Direction.EXIT,
         )
     }
 
@@ -384,7 +393,7 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
         controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION)
@@ -401,7 +410,7 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = false
+            immersive = false,
         )
 
         controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
@@ -419,17 +428,20 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
-        controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
-            .asExit()?.runOnTransitionStart?.invoke(transition)
+        controller
+            .exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
+            .asExit()
+            ?.runOnTransitionStart
+            ?.invoke(transition)
 
         assertTransitionPending(
             transition = transition,
             taskId = task.taskId,
             direction = Direction.EXIT,
-            animate = false
+            animate = false,
         )
     }
 
@@ -442,7 +454,7 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = false
+            immersive = false,
         )
 
         val result = controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
@@ -459,11 +471,16 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = false
+            immersive = false,
         )
 
-        val result = controller.exitImmersiveIfApplicable(
-            wct, task.displayId, excludeTaskId = null, USER_INTERACTION)
+        val result =
+            controller.exitImmersiveIfApplicable(
+                wct,
+                task.displayId,
+                excludeTaskId = null,
+                USER_INTERACTION,
+            )
 
         assertThat(result).isEqualTo(DesktopImmersiveController.ExitResult.NoExit)
     }
@@ -478,15 +495,13 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
         controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         controller.onTransitionReady(
             transition = transition,
-            info = createTransitionInfo(
-                changes = listOf(createChange(task))
-            ),
+            info = createTransitionInfo(changes = listOf(createChange(task))),
             startTransaction = SurfaceControl.Transaction(),
             finishTransaction = SurfaceControl.Transaction(),
         )
@@ -496,7 +511,7 @@
             transition = transition,
             taskId = task.taskId,
             direction = Direction.EXIT,
-            animate = false
+            animate = false,
         )
     }
 
@@ -511,15 +526,13 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
         controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         controller.onTransitionReady(
             transition = transition,
-            info = createTransitionInfo(
-                changes = listOf(createChange(task))
-            ),
+            info = createTransitionInfo(changes = listOf(createChange(task))),
             startTransaction = SurfaceControl.Transaction(),
             finishTransaction = SurfaceControl.Transaction(),
         )
@@ -530,13 +543,13 @@
             transition = transition,
             taskId = task.taskId,
             animate = false,
-            direction = Direction.EXIT
+            direction = Direction.EXIT,
         )
         assertTransitionNotPending(
             transition = mergedToTransition,
             taskId = task.taskId,
             animate = false,
-            direction = Direction.EXIT
+            direction = Direction.EXIT,
         )
     }
 
@@ -550,15 +563,13 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
         controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         controller.onTransitionReady(
             transition = transition,
-            info = createTransitionInfo(
-                changes = listOf(createChange(task))
-            ),
+            info = createTransitionInfo(changes = listOf(createChange(task))),
             startTransaction = SurfaceControl.Transaction(),
             finishTransaction = SurfaceControl.Transaction(),
         )
@@ -569,7 +580,7 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP,
-        Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE
+        Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE,
     )
     fun onTransitionReady_pendingExit_removesBoundsBeforeImmersive() {
         val task = createFreeformTask()
@@ -579,16 +590,14 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600))
         controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
 
         controller.onTransitionReady(
             transition = transition,
-            info = createTransitionInfo(
-                changes = listOf(createChange(task))
-            ),
+            info = createTransitionInfo(changes = listOf(createChange(task))),
             startTransaction = SurfaceControl.Transaction(),
             finishTransaction = SurfaceControl.Transaction(),
         )
@@ -606,20 +615,21 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
         controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION)
 
         assertThat(
-            wct.hasBoundsChange(task.token, calculateMaximizeBounds(mockDisplayLayout, task))
-        ).isTrue()
+                wct.hasBoundsChange(task.token, calculateMaximizeBounds(mockDisplayLayout, task))
+            )
+            .isTrue()
     }
 
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP,
-        Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE
+        Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE,
     )
     fun exitImmersiveIfApplicable_preImmersiveBoundsSaved_changesBoundsToPreImmersiveBounds() {
         val task = createFreeformTask()
@@ -628,23 +638,21 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
         val preImmersiveBounds = Rect(100, 100, 500, 500)
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, preImmersiveBounds)
 
         controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION)
 
-        assertThat(
-            wct.hasBoundsChange(task.token, preImmersiveBounds)
-        ).isTrue()
+        assertThat(wct.hasBoundsChange(task.token, preImmersiveBounds)).isTrue()
     }
 
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP,
         Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE,
-        Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS
+        Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS,
     )
     fun exitImmersiveIfApplicable_preImmersiveBoundsNotSaved_changesBoundsToInitialBounds() {
         val task = createFreeformTask()
@@ -653,14 +661,13 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
         controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION)
 
-        assertThat(
-            wct.hasBoundsChange(task.token, calculateInitialBounds(mockDisplayLayout, task))
-        ).isTrue()
+        assertThat(wct.hasBoundsChange(task.token, calculateInitialBounds(mockDisplayLayout, task)))
+            .isTrue()
     }
 
     @Test
@@ -672,10 +679,13 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
-        controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
-            .asExit()?.runOnTransitionStart?.invoke(Binder())
+        controller
+            .exitImmersiveIfApplicable(wct, task, USER_INTERACTION)
+            .asExit()
+            ?.runOnTransitionStart
+            ?.invoke(Binder())
 
         controller.moveTaskToNonImmersive(task, USER_INTERACTION)
 
@@ -693,7 +703,7 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = DEFAULT_DISPLAY,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
         controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION)
@@ -711,25 +721,23 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = true
+            immersive = true,
         )
 
         controller.moveTaskToNonImmersive(task, USER_INTERACTION)
 
         controller.animateResizeChange(
-            change = TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                taskInfo = task
-            },
+            change = TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task },
             startTransaction = StubTransaction(),
             finishTransaction = StubTransaction(),
-            finishCallback = { }
+            finishCallback = {},
         )
         animatorTestRule.advanceTimeBy(DesktopImmersiveController.FULL_IMMERSIVE_ANIM_DURATION_MS)
 
         assertTransitionPending(
             transition = mockBinder,
             taskId = task.taskId,
-            direction = Direction.EXIT
+            direction = Direction.EXIT,
         )
     }
 
@@ -743,7 +751,7 @@
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
             taskId = task.taskId,
-            immersive = false
+            immersive = false,
         )
 
         controller.moveTaskToImmersive(task)
@@ -753,13 +761,13 @@
             info = createTransitionInfo(changes = emptyList()),
             startTransaction = StubTransaction(),
             finishTransaction = StubTransaction(),
-            finishCallback = {}
+            finishCallback = {},
         )
 
         assertTransitionNotPending(
             transition = mockBinder,
             taskId = task.taskId,
-            direction = Direction.ENTER
+            direction = Direction.ENTER,
         )
     }
 
@@ -768,15 +776,18 @@
         taskId: Int,
         direction: Direction,
         animate: Boolean = true,
-        displayId: Int = DEFAULT_DISPLAY
+        displayId: Int = DEFAULT_DISPLAY,
     ) {
-        assertThat(controller.pendingImmersiveTransitions.any { pendingTransition ->
-            pendingTransition.transition == transition
-                    && pendingTransition.displayId == displayId
-                    && pendingTransition.taskId == taskId
-                    && pendingTransition.animate == animate
-                    && pendingTransition.direction == direction
-        }).isTrue()
+        assertThat(
+                controller.pendingImmersiveTransitions.any { pendingTransition ->
+                    pendingTransition.transition == transition &&
+                        pendingTransition.displayId == displayId &&
+                        pendingTransition.taskId == taskId &&
+                        pendingTransition.animate == animate &&
+                        pendingTransition.direction == direction
+                }
+            )
+            .isTrue()
     }
 
     private fun assertTransitionNotPending(
@@ -784,43 +795,44 @@
         taskId: Int,
         direction: Direction,
         animate: Boolean = true,
-        displayId: Int = DEFAULT_DISPLAY
+        displayId: Int = DEFAULT_DISPLAY,
     ) {
-        assertThat(controller.pendingImmersiveTransitions.any { pendingTransition ->
-            pendingTransition.transition == transition
-                    && pendingTransition.displayId == displayId
-                    && pendingTransition.taskId == taskId
-                    && pendingTransition.direction == direction
-        }).isFalse()
+        assertThat(
+                controller.pendingImmersiveTransitions.any { pendingTransition ->
+                    pendingTransition.transition == transition &&
+                        pendingTransition.displayId == displayId &&
+                        pendingTransition.taskId == taskId &&
+                        pendingTransition.direction == direction
+                }
+            )
+            .isFalse()
     }
 
     private fun createTransitionInfo(
         @TransitionType type: Int = TRANSIT_CHANGE,
         @TransitionFlags flags: Int = 0,
-        changes: List<TransitionInfo.Change> = emptyList()
-    ): TransitionInfo = TransitionInfo(type, flags).apply {
-        changes.forEach { change -> addChange(change) }
-    }
+        changes: List<TransitionInfo.Change> = emptyList(),
+    ): TransitionInfo =
+        TransitionInfo(type, flags).apply { changes.forEach { change -> addChange(change) } }
 
     private fun createChange(task: RunningTaskInfo): TransitionInfo.Change =
-        TransitionInfo.Change(task.token, SurfaceControl()).apply {
-            taskInfo = task
-        }
+        TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
 
     private fun WindowContainerTransaction.hasBoundsChange(token: WindowContainerToken): Boolean =
         this.changes.any { change ->
-            change.key == token.asBinder()
-                    && (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0
+            change.key == token.asBinder() &&
+                (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0
         }
 
     private fun WindowContainerTransaction.hasBoundsChange(
         token: WindowContainerToken,
         bounds: Rect,
-    ): Boolean = this.changes.any { change ->
-        change.key == token.asBinder()
-                && (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0
-                && change.value.configuration.windowConfiguration.bounds == bounds
-    }
+    ): Boolean =
+        this.changes.any { change ->
+            change.key == token.asBinder() &&
+                (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0 &&
+                change.value.configuration.windowConfiguration.bounds == bounds
+        }
 
     companion object {
         private val STABLE_BOUNDS = Rect(0, 100, 2000, 1900)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index 49a7e29..3cf84d9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -85,34 +85,22 @@
 
     @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
-    @Mock
-    lateinit var transitions: Transitions
-    @Mock
-    lateinit var userRepositories: DesktopUserRepositories
-    @Mock
-    lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
-    @Mock
-    lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
+    @Mock lateinit var transitions: Transitions
+    @Mock lateinit var userRepositories: DesktopUserRepositories
+    @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
+    @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
     @Mock
     lateinit var desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler
-    @Mock
-    lateinit var desktopImmersiveController: DesktopImmersiveController
-    @Mock
-    lateinit var interactionJankMonitor: InteractionJankMonitor
-    @Mock
-    lateinit var mockHandler: Handler
-    @Mock
-    lateinit var closingTaskLeash: SurfaceControl
-    @Mock
-    lateinit var shellInit: ShellInit
-    @Mock
-    lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
-    @Mock
-    private lateinit var desktopRepository: DesktopRepository
+    @Mock lateinit var desktopImmersiveController: DesktopImmersiveController
+    @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
+    @Mock lateinit var mockHandler: Handler
+    @Mock lateinit var closingTaskLeash: SurfaceControl
+    @Mock lateinit var shellInit: ShellInit
+    @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+    @Mock private lateinit var desktopRepository: DesktopRepository
 
     private lateinit var mixedHandler: DesktopMixedTransitionHandler
 
-
     @Before
     fun setUp() {
         whenever(userRepositories.current).thenReturn(desktopRepository)
@@ -157,11 +145,11 @@
     @Test
     @DisableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX,
+    )
     fun startRemoveTransition_callsFreeformTaskTransitionHandler() {
         val wct = WindowContainerTransaction()
-        whenever(freeformTaskTransitionHandler.startRemoveTransition(wct))
-            .thenReturn(mock())
+        whenever(freeformTaskTransitionHandler.startRemoveTransition(wct)).thenReturn(mock())
 
         mixedHandler.startRemoveTransition(wct)
 
@@ -171,7 +159,8 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX,
+    )
     fun startRemoveTransition_startsCloseTransition() {
         val wct = WindowContainerTransaction()
         whenever(transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler))
@@ -193,18 +182,19 @@
         val transitionInfo =
             createCloseTransitionInfo(
                 changeMode = TRANSIT_OPEN,
-                task = createTask(WINDOWING_MODE_FREEFORM)
+                task = createTask(WINDOWING_MODE_FREEFORM),
             )
         whenever(freeformTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any()))
             .thenReturn(true)
 
-        val started = mixedHandler.startAnimation(
-            transition = transition,
-            info = transitionInfo,
-            startTransaction = mock(),
-            finishTransaction = mock(),
-            finishCallback = {}
-        )
+        val started =
+            mixedHandler.startAnimation(
+                transition = transition,
+                info = transitionInfo,
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {},
+            )
 
         assertFalse("Should not start animation without closing desktop task", started)
     }
@@ -212,7 +202,8 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX,
+    )
     fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() {
         val wct = WindowContainerTransaction()
         val transition = mock<IBinder>()
@@ -225,13 +216,14 @@
             .thenReturn(transition)
         mixedHandler.startRemoveTransition(wct)
 
-        val started = mixedHandler.startAnimation(
-            transition = transition,
-            info = transitionInfo,
-            startTransaction = mock(),
-            finishTransaction = mock(),
-            finishCallback = {}
-        )
+        val started =
+            mixedHandler.startAnimation(
+                transition = transition,
+                info = transitionInfo,
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {},
+            )
 
         assertTrue("Should delegate animation to close transition handler", started)
         verify(closeDesktopTaskTransitionHandler)
@@ -241,12 +233,16 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX,
+    )
     fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() {
         val wct = WindowContainerTransaction()
         val transition = mock<IBinder>()
-        val transitionInfo = createCloseTransitionInfo(
-            task = createTask(WINDOWING_MODE_FREEFORM), withWallpaper = true)
+        val transitionInfo =
+            createCloseTransitionInfo(
+                task = createTask(WINDOWING_MODE_FREEFORM),
+                withWallpaper = true,
+            )
         whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any()))
             .thenReturn(mock())
         whenever(transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler))
@@ -258,7 +254,7 @@
             info = transitionInfo,
             startTransaction = mock(),
             finishTransaction = mock(),
-            finishCallback = {}
+            finishCallback = {},
         )
 
         verify(transitions)
@@ -268,14 +264,14 @@
                 any(),
                 any(),
                 any(),
-                eq(mixedHandler)
+                eq(mixedHandler),
             )
         verify(interactionJankMonitor)
             .begin(
                 closingTaskLeash,
                 context,
                 mockHandler,
-                CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
+                CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE,
             )
     }
 
@@ -283,7 +279,8 @@
     @DisableFlags(
         Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP,
         Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun startLaunchTransition_immersiveAndAppLaunchFlagsDisabled_doesNotUseMixedHandler() {
         val wct = WindowContainerTransaction()
         val task = createTask(WINDOWING_MODE_FREEFORM)
@@ -294,7 +291,7 @@
             transitionType = TRANSIT_OPEN,
             wct = wct,
             taskId = task.taskId,
-            exitingImmersiveTask = null
+            exitingImmersiveTask = null,
         )
 
         verify(transitions).startTransition(TRANSIT_OPEN, wct, /* handler= */ null)
@@ -312,7 +309,7 @@
             transitionType = TRANSIT_OPEN,
             wct = wct,
             taskId = task.taskId,
-            exitingImmersiveTask = null
+            exitingImmersiveTask = null,
         )
 
         verify(transitions).startTransition(TRANSIT_OPEN, wct, mixedHandler)
@@ -321,7 +318,8 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun startLaunchTransition_desktopAppLaunchEnabled_usesMixedHandler() {
         val wct = WindowContainerTransaction()
         val task = createTask(WINDOWING_MODE_FREEFORM)
@@ -332,7 +330,7 @@
             transitionType = TRANSIT_OPEN,
             wct = wct,
             taskId = task.taskId,
-            exitingImmersiveTask = null
+            exitingImmersiveTask = null,
         )
 
         verify(transitions).startTransition(TRANSIT_OPEN, wct, mixedHandler)
@@ -357,24 +355,22 @@
         val otherChange = createChange(createTask(WINDOWING_MODE_FREEFORM))
         mixedHandler.startAnimation(
             transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(launchTaskChange, otherChange)
-            ),
+            createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, otherChange)),
             SurfaceControl.Transaction(),
             SurfaceControl.Transaction(),
-        ) { }
+        ) {}
 
-        verify(transitions).dispatchTransition(
-            eq(transition),
-            argThat { info ->
-                info.changes.contains(launchTaskChange) && info.changes.contains(otherChange)
-            },
-            any(),
-            any(),
-            any(),
-            eq(mixedHandler),
-        )
+        verify(transitions)
+            .dispatchTransition(
+                eq(transition),
+                argThat { info ->
+                    info.changes.contains(launchTaskChange) && info.changes.contains(otherChange)
+                },
+                any(),
+                any(),
+                any(),
+                eq(mixedHandler),
+            )
     }
 
     @Test
@@ -397,32 +393,32 @@
         val immersiveChange = createChange(immersiveTask)
         mixedHandler.startAnimation(
             transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(launchTaskChange, immersiveChange)
-            ),
+            createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, immersiveChange)),
             SurfaceControl.Transaction(),
             SurfaceControl.Transaction(),
-        ) { }
+        ) {}
 
         verify(desktopImmersiveController)
             .animateResizeChange(eq(immersiveChange), any(), any(), any())
-        verify(transitions).dispatchTransition(
-            eq(transition),
-            argThat { info ->
-                info.changes.contains(launchTaskChange) && !info.changes.contains(immersiveChange)
-            },
-            any(),
-            any(),
-            any(),
-            eq(mixedHandler),
-        )
+        verify(transitions)
+            .dispatchTransition(
+                eq(transition),
+                argThat { info ->
+                    info.changes.contains(launchTaskChange) &&
+                        !info.changes.contains(immersiveChange)
+                },
+                any(),
+                any(),
+                any(),
+                eq(mixedHandler),
+            )
     }
 
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun startAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() {
         val wct = WindowContainerTransaction()
         val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
@@ -439,22 +435,19 @@
         )
         mixedHandler.startAnimation(
             transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(launchTaskChange)
-            ),
+            createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange)),
             SurfaceControl.Transaction(),
             SurfaceControl.Transaction(),
-        ) { }
+        ) {}
 
-        verify(rootTaskDisplayAreaOrganizer, times(0))
-            .reparentToDisplayArea(anyInt(), any(), any())
+        verify(rootTaskDisplayAreaOrganizer, times(0)).reparentToDisplayArea(anyInt(), any(), any())
     }
 
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun startAndAnimateLaunchTransition_withMinimizeChange_reparentsMinimizeChange() {
         val wct = WindowContainerTransaction()
         val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
@@ -473,22 +466,20 @@
         )
         mixedHandler.startAnimation(
             transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(launchTaskChange, minimizeChange)
-            ),
+            createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, minimizeChange)),
             SurfaceControl.Transaction(),
             SurfaceControl.Transaction(),
-        ) { }
+        ) {}
 
-        verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea(
-            anyInt(), eq(minimizeChange.leash), any())
+        verify(rootTaskDisplayAreaOrganizer)
+            .reparentToDisplayArea(anyInt(), eq(minimizeChange.leash), any())
     }
 
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun startAnimation_pendingTransition_noLaunchChange_returnsFalse() {
         val wct = WindowContainerTransaction()
         val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
@@ -505,15 +496,13 @@
             )
         )
 
-        val started = mixedHandler.startAnimation(
-            transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(nonLaunchTaskChange)
-            ),
-            SurfaceControl.Transaction(),
-            SurfaceControl.Transaction(),
-        ) { }
+        val started =
+            mixedHandler.startAnimation(
+                transition,
+                createCloseTransitionInfo(TRANSIT_OPEN, listOf(nonLaunchTaskChange)),
+                SurfaceControl.Transaction(),
+                SurfaceControl.Transaction(),
+            ) {}
 
         assertFalse("Should not start animation without launching desktop task", started)
     }
@@ -529,21 +518,18 @@
         whenever(transitions.dispatchTransition(eq(transition), any(), any(), any(), any(), any()))
             .thenReturn(mock())
 
-        mixedHandler.startLaunchTransition(
-            transitionType = TRANSIT_OPEN,
-            wct = wct,
-            taskId = null,
-        )
+        mixedHandler.startLaunchTransition(transitionType = TRANSIT_OPEN, wct = wct, taskId = null)
 
-        val started = mixedHandler.startAnimation(
-            transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(createChange(task, mode = TRANSIT_OPEN))
-            ),
-            StubTransaction(),
-            StubTransaction(),
-        ) { }
+        val started =
+            mixedHandler.startAnimation(
+                transition,
+                createCloseTransitionInfo(
+                    TRANSIT_OPEN,
+                    listOf(createChange(task, mode = TRANSIT_OPEN)),
+                ),
+                StubTransaction(),
+                StubTransaction(),
+            ) {}
 
         assertThat(started).isEqualTo(true)
     }
@@ -569,15 +555,13 @@
 
         val immersiveChange = createChange(immersiveTask, mode = TRANSIT_CHANGE)
         val openingChange = createChange(openingTask, mode = TRANSIT_OPEN)
-        val started = mixedHandler.startAnimation(
-            transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(immersiveChange, openingChange)
-            ),
-            StubTransaction(),
-            StubTransaction(),
-        ) { }
+        val started =
+            mixedHandler.startAnimation(
+                transition,
+                createCloseTransitionInfo(TRANSIT_OPEN, listOf(immersiveChange, openingChange)),
+                StubTransaction(),
+                StubTransaction(),
+            ) {}
 
         assertThat(started).isEqualTo(true)
         verify(desktopImmersiveController)
@@ -587,7 +571,8 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() {
         val wct = WindowContainerTransaction()
         val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
@@ -606,22 +591,19 @@
         )
         mixedHandler.startAnimation(
             transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(launchTaskChange)
-            ),
+            createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange)),
             SurfaceControl.Transaction(),
             SurfaceControl.Transaction(),
-        ) { }
+        ) {}
 
-        verify(rootTaskDisplayAreaOrganizer, times(0))
-            .reparentToDisplayArea(anyInt(), any(), any())
+        verify(rootTaskDisplayAreaOrganizer, times(0)).reparentToDisplayArea(anyInt(), any(), any())
     }
 
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS,
-        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
+        Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+    )
     fun addPendingAndAnimateLaunchTransition_withMinimizeChange_reparentsMinimizeChange() {
         val wct = WindowContainerTransaction()
         val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
@@ -642,16 +624,13 @@
         )
         mixedHandler.startAnimation(
             transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(launchTaskChange, minimizeChange)
-            ),
+            createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, minimizeChange)),
             SurfaceControl.Transaction(),
             SurfaceControl.Transaction(),
-        ) { }
+        ) {}
 
-        verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea(
-            anyInt(), eq(minimizeChange.leash), any())
+        verify(rootTaskDisplayAreaOrganizer)
+            .reparentToDisplayArea(anyInt(), eq(minimizeChange.leash), any())
     }
 
     @Test
@@ -672,13 +651,10 @@
         val launchTaskChange = createChange(launchingTask)
         mixedHandler.startAnimation(
             transition,
-            createCloseTransitionInfo(
-                TRANSIT_OPEN,
-                listOf(launchTaskChange)
-            ),
+            createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange)),
             SurfaceControl.Transaction(),
             SurfaceControl.Transaction(),
-        ) { }
+        ) {}
 
         assertThat(mixedHandler.pendingMixedTransitions).isEmpty()
     }
@@ -701,7 +677,7 @@
         mixedHandler.onTransitionConsumed(
             transition = transition,
             aborted = true,
-            finishTransaction = SurfaceControl.Transaction()
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(mixedHandler.pendingMixedTransitions).isEmpty()
@@ -714,8 +690,14 @@
         val transition = Binder()
         whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2)
         whenever(
-            desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any())
-        )
+                desktopBackNavigationTransitionHandler.startAnimation(
+                    any(),
+                    any(),
+                    any(),
+                    any(),
+                    any(),
+                )
+            )
             .thenReturn(true)
         mixedHandler.addPendingMixedTransition(
             PendingMixedTransition.Minimize(
@@ -726,24 +708,24 @@
         )
 
         val minimizingTaskChange = createChange(minimizingTask)
-        val started = mixedHandler.startAnimation(
-            transition = transition,
-            info =
-                createCloseTransitionInfo(
-                TRANSIT_TO_BACK,
-                listOf(minimizingTaskChange)
-            ),
-            startTransaction = mock(),
-            finishTransaction = mock(),
-            finishCallback = {}
-        )
+        val started =
+            mixedHandler.startAnimation(
+                transition = transition,
+                info = createCloseTransitionInfo(TRANSIT_TO_BACK, listOf(minimizingTaskChange)),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {},
+            )
 
         assertTrue("Should delegate animation to back navigation transition handler", started)
         verify(desktopBackNavigationTransitionHandler)
             .startAnimation(
                 eq(transition),
                 argThat { info -> info.changes.contains(minimizingTaskChange) },
-                any(), any(), any())
+                any(),
+                any(),
+                any(),
+            )
     }
 
     @Test
@@ -753,8 +735,14 @@
         val transition = Binder()
         whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2)
         whenever(
-            desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any())
-        )
+                desktopBackNavigationTransitionHandler.startAnimation(
+                    any(),
+                    any(),
+                    any(),
+                    any(),
+                    any(),
+                )
+            )
             .thenReturn(true)
         mixedHandler.addPendingMixedTransition(
             PendingMixedTransition.Minimize(
@@ -767,14 +755,10 @@
         val minimizingTaskChange = createChange(minimizingTask)
         mixedHandler.startAnimation(
             transition = transition,
-            info =
-            createCloseTransitionInfo(
-                TRANSIT_TO_BACK,
-                listOf(minimizingTaskChange)
-            ),
+            info = createCloseTransitionInfo(TRANSIT_TO_BACK, listOf(minimizingTaskChange)),
             startTransaction = mock(),
             finishTransaction = mock(),
-            finishCallback = {}
+            finishCallback = {},
         )
 
         verify(transitions)
@@ -784,7 +768,7 @@
                 any(),
                 any(),
                 any(),
-                eq(mixedHandler)
+                eq(mixedHandler),
             )
     }
 
@@ -814,14 +798,15 @@
 
     private fun createCloseTransitionInfo(
         @TransitionType type: Int,
-        changes: List<TransitionInfo.Change> = emptyList()
-    ): TransitionInfo = TransitionInfo(type, /* flags= */ 0).apply {
-        changes.forEach { change -> addChange(change) }
-    }
+        changes: List<TransitionInfo.Change> = emptyList(),
+    ): TransitionInfo =
+        TransitionInfo(type, /* flags= */ 0).apply {
+            changes.forEach { change -> addChange(change) }
+        }
 
     private fun createChange(
         task: RunningTaskInfo,
-        @TransitionInfo.TransitionMode mode: Int = TRANSIT_NONE
+        @TransitionInfo.TransitionMode mode: Int = TRANSIT_NONE,
     ): TransitionInfo.Change =
         TransitionInfo.Change(task.token, SurfaceControl()).apply {
             taskInfo = task
@@ -838,8 +823,6 @@
         RunningTaskInfo().apply {
             token = WindowContainerToken(mock<IWindowContainerToken>())
             baseIntent =
-                Intent().apply {
-                    component = DesktopWallpaperActivity.wallpaperActivityComponent
-                }
+                Intent().apply { component = DesktopWallpaperActivity.wallpaperActivityComponent }
         }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index 2f225f2..abd7078 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -53,9 +53,7 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
-/**
- * Tests for [DesktopModeEventLogger].
- */
+/** Tests for [DesktopModeEventLogger]. */
 class DesktopModeEventLoggerTest : ShellTestCase() {
 
     private val desktopModeEventLogger = DesktopModeEventLogger()
@@ -64,13 +62,13 @@
 
     @JvmField
     @Rule(order = 0)
-    val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
-        .mockStatic(FrameworkStatsLog::class.java)
-        .mockStatic(EventLogTags::class.java).build()!!
+    val extendedMockitoRule =
+        ExtendedMockitoRule.Builder(this)
+            .mockStatic(FrameworkStatsLog::class.java)
+            .mockStatic(EventLogTags::class.java)
+            .build()!!
 
-    @JvmField
-    @Rule(order = 1)
-    val setFlagsRule = SetFlagsRule()
+    @JvmField @Rule(order = 1) val setFlagsRule = SetFlagsRule()
 
     @Before
     fun setUp() {
@@ -95,14 +93,14 @@
                 /* exit_reason */
                 eq(0),
                 /* sessionId */
-                eq(sessionId)
+                eq(sessionId),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verify {
             EventLogTags.writeWmShellEnterDesktopMode(
                 eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason),
-                eq(sessionId)
+                eq(sessionId),
             )
         }
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -127,14 +125,14 @@
                 /* exit_reason */
                 eq(0),
                 /* sessionId */
-                eq(sessionId)
+                eq(sessionId),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verify {
             EventLogTags.writeWmShellEnterDesktopMode(
                 eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason),
-                eq(sessionId)
+                eq(sessionId),
             )
         }
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -164,14 +162,14 @@
                 /* exit_reason */
                 eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT),
                 /* sessionId */
-                eq(sessionId)
+                eq(sessionId),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verify {
             EventLogTags.writeWmShellExitDesktopMode(
                 eq(ExitReason.DRAG_TO_EXIT.reason),
-                eq(sessionId)
+                eq(sessionId),
             )
         }
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -214,16 +212,13 @@
                 eq(UNSET_MINIMIZE_REASON),
                 eq(UNSET_UNMINIMIZE_REASON),
                 /* visible_task_count */
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED
-                ),
+                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED),
                 eq(TASK_UPDATE.instanceId),
                 eq(TASK_UPDATE.uid),
                 eq(TASK_UPDATE.taskHeight),
@@ -233,7 +228,7 @@
                 eq(sessionId),
                 eq(UNSET_MINIMIZE_REASON),
                 eq(UNSET_UNMINIMIZE_REASON),
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -275,16 +270,13 @@
                 eq(UNSET_MINIMIZE_REASON),
                 eq(UNSET_UNMINIMIZE_REASON),
                 /* visible_task_count */
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED
-                ),
+                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED),
                 eq(TASK_UPDATE.instanceId),
                 eq(TASK_UPDATE.uid),
                 eq(TASK_UPDATE.taskHeight),
@@ -294,7 +286,7 @@
                 eq(sessionId),
                 eq(UNSET_MINIMIZE_REASON),
                 eq(UNSET_UNMINIMIZE_REASON),
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -339,7 +331,7 @@
                 eq(UNSET_MINIMIZE_REASON),
                 eq(UNSET_UNMINIMIZE_REASON),
                 /* visible_task_count */
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
@@ -358,7 +350,7 @@
                 eq(sessionId),
                 eq(UNSET_MINIMIZE_REASON),
                 eq(UNSET_UNMINIMIZE_REASON),
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -399,7 +391,7 @@
                 /* unminimize_reason */
                 eq(UNSET_UNMINIMIZE_REASON),
                 /* visible_task_count */
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
@@ -418,7 +410,7 @@
                 eq(sessionId),
                 eq(MinimizeReason.TASK_LIMIT.reason),
                 eq(UNSET_UNMINIMIZE_REASON),
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -459,7 +451,7 @@
                 /* unminimize_reason */
                 eq(UnminimizeReason.TASKBAR_TAP.reason),
                 /* visible_task_count */
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
@@ -478,7 +470,7 @@
                 eq(sessionId),
                 eq(UNSET_MINIMIZE_REASON),
                 eq(UnminimizeReason.TASKBAR_TAP.reason),
-                eq(TASK_COUNT)
+                eq(TASK_COUNT),
             )
         }
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -486,8 +478,11 @@
 
     @Test
     fun logTaskResizingStarted_noOngoingSession_doesNotLog() {
-        desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER,
-            InputMethod.UNKNOWN_INPUT_METHOD, createTaskInfo())
+        desktopModeEventLogger.logTaskResizingStarted(
+            ResizeTrigger.CORNER,
+            InputMethod.UNKNOWN_INPUT_METHOD,
+            createTaskInfo(),
+        )
 
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -498,19 +493,33 @@
     fun logTaskResizingStarted_logsTaskSizeUpdatedWithStartResizingStage() {
         val sessionId = startDesktopModeSession()
 
-        desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER,
-            InputMethod.UNKNOWN_INPUT_METHOD, createTaskInfo(), TASK_SIZE_UPDATE.taskWidth,
-            TASK_SIZE_UPDATE.taskHeight, displayController)
+        desktopModeEventLogger.logTaskResizingStarted(
+            ResizeTrigger.CORNER,
+            InputMethod.UNKNOWN_INPUT_METHOD,
+            createTaskInfo(),
+            TASK_SIZE_UPDATE.taskWidth,
+            TASK_SIZE_UPDATE.taskHeight,
+            displayController,
+        )
 
         verify {
             FrameworkStatsLog.write(
                 eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
                 /* resize_trigger */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER),
+                eq(
+                    FrameworkStatsLog
+                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER
+                ),
                 /* resizing_stage */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE),
+                eq(
+                    FrameworkStatsLog
+                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE
+                ),
                 /* input_method */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD),
+                eq(
+                    FrameworkStatsLog
+                        .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD
+                ),
                 /* desktop_mode_session_id */
                 eq(sessionId),
                 /* instance_id */
@@ -530,8 +539,11 @@
 
     @Test
     fun logTaskResizingEnded_noOngoingSession_doesNotLog() {
-        desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER,
-            InputMethod.UNKNOWN_INPUT_METHOD, createTaskInfo())
+        desktopModeEventLogger.logTaskResizingEnded(
+            ResizeTrigger.CORNER,
+            InputMethod.UNKNOWN_INPUT_METHOD,
+            createTaskInfo(),
+        )
 
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -542,18 +554,31 @@
     fun logTaskResizingEnded_logsTaskSizeUpdatedWithEndResizingStage() {
         val sessionId = startDesktopModeSession()
 
-        desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER,
-            InputMethod.UNKNOWN_INPUT_METHOD, createTaskInfo(), displayController = displayController)
+        desktopModeEventLogger.logTaskResizingEnded(
+            ResizeTrigger.CORNER,
+            InputMethod.UNKNOWN_INPUT_METHOD,
+            createTaskInfo(),
+            displayController = displayController,
+        )
 
         verify {
             FrameworkStatsLog.write(
                 eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
                 /* resize_trigger */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER),
+                eq(
+                    FrameworkStatsLog
+                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER
+                ),
                 /* resizing_stage */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE),
+                eq(
+                    FrameworkStatsLog
+                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE
+                ),
                 /* input_method */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD),
+                eq(
+                    FrameworkStatsLog
+                        .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD
+                ),
                 /* desktop_mode_session_id */
                 eq(sessionId),
                 /* instance_id */
@@ -582,9 +607,12 @@
     fun logTaskInfoStateInit_logsTaskInfoChangedStateInit() {
         desktopModeEventLogger.logTaskInfoStateInit()
         verify {
-            FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
                 /* task_event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD),
+                eq(
+                    FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD
+                ),
                 /* instance_id */
                 eq(0),
                 /* uid */
@@ -604,13 +632,14 @@
                 /* unminimize_reason */
                 eq(UNSET_UNMINIMIZE_REASON),
                 /* visible_task_count */
-                eq(0)
+                eq(0),
             )
         }
     }
 
     private fun createTaskInfo(): RunningTaskInfo {
-        return TestRunningTaskInfoBuilder().setTaskId(TASK_ID)
+        return TestRunningTaskInfoBuilder()
+            .setTaskId(TASK_ID)
             .setUid(TASK_UID)
             .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT))
             .build()
@@ -628,27 +657,42 @@
         private const val DISPLAY_HEIGHT = 500
         private const val DISPLAY_AREA = DISPLAY_HEIGHT * DISPLAY_WIDTH
 
-        private val TASK_UPDATE = TaskUpdate(
-            TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y,
-            visibleTaskCount = TASK_COUNT,
-        )
+        private val TASK_UPDATE =
+            TaskUpdate(
+                TASK_ID,
+                TASK_UID,
+                TASK_HEIGHT,
+                TASK_WIDTH,
+                TASK_X,
+                TASK_Y,
+                visibleTaskCount = TASK_COUNT,
+            )
 
-        private val TASK_SIZE_UPDATE = TaskSizeUpdate(
-            resizeTrigger = ResizeTrigger.UNKNOWN_RESIZE_TRIGGER,
-            inputMethod = InputMethod.UNKNOWN_INPUT_METHOD,
-            TASK_ID,
-            TASK_UID,
-            TASK_HEIGHT,
-            TASK_WIDTH,
-            DISPLAY_AREA,
-        )
+        private val TASK_SIZE_UPDATE =
+            TaskSizeUpdate(
+                resizeTrigger = ResizeTrigger.UNKNOWN_RESIZE_TRIGGER,
+                inputMethod = InputMethod.UNKNOWN_INPUT_METHOD,
+                TASK_ID,
+                TASK_UID,
+                TASK_HEIGHT,
+                TASK_WIDTH,
+                DISPLAY_AREA,
+            )
 
         private fun createTaskUpdate(
             minimizeReason: MinimizeReason? = null,
             unminimizeReason: UnminimizeReason? = null,
-        ) = TaskUpdate(
-            TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y, minimizeReason,
-            unminimizeReason, TASK_COUNT
-        )
+        ) =
+            TaskUpdate(
+                TASK_ID,
+                TASK_UID,
+                TASK_HEIGHT,
+                TASK_WIDTH,
+                TASK_X,
+                TASK_Y,
+                minimizeReason,
+                unminimizeReason,
+                TASK_COUNT,
+            )
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
index e57ae2a..413e7bc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
@@ -30,49 +30,49 @@
 import android.view.KeyEvent
 import android.window.DisplayAreaInfo
 import androidx.test.filters.SmallTest
-import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
-import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
-import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS
-import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT
-import com.android.wm.shell.MockToken
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer
-import com.android.wm.shell.ShellTaskOrganizer
-import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
-import com.android.wm.shell.transition.FocusTransitionObserver
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.eq
-import org.mockito.kotlin.whenever
 import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer
 import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
+import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS
+import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT
 import com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS
+import com.android.wm.shell.MockToken
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+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.DisplayLayout
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
 import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.FocusTransitionObserver
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel
+import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.setMain
 import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
 
 /**
@@ -130,21 +130,24 @@
         whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
 
         doAnswer {
-            keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler)
-            null
-        }.whenever(inputManager).registerKeyGestureEventHandler(any())
+                keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler)
+                null
+            }
+            .whenever(inputManager)
+            .registerKeyGestureEventHandler(any())
         shellInit.init()
 
-        desktopModeKeyGestureHandler = DesktopModeKeyGestureHandler(
-            context,
-            Optional.of(desktopModeWindowDecorViewModel),
-            Optional.of(desktopTasksController),
-            inputManager,
-            shellTaskOrganizer,
-            focusTransitionObserver,
-            testExecutor,
-            displayController
-        )
+        desktopModeKeyGestureHandler =
+            DesktopModeKeyGestureHandler(
+                context,
+                Optional.of(desktopModeWindowDecorViewModel),
+                Optional.of(desktopTasksController),
+                inputManager,
+                shellTaskOrganizer,
+                focusTransitionObserver,
+                testExecutor,
+                displayController,
+            )
     }
 
     @After
@@ -160,7 +163,7 @@
     @EnableFlags(
         FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS,
         FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT,
-        FLAG_USE_KEY_GESTURE_EVENT_HANDLER
+        FLAG_USE_KEY_GESTURE_EVENT_HANDLER,
     )
     fun keyGestureMoveToNextDisplay_shouldMoveToNextDisplay() {
         // Set up two display ids
@@ -176,12 +179,13 @@
         whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task))
         whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true)
 
-        val event = KeyGestureEvent.Builder()
-            .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY)
-            .setDisplayId(SECOND_DISPLAY)
-            .setKeycodes(intArrayOf(KeyEvent.KEYCODE_D))
-            .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON)
-            .build()
+        val event =
+            KeyGestureEvent.Builder()
+                .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY)
+                .setDisplayId(SECOND_DISPLAY)
+                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_D))
+                .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON)
+                .build()
         val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
@@ -190,108 +194,102 @@
     }
 
     @Test
-    @EnableFlags(
-        FLAG_USE_KEY_GESTURE_EVENT_HANDLER,
-        FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS
-    )
+    @EnableFlags(FLAG_USE_KEY_GESTURE_EVENT_HANDLER, FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS)
     fun keyGestureSnapLeft_shouldSnapResizeTaskToLeft() {
         val task = setUpFreeformTask()
         task.isFocused = true
         whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task))
         whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true)
 
-        val event = KeyGestureEvent.Builder()
-            .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW)
-            .setKeycodes(intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET))
-            .setModifierState(KeyEvent.META_META_ON)
-            .build()
+        val event =
+            KeyGestureEvent.Builder()
+                .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW)
+                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET))
+                .setModifierState(KeyEvent.META_META_ON)
+                .build()
         val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
         assertThat(result).isTrue()
-        verify(desktopModeWindowDecorViewModel).onSnapResize(
-            task.taskId,
-            true,
-            DesktopModeEventLogger.Companion.InputMethod.KEYBOARD,
-            /* fromMenu= */ false
-        )
+        verify(desktopModeWindowDecorViewModel)
+            .onSnapResize(
+                task.taskId,
+                true,
+                DesktopModeEventLogger.Companion.InputMethod.KEYBOARD,
+                /* fromMenu= */ false,
+            )
     }
 
     @Test
-    @EnableFlags(
-        FLAG_USE_KEY_GESTURE_EVENT_HANDLER,
-        FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS
-    )
+    @EnableFlags(FLAG_USE_KEY_GESTURE_EVENT_HANDLER, FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS)
     fun keyGestureSnapRight_shouldSnapResizeTaskToRight() {
         val task = setUpFreeformTask()
         task.isFocused = true
         whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task))
         whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true)
 
-        val event = KeyGestureEvent.Builder()
-            .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW)
-            .setKeycodes(intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET))
-            .setModifierState(KeyEvent.META_META_ON)
-            .build()
+        val event =
+            KeyGestureEvent.Builder()
+                .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW)
+                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET))
+                .setModifierState(KeyEvent.META_META_ON)
+                .build()
         val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
         assertThat(result).isTrue()
-        verify(desktopModeWindowDecorViewModel).onSnapResize(
-            task.taskId,
-            false,
-            DesktopModeEventLogger.Companion.InputMethod.KEYBOARD,
-            /* fromMenu= */ false
-        )
+        verify(desktopModeWindowDecorViewModel)
+            .onSnapResize(
+                task.taskId,
+                false,
+                DesktopModeEventLogger.Companion.InputMethod.KEYBOARD,
+                /* fromMenu= */ false,
+            )
     }
 
     @Test
-    @EnableFlags(
-        FLAG_USE_KEY_GESTURE_EVENT_HANDLER,
-        FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS
-    )
+    @EnableFlags(FLAG_USE_KEY_GESTURE_EVENT_HANDLER, FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS)
     fun keyGestureToggleFreeformWindowSize_shouldToggleTaskSize() {
         val task = setUpFreeformTask()
         task.isFocused = true
         whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task))
         whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true)
 
-        val event = KeyGestureEvent.Builder()
-            .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW)
-            .setKeycodes(intArrayOf(KeyEvent.KEYCODE_EQUALS))
-            .setModifierState(KeyEvent.META_META_ON)
-            .build()
+        val event =
+            KeyGestureEvent.Builder()
+                .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW)
+                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_EQUALS))
+                .setModifierState(KeyEvent.META_META_ON)
+                .build()
         val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
         assertThat(result).isTrue()
-        verify(desktopTasksController).toggleDesktopTaskSize(
-            task,
-            ToggleTaskSizeInteraction(
-                isMaximized = isTaskMaximized(task, displayController),
-                source = ToggleTaskSizeInteraction.Source.KEYBOARD_SHORTCUT,
-                inputMethod =
-                    DesktopModeEventLogger.Companion.InputMethod.KEYBOARD,
-            ),
-        )
+        verify(desktopTasksController)
+            .toggleDesktopTaskSize(
+                task,
+                ToggleTaskSizeInteraction(
+                    isMaximized = isTaskMaximized(task, displayController),
+                    source = ToggleTaskSizeInteraction.Source.KEYBOARD_SHORTCUT,
+                    inputMethod = DesktopModeEventLogger.Companion.InputMethod.KEYBOARD,
+                ),
+            )
     }
 
     @Test
-    @EnableFlags(
-        FLAG_USE_KEY_GESTURE_EVENT_HANDLER,
-        FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS
-    )
+    @EnableFlags(FLAG_USE_KEY_GESTURE_EVENT_HANDLER, FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS)
     fun keyGestureMinimizeFreeformWindow_shouldMinimizeTask() {
         val task = setUpFreeformTask()
         task.isFocused = true
         whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task))
         whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true)
 
-        val event = KeyGestureEvent.Builder()
-            .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW)
-            .setKeycodes(intArrayOf(KeyEvent.KEYCODE_MINUS))
-            .setModifierState(KeyEvent.META_META_ON)
-            .build()
+        val event =
+            KeyGestureEvent.Builder()
+                .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW)
+                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_MINUS))
+                .setModifierState(KeyEvent.META_META_ON)
+                .build()
         val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index 7c4ce4a..43684fb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -87,700 +87,735 @@
 @RunWith(AndroidTestingRunner::class)
 class DesktopModeLoggerTransitionObserverTest : ShellTestCase() {
 
-  @JvmField
-  @Rule
-  val extendedMockitoRule =
-      ExtendedMockitoRule.Builder(this)
-          .mockStatic(DesktopModeStatus::class.java)
-          .mockStatic(SystemProperties::class.java)
-          .mockStatic(Trace::class.java)
-          .build()!!
+    @JvmField
+    @Rule
+    val extendedMockitoRule =
+        ExtendedMockitoRule.Builder(this)
+            .mockStatic(DesktopModeStatus::class.java)
+            .mockStatic(SystemProperties::class.java)
+            .mockStatic(Trace::class.java)
+            .build()!!
 
-  private val testExecutor = mock<ShellExecutor>()
-  private val mockShellInit = mock<ShellInit>()
-  private val transitions = mock<Transitions>()
-  private val context = mock<Context>()
+    private val testExecutor = mock<ShellExecutor>()
+    private val mockShellInit = mock<ShellInit>()
+    private val transitions = mock<Transitions>()
+    private val context = mock<Context>()
 
-  private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
-  private lateinit var shellInit: ShellInit
-  private lateinit var desktopModeEventLogger: DesktopModeEventLogger
+    private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
+    private lateinit var shellInit: ShellInit
+    private lateinit var desktopModeEventLogger: DesktopModeEventLogger
 
-  @Before
-  fun setup() {
-    whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
-    shellInit = spy(ShellInit(testExecutor))
-    desktopModeEventLogger = mock<DesktopModeEventLogger>()
+    @Before
+    fun setup() {
+        whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
+        shellInit = spy(ShellInit(testExecutor))
+        desktopModeEventLogger = mock<DesktopModeEventLogger>()
 
-    transitionObserver = DesktopModeLoggerTransitionObserver(
-        context, mockShellInit, transitions, desktopModeEventLogger)
-    val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
-    verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
-    initRunnableCaptor.value.run()
-    // verify this initialisation interaction to leave the desktopmodeEventLogger mock in a
-    // consistent state with no outstanding interactions when test cases start executing.
-    verify(desktopModeEventLogger).logTaskInfoStateInit()
-  }
-
-  @Test
-  fun testInitialiseVisibleTasksSystemProperty() {
-    ExtendedMockito.verify {
-      SystemProperties.set(
-          eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY),
-          eq(DesktopModeLoggerTransitionObserver
-              .VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE))
-    }
-  }
-
-  @Test
-  fun testRegistersObserverAtInit() {
-    verify(transitions).registerObserver(same(transitionObserver))
-  }
-
-  @Test
-  fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() {
-    val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verify(desktopModeEventLogger, never()).logSessionEnter(any())
-    verify(desktopModeEventLogger, never()).logTaskAdded(any())
-  }
-
-  @Test
-  fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() {
-    val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.APP_FREEFORM_INTENT,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() {
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    // task change is finalised when drag ends
-    val transitionInfo =
-        TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0)
-            .addChange(change)
-            .build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() {
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0)
-            .addChange(change)
-            .build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_MENU_BUTTON,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonAppFromOverview() {
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
-            .addChange(change)
-            .build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() {
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0)
-            .addChange(change)
-            .build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.KEYBOARD_SHORTCUT_ENTER,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitToFront_logTaskAddedAndEnterReasonOverview() {
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitToFront_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
-    // previous exit to overview transition
-    // add a freeform task
-    val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
-    transitionObserver.isSessionActive = true
-    val previousTransitionInfo =
-        TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
-            .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
-            .build()
-
-    callOnTransitionReady(previousTransitionInfo)
-
-    verifyTaskRemovedAndExitLogging(
-        ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE
-    )
-
-    // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
-    // next transition involving freeform windows
-
-    // TRANSIT_TO_FRONT
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitChange_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
-    // previous exit to overview transition
-    // add a freeform task
-    val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
-    transitionObserver.isSessionActive = true
-    val previousTransitionInfo =
-        TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
-            .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
-            .build()
-
-    callOnTransitionReady(previousTransitionInfo)
-
-    verifyTaskRemovedAndExitLogging(
-        ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE
-    )
-
-    // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
-    // next transition involving freeform windows
-
-    // TRANSIT_CHANGE
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_CHANGE, 0).addChange(change).build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitOpen_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
-    // previous exit to overview transition
-    // add a freeform task
-    val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
-    transitionObserver.isSessionActive = true
-    val previousTransitionInfo =
-        TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
-            .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
-            .build()
-
-    callOnTransitionReady(previousTransitionInfo)
-
-    verifyTaskRemovedAndExitLogging(
-        ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE
-    )
-
-    // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
-    // next transition involving freeform windows
-
-    // TRANSIT_OPEN
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  @Suppress("ktlint:standard:max-line-length")
-  fun transitEnterDesktopFromAppFromOverview_previousTransitionExitToOverview_logTaskAddedAndEnterReasonAppFromOverview() {
-    // Tests for AppFromOverview precedence in compared to cancelled Overview
-
-    // previous exit to overview transition
-    // add a freeform task
-    val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
-    transitionObserver.isSessionActive = true
-    val previousTransitionInfo =
-        TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
-            .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
-            .build()
-
-    callOnTransitionReady(previousTransitionInfo)
-
-    verifyTaskRemovedAndExitLogging(
-        ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE
-    )
-
-    // TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
-            .addChange(change)
-            .build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() {
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.UNKNOWN_ENTER,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitWake_logTaskAddedAndEnterReasonScreenOn() {
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitBack_previousExitReasonScreenOff_logTaskAddedAndEnterReasonScreenOn() {
-    val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    // Previous Exit reason recorded as Screen Off
-    transitionObserver.addTaskInfosToCachedMap(freeformTask)
-    transitionObserver.isSessionActive = true
-    callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build())
-    verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE)
-    // Enter desktop through back transition, this happens when user enters after dismissing
-    // keyguard
-    val change = createChange(TRANSIT_TO_FRONT, freeformTask)
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_BACK, 0).addChange(change).build()
-
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitEndDragToDesktop_previousExitReasonScreenOff_logTaskAddedAndEnterReasonAppDrag() {
-    val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    // Previous Exit reason recorded as Screen Off
-    transitionObserver.addTaskInfosToCachedMap(freeformTask)
-    transitionObserver.isSessionActive = true
-    callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build())
-    verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE)
-
-    // Enter desktop through app handle drag. This represents cases where instead of moving to
-    // desktop right after turning the screen on, we move to fullscreen then move another task
-    // to desktop
-    val transitionInfo =
-        TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0)
-            .addChange(createChange(TRANSIT_TO_FRONT, freeformTask))
-            .build()
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun transitSleep_logTaskRemovedAndExitReasonScreenOff() {
-    // add a freeform task
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-    transitionObserver.isSessionActive = true
-
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE)
-  }
-
-  @Test
-  fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit() {
-    // add a freeform task
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-    transitionObserver.isSessionActive = true
-
-    // window mode changing from FREEFORM to FULLSCREEN
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build()
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskRemovedAndExitLogging(ExitReason.DRAG_TO_EXIT, DEFAULT_TASK_UPDATE)
-  }
-
-  @Test
-  fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton() {
-    // add a freeform task
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-    transitionObserver.isSessionActive = true
-
-    // window mode changing from FREEFORM to FULLSCREEN
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON)
-            .addChange(change)
-            .build()
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskRemovedAndExitLogging(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT, DEFAULT_TASK_UPDATE)
-  }
-
-  @Test
-  fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard() {
-    // add a freeform task
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-    transitionObserver.isSessionActive = true
-
-    // window mode changing from FREEFORM to FULLSCREEN
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT).addChange(change).build()
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskRemovedAndExitLogging(ExitReason.KEYBOARD_SHORTCUT_EXIT, DEFAULT_TASK_UPDATE)
-  }
-
-  @Test
-  fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown() {
-    // add a freeform task
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-    transitionObserver.isSessionActive = true
-
-    // window mode changing from FREEFORM to FULLSCREEN
-    val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build()
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskRemovedAndExitLogging(ExitReason.UNKNOWN_EXIT, DEFAULT_TASK_UPDATE)
-  }
-
-  @Test
-  fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview() {
-    // add a freeform task
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-    transitionObserver.isSessionActive = true
-
-    // recents transition
-    val change = createChange(TRANSIT_TO_BACK, createTaskInfo(WINDOWING_MODE_FREEFORM))
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build()
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskRemovedAndExitLogging(
-        ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE
-    )
-  }
-
-  @Test
-  fun transitClose_logTaskRemovedAndExitReasonTaskFinished() {
-    // add a freeform task
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-    transitionObserver.isSessionActive = true
-
-    // task closing
-    val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
-    callOnTransitionReady(transitionInfo)
-
-    verifyTaskRemovedAndExitLogging(ExitReason.TASK_FINISHED, DEFAULT_TASK_UPDATE)
-  }
-
-  @Test
-  fun transitMinimize_logExitReasongMinimized() {
-      // add a freeform task
-      transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-      transitionObserver.isSessionActive = true
-
-      // minimize the task
-      val change = createChange(TRANSIT_MINIMIZE, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
-      val transitionInfo = TransitionInfoBuilder(TRANSIT_MINIMIZE).addChange(change).build()
-      callOnTransitionReady(transitionInfo)
-
-      assertFalse(transitionObserver.isSessionActive)
-      verify(desktopModeEventLogger, times(1)).logSessionExit(eq(ExitReason.TASK_MINIMIZED))
-      verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(DEFAULT_TASK_UPDATE))
-      verifyZeroInteractions(desktopModeEventLogger)
-  }
-
-  @Test
-  fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
-    // add a freeform task to an existing session
-    val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    transitionObserver.addTaskInfosToCachedMap(taskInfo)
-    transitionObserver.isSessionActive = true
-
-    // recents transition sent freeform window to back
-    val change = createChange(TRANSIT_TO_BACK, taskInfo)
-    val transitionInfo1 =
-        TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build()
-    callOnTransitionReady(transitionInfo1)
-
-    verifyTaskRemovedAndExitLogging(
-        ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE
-    )
-
-    val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
-    callOnTransitionReady(transitionInfo2)
-
-    verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW,
-        DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1))
-  }
-
-  @Test
-  fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
-    // add an existing freeform task
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-    transitionObserver.isSessionActive = true
-
-    // new freeform task added
-    val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-    callOnTransitionReady(transitionInfo)
-
-    verify(desktopModeEventLogger, times(1))
-        .logTaskAdded(eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2, visibleTaskCount = 2)))
-    verify(desktopModeEventLogger, never()).logSessionEnter(any())
-  }
-
-  @Test
-  fun sessionAlreadyStarted_taskPositionChanged_logsTaskUpdate() {
-    // add an existing freeform task
-    val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    transitionObserver.addTaskInfosToCachedMap(taskInfo)
-    transitionObserver.isSessionActive = true
-
-    // task position changed
-    val newTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100)
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_CHANGE, 0)
-            .addChange(createChange(TRANSIT_CHANGE, newTaskInfo))
-            .build()
-    callOnTransitionReady(transitionInfo)
-
-    verify(desktopModeEventLogger, times(1))
-        .logTaskInfoChanged(
-            eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 1))
-        )
-    verifyZeroInteractions(desktopModeEventLogger)
-  }
-
-  @Test
-  fun sessionAlreadyStarted_taskResized_logsTaskUpdate() {
-    // add an existing freeform task
-    val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    transitionObserver.addTaskInfosToCachedMap(taskInfo)
-    transitionObserver.isSessionActive = true
-
-    // task resized
-    val newTaskInfo =
-        createTaskInfo(
-            WINDOWING_MODE_FREEFORM,
-            taskWidth = DEFAULT_TASK_WIDTH + 100,
-            taskHeight = DEFAULT_TASK_HEIGHT - 100)
-    val transitionInfo =
-        TransitionInfoBuilder(TRANSIT_CHANGE, 0)
-            .addChange(createChange(TRANSIT_CHANGE, newTaskInfo))
-            .build()
-    callOnTransitionReady(transitionInfo)
-
-    verify(desktopModeEventLogger, times(1))
-        .logTaskInfoChanged(
-            eq(
-                DEFAULT_TASK_UPDATE.copy(
-                    taskWidth = DEFAULT_TASK_WIDTH + 100, taskHeight = DEFAULT_TASK_HEIGHT - 100,
-                    visibleTaskCount = 1))
-        )
-    verifyZeroInteractions(desktopModeEventLogger)
-  }
-
-  @Test
-  fun sessionAlreadyStarted_multipleTasksUpdated_logsTaskUpdateForCorrectTask() {
-    // add 2 existing freeform task
-    val taskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM)
-    val taskInfo2 = createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)
-    transitionObserver.addTaskInfosToCachedMap(taskInfo1)
-    transitionObserver.addTaskInfosToCachedMap(taskInfo2)
-    transitionObserver.isSessionActive = true
-
-    // task 1 position update
-    val newTaskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100)
-    val transitionInfo1 =
-        TransitionInfoBuilder(TRANSIT_CHANGE, 0)
-            .addChange(createChange(TRANSIT_CHANGE, newTaskInfo1))
-            .build()
-    callOnTransitionReady(transitionInfo1)
-
-    verify(desktopModeEventLogger, times(1))
-        .logTaskInfoChanged(
-            eq(DEFAULT_TASK_UPDATE.copy(
-                taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 2))
-        )
-    verifyZeroInteractions(desktopModeEventLogger)
-
-    // task 2 resize
-    val newTaskInfo2 =
-        createTaskInfo(
-            WINDOWING_MODE_FREEFORM,
-            id = 2,
-            taskWidth = DEFAULT_TASK_WIDTH + 100,
-            taskHeight = DEFAULT_TASK_HEIGHT - 100)
-    val transitionInfo2 =
-        TransitionInfoBuilder(TRANSIT_CHANGE, 0)
-            .addChange(createChange(TRANSIT_CHANGE, newTaskInfo2))
-            .build()
-
-    callOnTransitionReady(transitionInfo2)
-
-    verify(desktopModeEventLogger, times(1))
-        .logTaskInfoChanged(
-            eq(
-                DEFAULT_TASK_UPDATE.copy(
-                    instanceId = 2,
-                    taskWidth = DEFAULT_TASK_WIDTH + 100,
-                    taskHeight = DEFAULT_TASK_HEIGHT - 100,
-                    visibleTaskCount = 2)),
+        transitionObserver =
+            DesktopModeLoggerTransitionObserver(
+                context,
+                mockShellInit,
+                transitions,
+                desktopModeEventLogger,
             )
-    verifyZeroInteractions(desktopModeEventLogger)
-  }
-
-  @Test
-  fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
-    // add two existing freeform tasks
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
-    transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2))
-    transitionObserver.isSessionActive = true
-
-    // new freeform task closed
-    val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2))
-    val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
-    callOnTransitionReady(transitionInfo)
-
-    verify(desktopModeEventLogger, times(1))
-        .logTaskRemoved(
-            eq(DEFAULT_TASK_UPDATE.copy(
-                instanceId = 2, visibleTaskCount = 1))
-        )
-    verify(desktopModeEventLogger, never()).logSessionExit(any())
-  }
-
-  /** Simulate calling the onTransitionReady() method */
-  private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
-    val transition = mock<IBinder>()
-    val startT = mock<SurfaceControl.Transaction>()
-    val finishT = mock<SurfaceControl.Transaction>()
-
-    transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
-  }
-
-  private fun verifyTaskAddedAndEnterLogging(enterReason: EnterReason, taskUpdate: TaskUpdate) {
-    assertTrue(transitionObserver.isSessionActive)
-    verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(enterReason))
-    verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(taskUpdate))
-    ExtendedMockito.verify {
-        Trace.setCounter(
-            eq(Trace.TRACE_TAG_WINDOW_MANAGER),
-            eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_NAME),
-            eq(taskUpdate.visibleTaskCount.toLong()))
+        val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+        verify(mockShellInit)
+            .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
+        initRunnableCaptor.value.run()
+        // verify this initialisation interaction to leave the desktopmodeEventLogger mock in a
+        // consistent state with no outstanding interactions when test cases start executing.
+        verify(desktopModeEventLogger).logTaskInfoStateInit()
     }
-    ExtendedMockito.verify {
-        SystemProperties.set(
-            eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY),
-            eq(taskUpdate.visibleTaskCount.toString()))
-    }
-    verifyZeroInteractions(desktopModeEventLogger)
-  }
 
-  private fun verifyTaskRemovedAndExitLogging(
-      exitReason: ExitReason,
-      taskUpdate: TaskUpdate
-  ) {
-    assertFalse(transitionObserver.isSessionActive)
-    verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(taskUpdate))
-    verify(desktopModeEventLogger, times(1)).logSessionExit(eq(exitReason))
-    verifyZeroInteractions(desktopModeEventLogger)
-  }
-
-  private companion object {
-    const val DEFAULT_TASK_ID = 1
-    const val DEFAULT_TASK_UID = 2
-    const val DEFAULT_TASK_HEIGHT = 100
-    const val DEFAULT_TASK_WIDTH = 200
-    const val DEFAULT_TASK_X = 30
-    const val DEFAULT_TASK_Y = 70
-    const val DEFAULT_VISIBLE_TASK_COUNT = 0
-    val DEFAULT_TASK_UPDATE =
-        TaskUpdate(
-            DEFAULT_TASK_ID,
-            DEFAULT_TASK_UID,
-            DEFAULT_TASK_HEIGHT,
-            DEFAULT_TASK_WIDTH,
-            DEFAULT_TASK_X,
-            DEFAULT_TASK_Y,
-            visibleTaskCount = DEFAULT_VISIBLE_TASK_COUNT,
-        )
-
-    fun createTaskInfo(
-        windowMode: Int,
-        id: Int = DEFAULT_TASK_ID,
-        uid: Int = DEFAULT_TASK_UID,
-        taskHeight: Int = DEFAULT_TASK_HEIGHT,
-        taskWidth: Int = DEFAULT_TASK_WIDTH,
-        taskX: Int = DEFAULT_TASK_X,
-        taskY: Int = DEFAULT_TASK_Y,
-    ) =
-        ActivityManager.RunningTaskInfo().apply {
-          taskId = id
-          effectiveUid = uid
-          configuration.windowConfiguration.apply {
-            windowingMode = windowMode
-            positionInParent = Point(taskX, taskY)
-            bounds.set(Rect(taskX, taskY, taskX + taskWidth, taskY + taskHeight))
-          }
+    @Test
+    fun testInitialiseVisibleTasksSystemProperty() {
+        ExtendedMockito.verify {
+            SystemProperties.set(
+                eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY),
+                eq(
+                    DesktopModeLoggerTransitionObserver
+                        .VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE
+                ),
+            )
         }
-
-    fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
-      val change =
-          Change(WindowContainerToken(mock<IWindowContainerToken>()), mock<SurfaceControl>())
-      change.mode = mode
-      change.taskInfo = taskInfo
-      return change
     }
-  }
+
+    @Test
+    fun testRegistersObserverAtInit() {
+        verify(transitions).registerObserver(same(transitionObserver))
+    }
+
+    @Test
+    fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() {
+        val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, never()).logSessionEnter(any())
+        verify(desktopModeEventLogger, never()).logTaskAdded(any())
+    }
+
+    @Test
+    fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() {
+        val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.APP_FREEFORM_INTENT,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        // task change is finalised when drag ends
+        val transitionInfo =
+            TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0)
+                .addChange(change)
+                .build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.APP_HANDLE_DRAG,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0)
+                .addChange(change)
+                .build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.APP_HANDLE_MENU_BUTTON,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonAppFromOverview() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
+                .addChange(change)
+                .build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.APP_FROM_OVERVIEW,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0)
+                .addChange(change)
+                .build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.KEYBOARD_SHORTCUT_ENTER,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitToFront_logTaskAddedAndEnterReasonOverview() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.OVERVIEW,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitToFront_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
+        // previous exit to overview transition
+        // add a freeform task
+        val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
+        transitionObserver.isSessionActive = true
+        val previousTransitionInfo =
+            TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+                .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
+                .build()
+
+        callOnTransitionReady(previousTransitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+        // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
+        // next transition involving freeform windows
+
+        // TRANSIT_TO_FRONT
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.OVERVIEW,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitChange_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
+        // previous exit to overview transition
+        // add a freeform task
+        val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
+        transitionObserver.isSessionActive = true
+        val previousTransitionInfo =
+            TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+                .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
+                .build()
+
+        callOnTransitionReady(previousTransitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+        // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
+        // next transition involving freeform windows
+
+        // TRANSIT_CHANGE
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_CHANGE, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.OVERVIEW,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitOpen_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
+        // previous exit to overview transition
+        // add a freeform task
+        val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
+        transitionObserver.isSessionActive = true
+        val previousTransitionInfo =
+            TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+                .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
+                .build()
+
+        callOnTransitionReady(previousTransitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+        // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
+        // next transition involving freeform windows
+
+        // TRANSIT_OPEN
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.OVERVIEW,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    @Suppress("ktlint:standard:max-line-length")
+    fun transitEnterDesktopFromAppFromOverview_previousTransitionExitToOverview_logTaskAddedAndEnterReasonAppFromOverview() {
+        // Tests for AppFromOverview precedence in compared to cancelled Overview
+
+        // previous exit to overview transition
+        // add a freeform task
+        val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
+        transitionObserver.isSessionActive = true
+        val previousTransitionInfo =
+            TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+                .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
+                .build()
+
+        callOnTransitionReady(previousTransitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+        // TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
+                .addChange(change)
+                .build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.APP_FROM_OVERVIEW,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.UNKNOWN_ENTER,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitWake_logTaskAddedAndEnterReasonScreenOn() {
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.SCREEN_ON,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitBack_previousExitReasonScreenOff_logTaskAddedAndEnterReasonScreenOn() {
+        val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        // Previous Exit reason recorded as Screen Off
+        transitionObserver.addTaskInfosToCachedMap(freeformTask)
+        transitionObserver.isSessionActive = true
+        callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build())
+        verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE)
+        // Enter desktop through back transition, this happens when user enters after dismissing
+        // keyguard
+        val change = createChange(TRANSIT_TO_FRONT, freeformTask)
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_BACK, 0).addChange(change).build()
+
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.SCREEN_ON,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitEndDragToDesktop_previousExitReasonScreenOff_logTaskAddedAndEnterReasonAppDrag() {
+        val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        // Previous Exit reason recorded as Screen Off
+        transitionObserver.addTaskInfosToCachedMap(freeformTask)
+        transitionObserver.isSessionActive = true
+        callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build())
+        verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE)
+
+        // Enter desktop through app handle drag. This represents cases where instead of moving to
+        // desktop right after turning the screen on, we move to fullscreen then move another task
+        // to desktop
+        val transitionInfo =
+            TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0)
+                .addChange(createChange(TRANSIT_TO_FRONT, freeformTask))
+                .build()
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.APP_HANDLE_DRAG,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun transitSleep_logTaskRemovedAndExitReasonScreenOff() {
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.isSessionActive = true
+
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE)
+    }
+
+    @Test
+    fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit() {
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.isSessionActive = true
+
+        // window mode changing from FREEFORM to FULLSCREEN
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.DRAG_TO_EXIT, DEFAULT_TASK_UPDATE)
+    }
+
+    @Test
+    fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton() {
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.isSessionActive = true
+
+        // window mode changing from FREEFORM to FULLSCREEN
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON)
+                .addChange(change)
+                .build()
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT, DEFAULT_TASK_UPDATE)
+    }
+
+    @Test
+    fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard() {
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.isSessionActive = true
+
+        // window mode changing from FREEFORM to FULLSCREEN
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT)
+                .addChange(change)
+                .build()
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.KEYBOARD_SHORTCUT_EXIT, DEFAULT_TASK_UPDATE)
+    }
+
+    @Test
+    fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown() {
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.isSessionActive = true
+
+        // window mode changing from FREEFORM to FULLSCREEN
+        val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.UNKNOWN_EXIT, DEFAULT_TASK_UPDATE)
+    }
+
+    @Test
+    fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview() {
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.isSessionActive = true
+
+        // recents transition
+        val change = createChange(TRANSIT_TO_BACK, createTaskInfo(WINDOWING_MODE_FREEFORM))
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+                .addChange(change)
+                .build()
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+    }
+
+    @Test
+    fun transitClose_logTaskRemovedAndExitReasonTaskFinished() {
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.isSessionActive = true
+
+        // task closing
+        val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.TASK_FINISHED, DEFAULT_TASK_UPDATE)
+    }
+
+    @Test
+    fun transitMinimize_logExitReasongMinimized() {
+        // add a freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.isSessionActive = true
+
+        // minimize the task
+        val change = createChange(TRANSIT_MINIMIZE, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_MINIMIZE).addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        assertFalse(transitionObserver.isSessionActive)
+        verify(desktopModeEventLogger, times(1)).logSessionExit(eq(ExitReason.TASK_MINIMIZED))
+        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(DEFAULT_TASK_UPDATE))
+        verifyZeroInteractions(desktopModeEventLogger)
+    }
+
+    @Test
+    fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
+        // add a freeform task to an existing session
+        val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        transitionObserver.addTaskInfosToCachedMap(taskInfo)
+        transitionObserver.isSessionActive = true
+
+        // recents transition sent freeform window to back
+        val change = createChange(TRANSIT_TO_BACK, taskInfo)
+        val transitionInfo1 =
+            TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+                .addChange(change)
+                .build()
+        callOnTransitionReady(transitionInfo1)
+
+        verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+        val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
+        callOnTransitionReady(transitionInfo2)
+
+        verifyTaskAddedAndEnterLogging(
+            EnterReason.OVERVIEW,
+            DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1),
+        )
+    }
+
+    @Test
+    fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
+        // add an existing freeform task
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.isSessionActive = true
+
+        // new freeform task added
+        val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1))
+            .logTaskAdded(eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2, visibleTaskCount = 2)))
+        verify(desktopModeEventLogger, never()).logSessionEnter(any())
+    }
+
+    @Test
+    fun sessionAlreadyStarted_taskPositionChanged_logsTaskUpdate() {
+        // add an existing freeform task
+        val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        transitionObserver.addTaskInfosToCachedMap(taskInfo)
+        transitionObserver.isSessionActive = true
+
+        // task position changed
+        val newTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100)
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_CHANGE, 0)
+                .addChange(createChange(TRANSIT_CHANGE, newTaskInfo))
+                .build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1))
+            .logTaskInfoChanged(
+                eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 1))
+            )
+        verifyZeroInteractions(desktopModeEventLogger)
+    }
+
+    @Test
+    fun sessionAlreadyStarted_taskResized_logsTaskUpdate() {
+        // add an existing freeform task
+        val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        transitionObserver.addTaskInfosToCachedMap(taskInfo)
+        transitionObserver.isSessionActive = true
+
+        // task resized
+        val newTaskInfo =
+            createTaskInfo(
+                WINDOWING_MODE_FREEFORM,
+                taskWidth = DEFAULT_TASK_WIDTH + 100,
+                taskHeight = DEFAULT_TASK_HEIGHT - 100,
+            )
+        val transitionInfo =
+            TransitionInfoBuilder(TRANSIT_CHANGE, 0)
+                .addChange(createChange(TRANSIT_CHANGE, newTaskInfo))
+                .build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1))
+            .logTaskInfoChanged(
+                eq(
+                    DEFAULT_TASK_UPDATE.copy(
+                        taskWidth = DEFAULT_TASK_WIDTH + 100,
+                        taskHeight = DEFAULT_TASK_HEIGHT - 100,
+                        visibleTaskCount = 1,
+                    )
+                )
+            )
+        verifyZeroInteractions(desktopModeEventLogger)
+    }
+
+    @Test
+    fun sessionAlreadyStarted_multipleTasksUpdated_logsTaskUpdateForCorrectTask() {
+        // add 2 existing freeform task
+        val taskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM)
+        val taskInfo2 = createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)
+        transitionObserver.addTaskInfosToCachedMap(taskInfo1)
+        transitionObserver.addTaskInfosToCachedMap(taskInfo2)
+        transitionObserver.isSessionActive = true
+
+        // task 1 position update
+        val newTaskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100)
+        val transitionInfo1 =
+            TransitionInfoBuilder(TRANSIT_CHANGE, 0)
+                .addChange(createChange(TRANSIT_CHANGE, newTaskInfo1))
+                .build()
+        callOnTransitionReady(transitionInfo1)
+
+        verify(desktopModeEventLogger, times(1))
+            .logTaskInfoChanged(
+                eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 2))
+            )
+        verifyZeroInteractions(desktopModeEventLogger)
+
+        // task 2 resize
+        val newTaskInfo2 =
+            createTaskInfo(
+                WINDOWING_MODE_FREEFORM,
+                id = 2,
+                taskWidth = DEFAULT_TASK_WIDTH + 100,
+                taskHeight = DEFAULT_TASK_HEIGHT - 100,
+            )
+        val transitionInfo2 =
+            TransitionInfoBuilder(TRANSIT_CHANGE, 0)
+                .addChange(createChange(TRANSIT_CHANGE, newTaskInfo2))
+                .build()
+
+        callOnTransitionReady(transitionInfo2)
+
+        verify(desktopModeEventLogger, times(1))
+            .logTaskInfoChanged(
+                eq(
+                    DEFAULT_TASK_UPDATE.copy(
+                        instanceId = 2,
+                        taskWidth = DEFAULT_TASK_WIDTH + 100,
+                        taskHeight = DEFAULT_TASK_HEIGHT - 100,
+                        visibleTaskCount = 2,
+                    )
+                )
+            )
+        verifyZeroInteractions(desktopModeEventLogger)
+    }
+
+    @Test
+    fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
+        // add two existing freeform tasks
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+        transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2))
+        transitionObserver.isSessionActive = true
+
+        // new freeform task closed
+        val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2))
+        val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
+        callOnTransitionReady(transitionInfo)
+
+        verify(desktopModeEventLogger, times(1))
+            .logTaskRemoved(eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2, visibleTaskCount = 1)))
+        verify(desktopModeEventLogger, never()).logSessionExit(any())
+    }
+
+    /** Simulate calling the onTransitionReady() method */
+    private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+        val transition = mock<IBinder>()
+        val startT = mock<SurfaceControl.Transaction>()
+        val finishT = mock<SurfaceControl.Transaction>()
+
+        transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
+    }
+
+    private fun verifyTaskAddedAndEnterLogging(enterReason: EnterReason, taskUpdate: TaskUpdate) {
+        assertTrue(transitionObserver.isSessionActive)
+        verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(enterReason))
+        verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(taskUpdate))
+        ExtendedMockito.verify {
+            Trace.setCounter(
+                eq(Trace.TRACE_TAG_WINDOW_MANAGER),
+                eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_NAME),
+                eq(taskUpdate.visibleTaskCount.toLong()),
+            )
+        }
+        ExtendedMockito.verify {
+            SystemProperties.set(
+                eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY),
+                eq(taskUpdate.visibleTaskCount.toString()),
+            )
+        }
+        verifyZeroInteractions(desktopModeEventLogger)
+    }
+
+    private fun verifyTaskRemovedAndExitLogging(exitReason: ExitReason, taskUpdate: TaskUpdate) {
+        assertFalse(transitionObserver.isSessionActive)
+        verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(taskUpdate))
+        verify(desktopModeEventLogger, times(1)).logSessionExit(eq(exitReason))
+        verifyZeroInteractions(desktopModeEventLogger)
+    }
+
+    private companion object {
+        const val DEFAULT_TASK_ID = 1
+        const val DEFAULT_TASK_UID = 2
+        const val DEFAULT_TASK_HEIGHT = 100
+        const val DEFAULT_TASK_WIDTH = 200
+        const val DEFAULT_TASK_X = 30
+        const val DEFAULT_TASK_Y = 70
+        const val DEFAULT_VISIBLE_TASK_COUNT = 0
+        val DEFAULT_TASK_UPDATE =
+            TaskUpdate(
+                DEFAULT_TASK_ID,
+                DEFAULT_TASK_UID,
+                DEFAULT_TASK_HEIGHT,
+                DEFAULT_TASK_WIDTH,
+                DEFAULT_TASK_X,
+                DEFAULT_TASK_Y,
+                visibleTaskCount = DEFAULT_VISIBLE_TASK_COUNT,
+            )
+
+        fun createTaskInfo(
+            windowMode: Int,
+            id: Int = DEFAULT_TASK_ID,
+            uid: Int = DEFAULT_TASK_UID,
+            taskHeight: Int = DEFAULT_TASK_HEIGHT,
+            taskWidth: Int = DEFAULT_TASK_WIDTH,
+            taskX: Int = DEFAULT_TASK_X,
+            taskY: Int = DEFAULT_TASK_Y,
+        ) =
+            ActivityManager.RunningTaskInfo().apply {
+                taskId = id
+                effectiveUid = uid
+                configuration.windowConfiguration.apply {
+                    windowingMode = windowMode
+                    positionInParent = Point(taskX, taskY)
+                    bounds.set(Rect(taskX, taskY, taskX + taskWidth, taskY + taskHeight))
+                }
+            }
+
+        fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
+            val change =
+                Change(WindowContainerToken(mock<IWindowContainerToken>()), mock<SurfaceControl>())
+            change.mode = mode
+            change.taskInfo = taskInfo
+            return change
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt
index db4e93d..f6eed5d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt
@@ -18,18 +18,18 @@
 
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType
-import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_FROM_OVERVIEW
+import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.KEYBOARD_SHORTCUT
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.TASK_DRAG
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN
@@ -53,8 +53,7 @@
             .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON)
         assertThat(APP_FROM_OVERVIEW.getEnterTransitionType())
             .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW)
-        assertThat(TASK_DRAG.getEnterTransitionType())
-            .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN)
+        assertThat(TASK_DRAG.getEnterTransitionType()).isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN)
         assertThat(KEYBOARD_SHORTCUT.getEnterTransitionType())
             .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT)
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt
index 94698e2..72b1fd9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.desktopmode
 
-
 import android.content.ComponentName
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
@@ -67,8 +66,7 @@
 
     @Test
     fun log_eventLogged() {
-        val event =
-            DESKTOP_WINDOW_EDGE_DRAG_RESIZE
+        val event = DESKTOP_WINDOW_EDGE_DRAG_RESIZE
         logger.log(UID, PACKAGE_NAME, event)
         assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
         assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(event.id)
@@ -97,8 +95,7 @@
 
     @Test
     fun logWithInstanceId_eventLogged() {
-        val event =
-            DESKTOP_WINDOW_EDGE_DRAG_RESIZE
+        val event = DESKTOP_WINDOW_EDGE_DRAG_RESIZE
         logger.logWithInstanceId(INSTANCE_ID, UID, PACKAGE_NAME, event)
         assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
         assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(event.id)
@@ -109,12 +106,12 @@
 
     @Test
     fun logWithTaskInfo_eventLogged() {
-        val event =
-            DESKTOP_WINDOW_EDGE_DRAG_RESIZE
-        val taskInfo = TestRunningTaskInfoBuilder()
-            .setUserId(USER_ID)
-            .setBaseActivity(ComponentName(PACKAGE_NAME, "test"))
-            .build()
+        val event = DESKTOP_WINDOW_EDGE_DRAG_RESIZE
+        val taskInfo =
+            TestRunningTaskInfoBuilder()
+                .setUserId(USER_ID)
+                .setBaseActivity(ComponentName(PACKAGE_NAME, "test"))
+                .build()
         whenever(mockPackageManager.getApplicationInfoAsUser(PACKAGE_NAME, /* flags= */ 0, USER_ID))
             .thenReturn(ApplicationInfo().apply { uid = UID })
         logger.log(taskInfo, event)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index 935e6d0..e46d2c7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -66,74 +66,109 @@
     fun testFullscreenRegionCalculation() {
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
         var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT)
-        assertThat(testRegion.bounds).isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400,
-            2 * STABLE_INSETS.top))
+        assertThat(testRegion.bounds)
+            .isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400, 2 * STABLE_INSETS.top))
 
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM)
         testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT)
         val transitionHeight = SystemBarUtils.getStatusBarHeight(context)
-        val toFullscreenScale = mContext.resources.getFloat(
-            R.dimen.desktop_mode_fullscreen_region_scale
-        )
+        val toFullscreenScale =
+            mContext.resources.getFloat(R.dimen.desktop_mode_fullscreen_region_scale)
         val toFullscreenWidth = displayLayout.width() * toFullscreenScale
-        assertThat(testRegion.bounds).isEqualTo(Rect(
-            (DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(),
-            Short.MIN_VALUE.toInt(),
-            (DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(),
-            transitionHeight))
+        assertThat(testRegion.bounds)
+            .isEqualTo(
+                Rect(
+                    (DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(),
+                    Short.MIN_VALUE.toInt(),
+                    (DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(),
+                    transitionHeight,
+                )
+            )
 
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
         testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT)
-        assertThat(testRegion.bounds).isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400,
-            2 * STABLE_INSETS.top))
+        assertThat(testRegion.bounds)
+            .isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400, 2 * STABLE_INSETS.top))
 
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
         testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT)
-        assertThat(testRegion.bounds).isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400,
-            transitionHeight))
+        assertThat(testRegion.bounds)
+            .isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400, transitionHeight))
     }
 
     @Test
     fun testSplitLeftRegionCalculation() {
-        val transitionHeight = context.resources.getDimensionPixelSize(
-            R.dimen.desktop_mode_split_from_desktop_height)
+        val transitionHeight =
+            context.resources.getDimensionPixelSize(R.dimen.desktop_mode_split_from_desktop_height)
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
-        var testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout,
-            TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
+        var testRegion =
+            visualIndicator.calculateSplitLeftRegion(
+                displayLayout,
+                TRANSITION_AREA_WIDTH,
+                CAPTION_HEIGHT,
+            )
         assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600))
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM)
-        testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout,
-            TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
+        testRegion =
+            visualIndicator.calculateSplitLeftRegion(
+                displayLayout,
+                TRANSITION_AREA_WIDTH,
+                CAPTION_HEIGHT,
+            )
         assertThat(testRegion.bounds).isEqualTo(Rect(0, transitionHeight, 32, 1600))
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
-        testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout,
-            TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
+        testRegion =
+            visualIndicator.calculateSplitLeftRegion(
+                displayLayout,
+                TRANSITION_AREA_WIDTH,
+                CAPTION_HEIGHT,
+            )
         assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600))
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
-        testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout,
-            TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
+        testRegion =
+            visualIndicator.calculateSplitLeftRegion(
+                displayLayout,
+                TRANSITION_AREA_WIDTH,
+                CAPTION_HEIGHT,
+            )
         assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600))
     }
 
     @Test
     fun testSplitRightRegionCalculation() {
-        val transitionHeight = context.resources.getDimensionPixelSize(
-            R.dimen.desktop_mode_split_from_desktop_height)
+        val transitionHeight =
+            context.resources.getDimensionPixelSize(R.dimen.desktop_mode_split_from_desktop_height)
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
-        var testRegion = visualIndicator.calculateSplitRightRegion(displayLayout,
-            TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
+        var testRegion =
+            visualIndicator.calculateSplitRightRegion(
+                displayLayout,
+                TRANSITION_AREA_WIDTH,
+                CAPTION_HEIGHT,
+            )
         assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600))
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM)
-        testRegion = visualIndicator.calculateSplitRightRegion(displayLayout,
-            TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
+        testRegion =
+            visualIndicator.calculateSplitRightRegion(
+                displayLayout,
+                TRANSITION_AREA_WIDTH,
+                CAPTION_HEIGHT,
+            )
         assertThat(testRegion.bounds).isEqualTo(Rect(2368, transitionHeight, 2400, 1600))
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
-        testRegion = visualIndicator.calculateSplitRightRegion(displayLayout,
-            TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
+        testRegion =
+            visualIndicator.calculateSplitRightRegion(
+                displayLayout,
+                TRANSITION_AREA_WIDTH,
+                CAPTION_HEIGHT,
+            )
         assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600))
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
-        testRegion = visualIndicator.calculateSplitRightRegion(displayLayout,
-            TRANSITION_AREA_WIDTH, CAPTION_HEIGHT)
+        testRegion =
+            visualIndicator.calculateSplitRightRegion(
+                displayLayout,
+                TRANSITION_AREA_WIDTH,
+                CAPTION_HEIGHT,
+            )
         assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600))
     }
 
@@ -141,10 +176,12 @@
     fun testDefaultIndicators() {
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
         var result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f))
-        assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR)
+        assertThat(result)
+            .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR)
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
         result = visualIndicator.updateIndicatorType(PointF(10000f, 500f))
-        assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR)
+        assertThat(result)
+            .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR)
         createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
         result = visualIndicator.updateIndicatorType(PointF(500f, 10000f))
         assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
@@ -154,8 +191,16 @@
     }
 
     private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) {
-        visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo, displayController,
-            context, taskSurface, taskDisplayAreaOrganizer, dragStartState)
+        visualIndicator =
+            DesktopModeVisualIndicator(
+                syncQueue,
+                taskInfo,
+                displayController,
+                context,
+                taskSurface,
+                taskDisplayAreaOrganizer,
+                dragStartState,
+            )
     }
 
     companion object {
@@ -163,11 +208,12 @@
         private const val CAPTION_HEIGHT = 50
         private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
         private const val NAVBAR_HEIGHT = 50
-        private val STABLE_INSETS = Rect(
-            DISPLAY_BOUNDS.left,
-            DISPLAY_BOUNDS.top + CAPTION_HEIGHT,
-            DISPLAY_BOUNDS.right,
-            DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT
-        )
+        private val STABLE_INSETS =
+            Rect(
+                DISPLAY_BOUNDS.left,
+                DISPLAY_BOUNDS.top + CAPTION_HEIGHT,
+                DISPLAY_BOUNDS.right,
+                DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT,
+            )
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 344140d..e777ec7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -76,15 +76,9 @@
         datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
         shellInit = spy(ShellInit(testExecutor))
 
-        repo =
-            DesktopRepository(
-                persistentRepository,
-                datastoreScope,
-                DEFAULT_USER_ID
-            )
-        whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn(
-            Desktop.getDefaultInstance()
-        )
+        repo = DesktopRepository(persistentRepository, datastoreScope, DEFAULT_USER_ID)
+        whenever(runBlocking { persistentRepository.readDesktop(any(), any()) })
+            .thenReturn(Desktop.getDefaultInstance())
         shellInit.init()
     }
 
@@ -245,7 +239,7 @@
                         DEFAULT_DESKTOP_ID,
                         visibleTasks = ArraySet(arrayOf(1)),
                         minimizedTasks = ArraySet(),
-                        freeformTasksInZOrder = arrayListOf()
+                        freeformTasksInZOrder = arrayListOf(),
                     )
                 verify(persistentRepository)
                     .addOrUpdateDesktop(
@@ -253,7 +247,7 @@
                         DEFAULT_DESKTOP_ID,
                         visibleTasks = ArraySet(arrayOf(1, 2)),
                         minimizedTasks = ArraySet(),
-                        freeformTasksInZOrder = arrayListOf()
+                        freeformTasksInZOrder = arrayListOf(),
                     )
             }
         }
@@ -441,8 +435,8 @@
     }
 
     /**
-     * When a task vanishes, the displayId of the task is set to INVALID_DISPLAY.
-     * This tests that task is removed from the last parent display when it vanishes.
+     * When a task vanishes, the displayId of the task is set to INVALID_DISPLAY. This tests that
+     * task is removed from the last parent display when it vanishes.
      */
     @Test
     fun updateTask_removeVisibleTasksRemovesTaskWithInvalidDisplay() {
@@ -562,7 +556,7 @@
                         DEFAULT_DESKTOP_ID,
                         visibleTasks = ArraySet(),
                         minimizedTasks = ArraySet(),
-                        freeformTasksInZOrder = arrayListOf(5)
+                        freeformTasksInZOrder = arrayListOf(5),
                     )
                 verify(persistentRepository)
                     .addOrUpdateDesktop(
@@ -570,7 +564,7 @@
                         DEFAULT_DESKTOP_ID,
                         visibleTasks = ArraySet(arrayOf(5)),
                         minimizedTasks = ArraySet(),
-                        freeformTasksInZOrder = arrayListOf(6, 5)
+                        freeformTasksInZOrder = arrayListOf(6, 5),
                     )
                 verify(persistentRepository)
                     .addOrUpdateDesktop(
@@ -578,10 +572,10 @@
                         DEFAULT_DESKTOP_ID,
                         visibleTasks = ArraySet(arrayOf(5, 6)),
                         minimizedTasks = ArraySet(),
-                        freeformTasksInZOrder = arrayListOf(7, 6, 5)
+                        freeformTasksInZOrder = arrayListOf(7, 6, 5),
                     )
             }
-    }
+        }
 
     @Test
     fun addTask_alreadyExists_movesToTop() {
@@ -628,7 +622,7 @@
                         DEFAULT_DESKTOP_ID,
                         visibleTasks = ArraySet(),
                         minimizedTasks = ArraySet(),
-                        freeformTasksInZOrder = arrayListOf(5)
+                        freeformTasksInZOrder = arrayListOf(5),
                     )
                 verify(persistentRepository)
                     .addOrUpdateDesktop(
@@ -636,7 +630,7 @@
                         DEFAULT_DESKTOP_ID,
                         visibleTasks = ArraySet(arrayOf(5)),
                         minimizedTasks = ArraySet(),
-                        freeformTasksInZOrder = arrayListOf(6, 5)
+                        freeformTasksInZOrder = arrayListOf(6, 5),
                     )
                 verify(persistentRepository)
                     .addOrUpdateDesktop(
@@ -644,7 +638,7 @@
                         DEFAULT_DESKTOP_ID,
                         visibleTasks = ArraySet(arrayOf(5, 6)),
                         minimizedTasks = ArraySet(),
-                        freeformTasksInZOrder = arrayListOf(7, 6, 5)
+                        freeformTasksInZOrder = arrayListOf(7, 6, 5),
                     )
                 verify(persistentRepository, times(2))
                     .addOrUpdateDesktop(
@@ -652,10 +646,10 @@
                         DEFAULT_DESKTOP_ID,
                         visibleTasks = ArraySet(arrayOf(5, 7)),
                         minimizedTasks = ArraySet(arrayOf(6)),
-                        freeformTasksInZOrder = arrayListOf(7, 6, 5)
+                        freeformTasksInZOrder = arrayListOf(7, 6, 5),
                     )
             }
-    }
+        }
 
     @Test
     fun addTask_taskIsUnminimized_noop() {
@@ -694,7 +688,7 @@
                     DEFAULT_DESKTOP_ID,
                     visibleTasks = ArraySet(),
                     minimizedTasks = ArraySet(),
-                    freeformTasksInZOrder = arrayListOf(1)
+                    freeformTasksInZOrder = arrayListOf(1),
                 )
             verify(persistentRepository)
                 .addOrUpdateDesktop(
@@ -702,7 +696,7 @@
                     DEFAULT_DESKTOP_ID,
                     visibleTasks = ArraySet(),
                     minimizedTasks = ArraySet(),
-                    freeformTasksInZOrder = ArrayList()
+                    freeformTasksInZOrder = ArrayList(),
                 )
         }
     }
@@ -731,7 +725,7 @@
                     DEFAULT_DESKTOP_ID,
                     visibleTasks = ArraySet(),
                     minimizedTasks = ArraySet(),
-                    freeformTasksInZOrder = arrayListOf(1)
+                    freeformTasksInZOrder = arrayListOf(1),
                 )
             verify(persistentRepository)
                 .addOrUpdateDesktop(
@@ -739,7 +733,7 @@
                     DEFAULT_DESKTOP_ID,
                     visibleTasks = ArraySet(),
                     minimizedTasks = ArraySet(),
-                    freeformTasksInZOrder = ArrayList()
+                    freeformTasksInZOrder = ArrayList(),
                 )
         }
     }
@@ -768,7 +762,7 @@
                     DEFAULT_DESKTOP_ID,
                     visibleTasks = ArraySet(),
                     minimizedTasks = ArraySet(),
-                    freeformTasksInZOrder = arrayListOf(1)
+                    freeformTasksInZOrder = arrayListOf(1),
                 )
             verify(persistentRepository, never())
                 .addOrUpdateDesktop(
@@ -776,7 +770,7 @@
                     DEFAULT_DESKTOP_ID,
                     visibleTasks = ArraySet(),
                     minimizedTasks = ArraySet(),
-                    freeformTasksInZOrder = ArrayList()
+                    freeformTasksInZOrder = ArrayList(),
                 )
         }
     }
@@ -928,7 +922,6 @@
         assertThat(repo.isMinimizedTask(taskId = 2)).isFalse()
     }
 
-
     @Test
     fun updateTask_minimizedTaskBecomesVisible_unminimizesTask() {
         repo.minimizeTask(displayId = 10, taskId = 2)
@@ -1056,6 +1049,7 @@
     class TestListener : DesktopRepository.ActiveTasksListener {
         var activeChangesOnDefaultDisplay = 0
         var activeChangesOnSecondaryDisplay = 0
+
         override fun onActiveTasksChanged(displayId: Int) {
             when (displayId) {
                 DEFAULT_DISPLAY -> activeChangesOnDefaultDisplay++
@@ -1093,4 +1087,4 @@
         private const val DEFAULT_USER_ID = 1000
         private const val DEFAULT_DESKTOP_ID = 0
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt
index b4daa66..19ab911 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt
@@ -22,7 +22,6 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-import com.android.wm.shell.desktopmode.DesktopUserRepositories
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask
@@ -45,167 +44,144 @@
 @RunWith(AndroidTestingRunner::class)
 class DesktopTaskChangeListenerTest : ShellTestCase() {
 
-  @JvmField @Rule val setFlagsRule = SetFlagsRule()
+    @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
-  private lateinit var desktopTaskChangeListener: DesktopTaskChangeListener
+    private lateinit var desktopTaskChangeListener: DesktopTaskChangeListener
 
-  private val desktopUserRepositories = mock<DesktopUserRepositories>()
-  private val desktopRepository = mock<DesktopRepository>()
+    private val desktopUserRepositories = mock<DesktopUserRepositories>()
+    private val desktopRepository = mock<DesktopRepository>()
 
-  @Before
-  fun setUp() {
-    desktopTaskChangeListener = DesktopTaskChangeListener(desktopUserRepositories)
+    @Before
+    fun setUp() {
+        desktopTaskChangeListener = DesktopTaskChangeListener(desktopUserRepositories)
 
-    whenever(desktopUserRepositories.current).thenReturn(desktopRepository)
-    whenever(desktopUserRepositories.getProfile(anyInt())).thenReturn(desktopRepository)
-  }
+        whenever(desktopUserRepositories.current).thenReturn(desktopRepository)
+        whenever(desktopUserRepositories.getProfile(anyInt())).thenReturn(desktopRepository)
+    }
 
-  @Test
-  fun onTaskOpening_fullscreenTask_notActiveDesktopTask_noop() {
-    val task = createFullscreenTask().apply { isVisible = true }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(false)
+    @Test
+    fun onTaskOpening_fullscreenTask_notActiveDesktopTask_noop() {
+        val task = createFullscreenTask().apply { isVisible = true }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(false)
 
-    desktopTaskChangeListener.onTaskOpening(task)
+        desktopTaskChangeListener.onTaskOpening(task)
 
-    verify(desktopUserRepositories.current, never())
-        .addTask(task.displayId, task.taskId, task.isVisible)
-    verify(desktopUserRepositories.current, never())
-        .removeFreeformTask(task.displayId, task.taskId)
-  }
+        verify(desktopUserRepositories.current, never())
+            .addTask(task.displayId, task.taskId, task.isVisible)
+        verify(desktopUserRepositories.current, never())
+            .removeFreeformTask(task.displayId, task.taskId)
+    }
 
-  @Test
-  fun onTaskOpening_freeformTask_activeDesktopTask_removesTaskFromRepo() {
-    val task = createFullscreenTask().apply { isVisible = true }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(true)
+    @Test
+    fun onTaskOpening_freeformTask_activeDesktopTask_removesTaskFromRepo() {
+        val task = createFullscreenTask().apply { isVisible = true }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true)
 
-    desktopTaskChangeListener.onTaskOpening(task)
+        desktopTaskChangeListener.onTaskOpening(task)
 
-    verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
-  }
+        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+    }
 
-  @Test
-  fun onTaskOpening_freeformTask_visibleDesktopTask_addsTaskToRepository() {
-    val task = createFreeformTask().apply { isVisible = true }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(false)
+    @Test
+    fun onTaskOpening_freeformTask_visibleDesktopTask_addsTaskToRepository() {
+        val task = createFreeformTask().apply { isVisible = true }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(false)
 
-    desktopTaskChangeListener.onTaskOpening(task)
+        desktopTaskChangeListener.onTaskOpening(task)
 
-    verify(desktopUserRepositories.current)
-        .addTask(task.displayId, task.taskId, task.isVisible)
-  }
+        verify(desktopUserRepositories.current).addTask(task.displayId, task.taskId, task.isVisible)
+    }
 
-  @Test
-  fun onTaskOpening_freeformTask_nonVisibleDesktopTask_addsTaskToRepository() {
-    val task = createFreeformTask().apply { isVisible = false }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(true)
+    @Test
+    fun onTaskOpening_freeformTask_nonVisibleDesktopTask_addsTaskToRepository() {
+        val task = createFreeformTask().apply { isVisible = false }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true)
 
-    desktopTaskChangeListener.onTaskOpening(task)
+        desktopTaskChangeListener.onTaskOpening(task)
 
-    verify(desktopUserRepositories.current)
-        .addTask(task.displayId, task.taskId, task.isVisible)
-  }
+        verify(desktopUserRepositories.current).addTask(task.displayId, task.taskId, task.isVisible)
+    }
 
-  @Test
-  fun onTaskChanging_freeformTaskOutsideDesktop_removesTaskFromRepo() {
-    val task = createFullscreenTask().apply { isVisible = true }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(true)
+    @Test
+    fun onTaskChanging_freeformTaskOutsideDesktop_removesTaskFromRepo() {
+        val task = createFullscreenTask().apply { isVisible = true }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true)
 
-    desktopTaskChangeListener.onTaskChanging(task)
+        desktopTaskChangeListener.onTaskChanging(task)
 
-    verify(desktopUserRepositories.current)
-        .removeFreeformTask(task.displayId, task.taskId)
-  }
+        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+    }
 
-  @Test
-  fun onTaskChanging_visibleTaskInDesktop_updatesTaskVisibility() {
-    val task = createFreeformTask().apply { isVisible = true }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(true)
+    @Test
+    fun onTaskChanging_visibleTaskInDesktop_updatesTaskVisibility() {
+        val task = createFreeformTask().apply { isVisible = true }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true)
 
-    desktopTaskChangeListener.onTaskChanging(task)
+        desktopTaskChangeListener.onTaskChanging(task)
 
-    verify(desktopUserRepositories.current)
-        .updateTask(task.displayId, task.taskId, task.isVisible)
-  }
+        verify(desktopUserRepositories.current)
+            .updateTask(task.displayId, task.taskId, task.isVisible)
+    }
 
-  @Test
-  fun onTaskChanging_nonVisibleTask_updatesTaskVisibility() {
-    val task = createFreeformTask().apply { isVisible = false }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(true)
+    @Test
+    fun onTaskChanging_nonVisibleTask_updatesTaskVisibility() {
+        val task = createFreeformTask().apply { isVisible = false }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true)
 
-    desktopTaskChangeListener.onTaskChanging(task)
+        desktopTaskChangeListener.onTaskChanging(task)
 
-    verify(desktopUserRepositories.current)
-        .updateTask(task.displayId, task.taskId, task.isVisible)
-  }
+        verify(desktopUserRepositories.current)
+            .updateTask(task.displayId, task.taskId, task.isVisible)
+    }
 
-  @Test
-  fun onTaskMovingToFront_freeformTaskOutsideDesktop_removesTaskFromRepo() {
-    val task = createFullscreenTask().apply { isVisible = true }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(true)
+    @Test
+    fun onTaskMovingToFront_freeformTaskOutsideDesktop_removesTaskFromRepo() {
+        val task = createFullscreenTask().apply { isVisible = true }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true)
 
-    desktopTaskChangeListener.onTaskMovingToFront(task)
+        desktopTaskChangeListener.onTaskMovingToFront(task)
 
-    verify(desktopUserRepositories.current)
-        .removeFreeformTask(task.displayId, task.taskId)
-  }
+        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+    }
 
-  @Test
-  @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun onTaskClosing_backNavEnabled_nonClosingTask_minimizesTaskInRepo() {
-    val task = createFreeformTask().apply { isVisible = true }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(true)
-    whenever(desktopUserRepositories.current.isClosingTask(task.taskId))
-        .thenReturn(false)
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun onTaskClosing_backNavEnabled_nonClosingTask_minimizesTaskInRepo() {
+        val task = createFreeformTask().apply { isVisible = true }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true)
+        whenever(desktopUserRepositories.current.isClosingTask(task.taskId)).thenReturn(false)
 
-    desktopTaskChangeListener.onTaskClosing(task)
+        desktopTaskChangeListener.onTaskClosing(task)
 
-    verify(desktopUserRepositories.current)
-        .updateTask(task.displayId, task.taskId, isVisible = false)
-    verify(desktopUserRepositories.current)
-        .minimizeTask(task.displayId, task.taskId)
-  }
+        verify(desktopUserRepositories.current)
+            .updateTask(task.displayId, task.taskId, isVisible = false)
+        verify(desktopUserRepositories.current).minimizeTask(task.displayId, task.taskId)
+    }
 
-  @Test
-  @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun onTaskClosing_backNavDisabled_closingTask_removesTaskInRepo() {
-    val task = createFreeformTask().apply { isVisible = true }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(true)
-    whenever(desktopUserRepositories.current.isClosingTask(task.taskId))
-        .thenReturn(true)
+    @Test
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun onTaskClosing_backNavDisabled_closingTask_removesTaskInRepo() {
+        val task = createFreeformTask().apply { isVisible = true }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true)
+        whenever(desktopUserRepositories.current.isClosingTask(task.taskId)).thenReturn(true)
 
-    desktopTaskChangeListener.onTaskClosing(task)
+        desktopTaskChangeListener.onTaskClosing(task)
 
-    verify(desktopUserRepositories.current, never())
-        .minimizeTask(task.displayId, task.taskId)
-    verify(desktopUserRepositories.current)
-        .removeClosingTask(task.taskId)
-    verify(desktopUserRepositories.current)
-        .removeFreeformTask(task.displayId, task.taskId)
-  }
+        verify(desktopUserRepositories.current, never()).minimizeTask(task.displayId, task.taskId)
+        verify(desktopUserRepositories.current).removeClosingTask(task.taskId)
+        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+    }
 
-  @Test
-  @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun onTaskClosing_backNavEnabled_closingTask_removesTaskFromRepo() {
-    val task = createFreeformTask().apply { isVisible = true }
-    whenever(desktopUserRepositories.current.isActiveTask(task.taskId))
-        .thenReturn(true)
-    whenever(desktopUserRepositories.current.isClosingTask(task.taskId))
-        .thenReturn(true)
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun onTaskClosing_backNavEnabled_closingTask_removesTaskFromRepo() {
+        val task = createFreeformTask().apply { isVisible = true }
+        whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true)
+        whenever(desktopUserRepositories.current.isClosingTask(task.taskId)).thenReturn(true)
 
-    desktopTaskChangeListener.onTaskClosing(task)
+        desktopTaskChangeListener.onTaskClosing(task)
 
-    verify(desktopUserRepositories.current).removeClosingTask(task.taskId)
-    verify(desktopUserRepositories.current)
-        .removeFreeformTask(task.displayId, task.taskId)
-  }
+        verify(desktopUserRepositories.current).removeClosingTask(task.taskId)
+        verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId)
+    }
 }
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 0b12d22..0eb88e3 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
@@ -81,9 +81,9 @@
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.window.flags.Flags
-import com.android.window.flags.Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
 import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP
+import com.android.window.flags.Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY
 import com.android.wm.shell.MockToken
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -96,7 +96,6 @@
 import com.android.wm.shell.common.MultiInstanceHelper
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
-import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
 import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
@@ -109,6 +108,7 @@
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createSplitScreenTask
 import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION
 import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCREEN_ANIMATION_DURATION
+import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
 import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler
 import com.android.wm.shell.desktopmode.persistence.Desktop
 import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
@@ -137,8 +137,8 @@
 import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
-import java.util.function.Consumer
 import java.util.Optional
+import java.util.function.Consumer
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 import kotlin.test.assertIs
@@ -167,8 +167,8 @@
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.spy
-import org.mockito.Mockito.verify
 import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.argumentCaptor
@@ -189,4415 +189,4737 @@
 @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
 class DesktopTasksControllerTest : ShellTestCase() {
 
-  @JvmField @Rule val setFlagsRule = SetFlagsRule()
+    @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
-  @Mock lateinit var testExecutor: ShellExecutor
-  @Mock lateinit var shellCommandHandler: ShellCommandHandler
-  @Mock lateinit var shellController: ShellController
-  @Mock lateinit var displayController: DisplayController
-  @Mock lateinit var displayLayout: DisplayLayout
-  @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
-  @Mock lateinit var syncQueue: SyncTransactionQueue
-  @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
-  @Mock lateinit var transitions: Transitions
-  @Mock lateinit var keyguardManager: KeyguardManager
-  @Mock lateinit var mReturnToDragStartAnimator: ReturnToDragStartAnimator
-  @Mock lateinit var desktopMixedTransitionHandler: DesktopMixedTransitionHandler
-  @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
-  @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
-  @Mock lateinit var dragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler
-  @Mock
-  lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
-  @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
-  @Mock
-  lateinit var mMockDesktopImmersiveController: DesktopImmersiveController
-  @Mock lateinit var splitScreenController: SplitScreenController
-  @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
-  @Mock lateinit var dragAndDropController: DragAndDropController
-  @Mock lateinit var multiInstanceHelper: MultiInstanceHelper
-  @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
-  @Mock lateinit var recentTasksController: RecentTasksController
-  @Mock
-  private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
-  @Mock private lateinit var mockSurface: SurfaceControl
-  @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
-  @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
-  @Mock private lateinit var mockHandler: Handler
-  @Mock private lateinit var desktopModeEventLogger: DesktopModeEventLogger
-  @Mock private lateinit var desktopModeUiEventLogger: DesktopModeUiEventLogger
-  @Mock lateinit var persistentRepository: DesktopPersistentRepository
-  @Mock lateinit var motionEvent: MotionEvent
-  @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer
-  @Mock private lateinit var mockToast: Toast
-  private lateinit var mockitoSession: StaticMockitoSession
-  @Mock
-  private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
-  @Mock
-  private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration
-  @Mock private lateinit var resources: Resources
-  @Mock
-  lateinit var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener
-  @Mock private lateinit var userManager: UserManager
-  private lateinit var controller: DesktopTasksController
-  private lateinit var shellInit: ShellInit
-  private lateinit var taskRepository: DesktopRepository
-  private lateinit var userRepositories: DesktopUserRepositories
-  private lateinit var desktopTasksLimiter: DesktopTasksLimiter
-  private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener
-  private lateinit var testScope: CoroutineScope
+    @Mock lateinit var testExecutor: ShellExecutor
+    @Mock lateinit var shellCommandHandler: ShellCommandHandler
+    @Mock lateinit var shellController: ShellController
+    @Mock lateinit var displayController: DisplayController
+    @Mock lateinit var displayLayout: DisplayLayout
+    @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
+    @Mock lateinit var syncQueue: SyncTransactionQueue
+    @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+    @Mock lateinit var transitions: Transitions
+    @Mock lateinit var keyguardManager: KeyguardManager
+    @Mock lateinit var mReturnToDragStartAnimator: ReturnToDragStartAnimator
+    @Mock lateinit var desktopMixedTransitionHandler: DesktopMixedTransitionHandler
+    @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
+    @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
+    @Mock lateinit var dragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler
+    @Mock
+    lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
+    @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
+    @Mock lateinit var mMockDesktopImmersiveController: DesktopImmersiveController
+    @Mock lateinit var splitScreenController: SplitScreenController
+    @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
+    @Mock lateinit var dragAndDropController: DragAndDropController
+    @Mock lateinit var multiInstanceHelper: MultiInstanceHelper
+    @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
+    @Mock lateinit var recentTasksController: RecentTasksController
+    @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
+    @Mock private lateinit var mockSurface: SurfaceControl
+    @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
+    @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
+    @Mock private lateinit var mockHandler: Handler
+    @Mock private lateinit var desktopModeEventLogger: DesktopModeEventLogger
+    @Mock private lateinit var desktopModeUiEventLogger: DesktopModeUiEventLogger
+    @Mock lateinit var persistentRepository: DesktopPersistentRepository
+    @Mock lateinit var motionEvent: MotionEvent
+    @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer
+    @Mock private lateinit var mockToast: Toast
+    private lateinit var mockitoSession: StaticMockitoSession
+    @Mock private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
+    @Mock private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration
+    @Mock private lateinit var resources: Resources
+    @Mock
+    lateinit var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener
+    @Mock private lateinit var userManager: UserManager
+    private lateinit var controller: DesktopTasksController
+    private lateinit var shellInit: ShellInit
+    private lateinit var taskRepository: DesktopRepository
+    private lateinit var userRepositories: DesktopUserRepositories
+    private lateinit var desktopTasksLimiter: DesktopTasksLimiter
+    private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener
+    private lateinit var testScope: CoroutineScope
 
-  private val shellExecutor = TestShellExecutor()
+    private val shellExecutor = TestShellExecutor()
 
-  // Mock running tasks are registered here so we can get the list from mock shell task organizer
-  private val runningTasks = mutableListOf<RunningTaskInfo>()
+    // Mock running tasks are registered here so we can get the list from mock shell task organizer
+    private val runningTasks = mutableListOf<RunningTaskInfo>()
 
-  private val DISPLAY_DIMENSION_SHORT = 1600
-  private val DISPLAY_DIMENSION_LONG = 2560
-  private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 75, 2240, 1275)
-  private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 165, 1400, 2085)
-  private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 435, 1575, 1635)
-  private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 75, 1880, 1275)
-  private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611)
-  private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275)
+    private val DISPLAY_DIMENSION_SHORT = 1600
+    private val DISPLAY_DIMENSION_LONG = 2560
+    private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 75, 2240, 1275)
+    private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 165, 1400, 2085)
+    private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 435, 1575, 1635)
+    private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 75, 1880, 1275)
+    private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611)
+    private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275)
 
-  @Before
-  fun setUp() {
-    Dispatchers.setMain(StandardTestDispatcher())
-    mockitoSession =
-        mockitoSession()
-            .strictness(Strictness.LENIENT)
-            .spyStatic(DesktopModeStatus::class.java)
-            .spyStatic(Toast::class.java)
-            .startMocking()
-    doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+    @Before
+    fun setUp() {
+        Dispatchers.setMain(StandardTestDispatcher())
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DesktopModeStatus::class.java)
+                .spyStatic(Toast::class.java)
+                .startMocking()
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
 
-    testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
-    shellInit = spy(ShellInit(testExecutor))
-    userRepositories =
-      DesktopUserRepositories(
-        context,
-        shellInit,
-        shellController,
-        persistentRepository,
-        repositoryInitializer,
-        testScope,
-        userManager)
-    desktopTasksLimiter =
-        DesktopTasksLimiter(
-            transitions,
-            userRepositories,
-            shellTaskOrganizer,
-            MAX_TASK_LIMIT,
-            mockInteractionJankMonitor,
-            mContext,
-            mockHandler)
+        testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
+        shellInit = spy(ShellInit(testExecutor))
+        userRepositories =
+            DesktopUserRepositories(
+                context,
+                shellInit,
+                shellController,
+                persistentRepository,
+                repositoryInitializer,
+                testScope,
+                userManager,
+            )
+        desktopTasksLimiter =
+            DesktopTasksLimiter(
+                transitions,
+                userRepositories,
+                shellTaskOrganizer,
+                MAX_TASK_LIMIT,
+                mockInteractionJankMonitor,
+                mContext,
+                mockHandler,
+            )
 
-    whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
-    whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
-    whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() }
-    whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
-    whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
-      (i.arguments.first() as Rect).set(STABLE_BOUNDS)
-    }
-    whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn(
-      Desktop.getDefaultInstance()
-    )
-    doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) }
-
-    val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-    whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
-    whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), any<RunningTaskInfo>(), any()))
-      .thenReturn(ExitResult.NoExit)
-    whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull(), any()))
-      .thenReturn(ExitResult.NoExit)
-
-    controller = createController()
-    controller.setSplitScreenController(splitScreenController)
-    controller.freeformTaskTransitionStarter = freeformTaskTransitionStarter
-    controller.desktopModeEnterExitTransitionListener = desktopModeEnterExitTransitionListener
-
-    shellInit.init()
-
-    val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java)
-    verify(recentsTransitionHandler).addTransitionStateListener(captor.capture())
-    recentsTransitionStateListener = captor.value
-
-    controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener
-
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
-    taskRepository = userRepositories.current
-  }
-
-  private fun createController(): DesktopTasksController {
-    return DesktopTasksController(
-        context,
-        shellInit,
-        shellCommandHandler,
-        shellController,
-        displayController,
-        shellTaskOrganizer,
-        syncQueue,
-        rootTaskDisplayAreaOrganizer,
-        dragAndDropController,
-        transitions,
-        keyguardManager,
-        mReturnToDragStartAnimator,
-        desktopMixedTransitionHandler,
-        enterDesktopTransitionHandler,
-        exitDesktopTransitionHandler,
-        dragAndDropTransitionHandler,
-        toggleResizeDesktopTaskTransitionHandler,
-        dragToDesktopTransitionHandler,
-        mMockDesktopImmersiveController,
-        userRepositories,
-        recentsTransitionHandler,
-        multiInstanceHelper,
-        shellExecutor,
-        Optional.of(desktopTasksLimiter),
-        recentTasksController,
-        mockInteractionJankMonitor,
-        mockHandler,
-        desktopModeEventLogger,
-        desktopModeUiEventLogger,
-        desktopTilingDecorViewModel,
-      )
-  }
-
-  @After
-  fun tearDown() {
-    mockitoSession.finishMocking()
-
-    runningTasks.clear()
-    testScope.cancel()
-  }
-
-  @Test
-  fun instantiate_addInitCallback() {
-    verify(shellInit).addInitCallback(any(), any<DesktopTasksController>())
-  }
-
-  @Test
-  fun doesAnyTaskRequireTaskbarRounding_onlyFreeFormTaskIsRunning_returnFalse() {
-    setUpFreeformTask()
-
-    assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isFalse()
-  }
-
-  @Test
-  fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() {
-    val task1 = setUpFreeformTask()
-
-    val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-    controller.toggleDesktopTaskSize(
-      task1,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.MAXIMIZE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
-        InputMethod.TOUCH
-      )
-    )
-
-    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.MAXIMIZE_BUTTON,
-      InputMethod.TOUCH,
-      task1,
-      STABLE_BOUNDS.width(),
-      STABLE_BOUNDS.height(),
-      displayController
-    )
-    assertThat(argumentCaptor.value).isTrue()
-  }
-
-  @Test
-  fun doesAnyTaskRequireTaskbarRounding_fullScreenTaskIsRunning_returnTrue() {
-    val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
-    setUpFreeformTask(bounds = stableBounds, active = true)
-    assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue()
-  }
-
-  @Test
-  fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfMaximizedTask_returnFalse() {
-    val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
-    val task1 = setUpFreeformTask(bounds = stableBounds, active = true)
-
-    val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-    controller.toggleDesktopTaskSize(
-      task1,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.RESTORE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
-        InputMethod.TOUCH
-      )
-    )
-
-    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      eq(ResizeTrigger.MAXIMIZE_BUTTON),
-      eq(InputMethod.TOUCH),
-      eq(task1),
-      anyOrNull(),
-      anyOrNull(),
-      eq(displayController),
-      anyOrNull()
-    )
-    assertThat(argumentCaptor.value).isFalse()
-  }
-
-  @Test
-  fun doesAnyTaskRequireTaskbarRounding_splitScreenTaskIsRunning_returnTrue() {
-    val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
-    setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom))
-
-    assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue()
-  }
-
-
-  @Test
-  fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() {
-    whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
-    clearInvocations(shellInit)
-
-    createController()
-
-    verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>())
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() {
-    val homeTask = setUpHomeTask()
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    markTaskHidden(task1)
-    markTaskHidden(task2)
-
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Expect order to be from bottom: home, task1, task2
-    wct.assertReorderAt(index = 0, homeTask)
-    wct.assertReorderAt(index = 1, task1)
-    wct.assertReorderAt(index = 2, task2)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    markTaskHidden(task1)
-    markTaskHidden(task2)
-
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Expect order to be from bottom: wallpaper intent, task1, task2
-    wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
-    wct.assertReorderAt(index = 1, task1)
-    wct.assertReorderAt(index = 2, task2)
-  }
-
-  @Test
-  fun isDesktopModeShowing_noTasks_returnsFalse() {
-    assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
-  }
-
-  @Test
-  fun isDesktopModeShowing_noTasksVisible_returnsFalse() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    markTaskHidden(task1)
-    markTaskHidden(task2)
-
-    assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
-  }
-
-  @Test
-  fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    markTaskVisible(task1)
-    markTaskHidden(task2)
-
-    assertThat(controller.isDesktopModeShowing(displayId = 0)).isTrue()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() {
-    val homeTask = setUpHomeTask(SECOND_DISPLAY)
-    val task1 = setUpFreeformTask(SECOND_DISPLAY)
-    val task2 = setUpFreeformTask(SECOND_DISPLAY)
-    markTaskHidden(task1)
-    markTaskHidden(task2)
-
-    controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Expect order to be from bottom: home, task1, task2 (no wallpaper intent)
-    wct.assertReorderAt(index = 0, homeTask)
-    wct.assertReorderAt(index = 1, task1)
-    wct.assertReorderAt(index = 2, task2)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() {
-    val homeTask = setUpHomeTask()
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    markTaskVisible(task1)
-    markTaskVisible(task2)
-
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Expect order to be from bottom: home, task1, task2
-    wct.assertReorderAt(index = 0, homeTask)
-    wct.assertReorderAt(index = 1, task1)
-    wct.assertReorderAt(index = 2, task2)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() {
-    val homeTask = setUpHomeTask(SECOND_DISPLAY)
-    val task1 = setUpFreeformTask(SECOND_DISPLAY)
-    val task2 = setUpFreeformTask(SECOND_DISPLAY)
-    markTaskHidden(task1)
-    markTaskHidden(task2)
-
-    controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Expect order to be from bottom: home, task1, task2
-    wct.assertReorderAt(index = 0, homeTask)
-    wct.assertReorderAt(index = 1, task1)
-    wct.assertReorderAt(index = 2, task2)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    markTaskVisible(task1)
-    markTaskVisible(task2)
-
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Expect order to be from bottom: wallpaper intent, task1, task2
-    wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
-    wct.assertReorderAt(index = 1, task1)
-    wct.assertReorderAt(index = 2, task2)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() {
-    val homeTask = setUpHomeTask()
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    markTaskHidden(task1)
-    markTaskVisible(task2)
-
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Expect order to be from bottom: home, task1, task2
-    wct.assertReorderAt(index = 0, homeTask)
-    wct.assertReorderAt(index = 1, task1)
-    wct.assertReorderAt(index = 2, task2)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    markTaskHidden(task1)
-    markTaskVisible(task2)
-
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Expect order to be from bottom: wallpaper intent, task1, task2
-    wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
-    wct.assertReorderAt(index = 1, task1)
-    wct.assertReorderAt(index = 2, task2)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() {
-    val homeTask = setUpHomeTask()
-
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(1)
-    wct.assertReorderAt(index = 0, homeTask)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() {
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
-    val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
-    val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
-    setUpHomeTask(SECOND_DISPLAY)
-    val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
-    markTaskHidden(taskDefaultDisplay)
-    markTaskHidden(taskSecondDisplay)
-
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(2)
-    // Expect order to be from bottom: home, task
-    wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
-    wct.assertReorderAt(index = 1, taskDefaultDisplay)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() {
-    val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
-    val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
-    setUpHomeTask(SECOND_DISPLAY)
-    val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
-    markTaskHidden(taskDefaultDisplay)
-    markTaskHidden(taskSecondDisplay)
-
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Move home to front
-    wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
-    // Add desktop wallpaper activity
-    wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent)
-    // Move freeform task to front
-    wct.assertReorderAt(index = 2, taskDefaultDisplay)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() {
-    val homeTask = setUpHomeTask()
-    val freeformTask = setUpFreeformTask()
-    val minimizedTask = setUpFreeformTask()
-
-    markTaskHidden(freeformTask)
-    markTaskHidden(minimizedTask)
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(2)
-    // Reorder home and freeform task to top, don't reorder the minimized task
-    wct.assertReorderAt(index = 0, homeTask, toTop = true)
-    wct.assertReorderAt(index = 1, freeformTask, toTop = true)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() {
-    val homeTask = setUpHomeTask()
-    val freeformTask = setUpFreeformTask()
-    val minimizedTask = setUpFreeformTask()
-
-    markTaskHidden(freeformTask)
-    markTaskHidden(minimizedTask)
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
-    controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // Move home to front
-    wct.assertReorderAt(index = 0, homeTask, toTop = true)
-    // Add desktop wallpaper activity
-    wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent)
-    // Reorder freeform task to top, don't reorder the minimized task
-    wct.assertReorderAt(index = 2, freeformTask, toTop = true)
-  }
-
-  @Test
-  fun visibleTaskCount_noTasks_returnsZero() {
-    assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
-  }
-
-  @Test
-  fun visibleTaskCount_twoTasks_bothVisible_returnsTwo() {
-    setUpHomeTask()
-    setUpFreeformTask().also(::markTaskVisible)
-    setUpFreeformTask().also(::markTaskVisible)
-    assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
-  }
-
-  @Test
-  fun visibleTaskCount_twoTasks_oneVisible_returnsOne() {
-    setUpHomeTask()
-    setUpFreeformTask().also(::markTaskVisible)
-    setUpFreeformTask().also(::markTaskHidden)
-    assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
-  }
-
-  @Test
-  fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() {
-    setUpHomeTask()
-    setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible)
-    setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible)
-    assertThat(controller.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
-  }
-
-  @Test
-  fun addMoveToDesktopChanges_gravityLeft_noBoundsApplied() {
-    setUpLandscapeDisplay()
-    val task = setUpFullscreenTask(gravity = Gravity.LEFT)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(finalBounds).isEqualTo(Rect())
-  }
-
-  @Test
-  fun addMoveToDesktopChanges_gravityRight_noBoundsApplied() {
-    setUpLandscapeDisplay()
-    val task = setUpFullscreenTask(gravity = Gravity.RIGHT)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(finalBounds).isEqualTo(Rect())
-  }
-
-  @Test
-  fun addMoveToDesktopChanges_gravityTop_noBoundsApplied() {
-    setUpLandscapeDisplay()
-    val task = setUpFullscreenTask(gravity = Gravity.TOP)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(finalBounds).isEqualTo(Rect())
-  }
-
-  @Test
-  fun addMoveToDesktopChanges_gravityBottom_noBoundsApplied() {
-    setUpLandscapeDisplay()
-    val task = setUpFullscreenTask(gravity = Gravity.BOTTOM)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(finalBounds).isEqualTo(Rect())
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun handleRequest_newFreeformTaskLaunch_cascadeApplied() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
-    val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS, active = false)
-
-    val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
-
-    assertNotNull(wct, "should handle request")
-    val finalBounds = findBoundsChange(wct, freeformTask)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.BottomRight)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun handleRequest_freeformTaskAlreadyExistsInDesktopMode_cascadeNotApplied() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
-    val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
-
-    val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
-
-    assertNull(wct, "should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun addMoveToDesktopChanges_positionBottomRight() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
-
-    val task = setUpFullscreenTask()
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.BottomRight)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun addMoveToDesktopChanges_positionTopLeft() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    addFreeformTaskAtPosition(DesktopTaskPosition.BottomRight, stableBounds)
-
-    val task = setUpFullscreenTask()
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.TopLeft)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun addMoveToDesktopChanges_positionBottomLeft() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    addFreeformTaskAtPosition(DesktopTaskPosition.TopLeft, stableBounds)
-
-    val task = setUpFullscreenTask()
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.BottomLeft)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun addMoveToDesktopChanges_positionTopRight() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    addFreeformTaskAtPosition(DesktopTaskPosition.BottomLeft, stableBounds)
-
-    val task = setUpFullscreenTask()
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.TopRight)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun addMoveToDesktopChanges_positionResetsToCenter() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    addFreeformTaskAtPosition(DesktopTaskPosition.TopRight, stableBounds)
-
-    val task = setUpFullscreenTask()
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.Center)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun addMoveToDesktopChanges_lastWindowSnapLeft_positionResetsToCenter() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    // Add freeform task with half display size snap bounds at left side.
-    setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom))
-
-    val task = setUpFullscreenTask()
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.Center)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun addMoveToDesktopChanges_lastWindowSnapRight_positionResetsToCenter() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    // Add freeform task with half display size snap bounds at right side.
-    setUpFreeformTask(bounds = Rect(
-      stableBounds.right - 500, stableBounds.top, stableBounds.right, stableBounds.bottom))
-
-    val task = setUpFullscreenTask()
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.Center)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun addMoveToDesktopChanges_lastWindowMaximised_positionResetsToCenter() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    // Add maximised freeform task.
-    setUpFreeformTask(bounds = Rect(stableBounds))
-
-    val task = setUpFullscreenTask()
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.Center)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
-  fun addMoveToDesktopChanges_defaultToCenterIfFree() {
-    setUpLandscapeDisplay()
-    val stableBounds = Rect()
-    displayLayout.getStableBoundsForDesktopMode(stableBounds)
-
-    val minTouchTarget = context.resources.getDimensionPixelSize(
-      R.dimen.freeform_required_visible_empty_space_in_header)
-    addFreeformTaskAtPosition(DesktopTaskPosition.Center, stableBounds,
-      Rect(0, 0, 1600, 1200), Point(0, minTouchTarget + 1))
-
-    val task = setUpFullscreenTask()
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    val finalBounds = findBoundsChange(wct, task)
-    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
-      .isEqualTo(DesktopTaskPosition.Center)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun addMoveToDesktopChanges_landscapeDevice_userFullscreenOverride_defaultPortraitBounds() {
-    setUpLandscapeDisplay()
-    val task = setUpFullscreenTask(enableUserFullscreenOverride = true)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun addMoveToDesktopChanges_landscapeDevice_systemFullscreenOverride_defaultPortraitBounds() {
-    setUpLandscapeDisplay()
-    val task = setUpFullscreenTask(enableSystemFullscreenOverride = true)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun addMoveToDesktopChanges_landscapeDevice_portraitResizableApp_aspectRatioOverridden() {
-    setUpLandscapeDisplay()
-    val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
-      shouldLetterbox = true, aspectRatioOverrideApplied = true)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun addMoveToDesktopChanges_portraitDevice_userFullscreenOverride_defaultPortraitBounds() {
-    setUpPortraitDisplay()
-    val task = setUpFullscreenTask(enableUserFullscreenOverride = true)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun addMoveToDesktopChanges_portraitDevice_systemFullscreenOverride_defaultPortraitBounds() {
-    setUpPortraitDisplay()
-    val task = setUpFullscreenTask(enableSystemFullscreenOverride = true)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun addMoveToDesktopChanges_portraitDevice_landscapeResizableApp_aspectRatioOverridden() {
-    setUpPortraitDisplay()
-    val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
-      deviceOrientation = ORIENTATION_PORTRAIT,
-      shouldLetterbox = true, aspectRatioOverrideApplied = true)
-    val wct = WindowContainerTransaction()
-    controller.addMoveToDesktopChanges(wct, task)
-
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
-    val task = setUpFullscreenTask()
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-    controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-  }
-
-  @Test
-  fun moveRunningTaskToDesktop_tdaFreeform_windowingModeSetToUndefined() {
-    val task = setUpFullscreenTask()
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
-    controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
-        .isEqualTo(WINDOWING_MODE_UNDEFINED)
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-  }
-
-  @Test
-  fun moveTaskToDesktop_nonExistentTask_doesNothing() {
-    controller.moveTaskToDesktop(999, transitionSource = UNKNOWN)
-    verifyEnterDesktopWCTNotExecuted()
-    verify(desktopModeEnterExitTransitionListener, times(0)).onEnterDesktopModeTransitionStarted(anyInt())
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun moveTaskToDesktop_desktopWallpaperDisabled_nonRunningTask_launchesInFreeform() {
-    val task = createTaskInfo(1)
-    whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
-    whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
-
-    controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN)
-
-    with(getLatestEnterDesktopWct()) {
-      assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
-    }
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() {
-    val task = createTaskInfo(1)
-    whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
-    whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
-
-    controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN)
-
-    with(getLatestEnterDesktopWct()) {
-      // Add desktop wallpaper activity
-      assertPendingIntentAt(index = 0, desktopWallpaperIntent)
-      // Launch task
-      assertLaunchTaskAt(index = 1, task.taskId, WINDOWING_MODE_FREEFORM)
-    }
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
-  fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop() {
-    val task =
-      setUpFullscreenTask().apply {
-        isActivityStackTransparent = true
-        isTopActivityNoDisplay = true
-        numActivities = 1
-      }
-
-    controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
-  fun moveRunningTaskToDesktop_topActivityTranslucentWithDisplay_doesNothing() {
-    val task =
-      setUpFullscreenTask().apply {
-        isActivityStackTransparent = true
-        isTopActivityNoDisplay = false
-        numActivities = 1
-      }
-
-    controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
-    verifyEnterDesktopWCTNotExecuted()
-    verify(desktopModeEnterExitTransitionListener, times(0)).onEnterDesktopModeTransitionStarted(
-      FREEFORM_ANIMATION_DURATION
-    )
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
-  fun moveRunningTaskToDesktop_systemUIActivityWithDisplay_doesNothing() {
-    // Set task as systemUI package
-    val systemUIPackageName = context.resources.getString(
-      com.android.internal.R.string.config_systemUi)
-    val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
-    val task =
-      setUpFullscreenTask().apply {
-        baseActivity = baseComponent
-        isTopActivityNoDisplay = false
-      }
-
-    controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
-    verifyEnterDesktopWCTNotExecuted()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
-  fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing() {
-    // Set task as systemUI package
-    val systemUIPackageName = context.resources.getString(
-      com.android.internal.R.string.config_systemUi)
-    val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
-    val task =
-      setUpFullscreenTask().apply {
-        baseActivity = baseComponent
-        isTopActivityNoDisplay = true
-      }
-
-    controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
-
-    val wct = getLatestEnterDesktopWct()
-    assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() {
-    val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
-    whenever(
-      transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())
-    ).thenReturn(Binder())
-
-    val task = createTaskInfo(1)
-    whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
-    whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
-    controller.moveTaskToDesktop(
-      taskId = task.taskId,
-      transitionSource = UNKNOWN,
-      remoteTransition = RemoteTransition(spy(TestRemoteTransition())))
-
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-    assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
-  }
-
-
-  @Test
-  fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() {
-    val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
-    whenever(
-      transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())
-    ).thenReturn(Binder())
-
-    controller.moveRunningTaskToDesktop(
-      task = setUpFullscreenTask(),
-      transitionSource = UNKNOWN,
-      remoteTransition = RemoteTransition(spy(TestRemoteTransition())))
-
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-    assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() {
-    val homeTask = setUpHomeTask()
-    val freeformTask = setUpFreeformTask()
-    val fullscreenTask = setUpFullscreenTask()
-    markTaskHidden(freeformTask)
-
-    controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN)
-
-    with(getLatestEnterDesktopWct()) {
-      // Operations should include home task, freeform task
-      assertThat(hierarchyOps).hasSize(3)
-      assertReorderSequence(homeTask, freeformTask, fullscreenTask)
-      assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
-          .isEqualTo(WINDOWING_MODE_FREEFORM)
-    }
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() {
-    val freeformTask = setUpFreeformTask()
-    val fullscreenTask = setUpFullscreenTask()
-    markTaskHidden(freeformTask)
-
-    controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN)
-
-    with(getLatestEnterDesktopWct()) {
-      // Operations should include wallpaper intent, freeform task, fullscreen task
-      assertThat(hierarchyOps).hasSize(3)
-      assertPendingIntentAt(index = 0, desktopWallpaperIntent)
-      assertReorderAt(index = 1, freeformTask)
-      assertReorderAt(index = 2, fullscreenTask)
-      assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
-          .isEqualTo(WINDOWING_MODE_FREEFORM)
-    }
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-  }
-
-  @Test
-  fun moveRunningTaskToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() {
-    setUpHomeTask(displayId = DEFAULT_DISPLAY)
-    val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
-    markTaskHidden(freeformTaskDefault)
-
-    val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY)
-    val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY)
-    markTaskHidden(freeformTaskSecond)
-
-    controller.moveRunningTaskToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN)
-
-    with(getLatestEnterDesktopWct()) {
-      // Check that hierarchy operations do not include tasks from second display
-      assertThat(hierarchyOps.map { it.container }).doesNotContain(homeTaskSecond.token.asBinder())
-      assertThat(hierarchyOps.map { it.container })
-          .doesNotContain(freeformTaskSecond.token.asBinder())
-    }
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-  }
-
-  @Test
-  fun moveRunningTaskToDesktop_splitTaskExitsSplit() {
-    val task = setUpSplitScreenTask()
-    controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-    verify(splitScreenController)
-        .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE))
-  }
-
-  @Test
-  fun moveRunningTaskToDesktop_fullscreenTaskDoesNotExitSplit() {
-    val task = setUpFullscreenTask()
-    controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-    verify(splitScreenController, never())
-        .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE))
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun moveRunningTaskToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() {
-    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
-    val newTask = setUpFullscreenTask()
-    val homeTask = setUpHomeTask()
-
-    controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN)
-
-    val wct = getLatestEnterDesktopWct()
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-    assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 1) // visible tasks + home
-    wct.assertReorderAt(0, homeTask)
-    wct.assertReorderSequenceInRange(
-        range = 1..<(MAX_TASK_LIMIT + 1),
-        *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
-        newTask)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() {
-    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
-    val newTask = setUpFullscreenTask()
-    val homeTask = setUpHomeTask()
-
-    controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN)
-
-    val wct = getLatestEnterDesktopWct()
-    verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
-    assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 2) // tasks + home + wallpaper
-    // Move home to front
-    wct.assertReorderAt(0, homeTask)
-    // Add desktop wallpaper activity
-    wct.assertPendingIntentAt(1, desktopWallpaperIntent)
-    // Bring freeform tasks to front
-    wct.assertReorderSequenceInRange(
-        range = 2..<(MAX_TASK_LIMIT + 2),
-        *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
-        newTask)
-  }
-
-  @Test
-  fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
-    val task = setUpFreeformTask()
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-    controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
-    val wct = getLatestExitDesktopWct()
-    verify(desktopModeEnterExitTransitionListener, times(1)).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
-    assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
-        .isEqualTo(WINDOWING_MODE_UNDEFINED)
-  }
-
-  @Test
-  fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
-      .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-
-    controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
-
-    val wct = getLatestExitDesktopWct()
-    val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
-    verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
-    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
-    // Removes wallpaper activity when leaving desktop
-    wct.assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() {
-    val task = setUpFreeformTask()
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
-    controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
-    val wct = getLatestExitDesktopWct()
-    assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
-        .isEqualTo(WINDOWING_MODE_FULLSCREEN)
-    verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
-  }
-
-  @Test
-  fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
-      .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
-
-    controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
-
-    val wct = getLatestExitDesktopWct()
-    val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
-    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN)
-    verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
-    // Removes wallpaper activity when leaving desktop
-    wct.assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
-    val task1 = setUpFreeformTask()
-    // Setup task2
-    setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
-      .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-
-    controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
-
-    val wct = getLatestExitDesktopWct()
-    val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
-    assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
-    verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
-    // Does not remove wallpaper activity, as desktop still has a visible desktop task
-    assertThat(wct.hierarchyOps).isEmpty()
-  }
-
-  @Test
-  fun moveToFullscreen_nonExistentTask_doesNothing() {
-    controller.moveToFullscreen(999, transitionSource = UNKNOWN)
-    verifyExitDesktopWCTNotExecuted()
-  }
-
-  @Test
-  fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() {
-    val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY)
-    controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN)
-
-    with(getLatestExitDesktopWct()) {
-      assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder())
-      assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder())
-    }
-    verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
-  }
-
-  @Test
-  fun moveTaskToFront_postsWctWithReorderOp() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-
-    controller.moveTaskToFront(task1, remoteTransition = null)
-
-    val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
-    assertThat(wct.hierarchyOps).hasSize(1)
-    wct.assertReorderAt(index = 0, task1)
-  }
-
-  @Test
-  fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() {
-    setUpHomeTask()
-    val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() }
-    whenever(desktopMixedTransitionHandler.startLaunchTransition(
-      eq(TRANSIT_TO_FRONT),
-      any(),
-      eq(freeformTasks[0].taskId),
-      anyOrNull(),
-      anyOrNull(),
-    )).thenReturn(Binder())
-
-    controller.moveTaskToFront(freeformTasks[0], remoteTransition = null)
-
-    val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
-    assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize
-    wct.assertReorderAt(0, freeformTasks[0], toTop = true)
-    wct.assertReorderAt(1, freeformTasks[1], toTop = false)
-  }
-
-  @Test
-  fun moveTaskToFront_remoteTransition_usesOneshotHandler() {
-    setUpHomeTask()
-    val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() }
-    val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
-    whenever(
-      transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())
-    ).thenReturn(Binder())
-
-    controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition()))
-
-    assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
-  }
-
-  @Test
-  fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() {
-    setUpHomeTask()
-    val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() }
-    val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
-    whenever(
-      transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())
-    ).thenReturn(Binder())
-
-    controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition()))
-
-    assertThat(transitionHandlerArgCaptor.value)
-      .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java)
-  }
-
-  @Test
-  fun moveTaskToFront_backgroundTask_launchesTask() {
-    val task = createTaskInfo(1)
-    whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
-
-    controller.moveTaskToFront(task.taskId, remoteTransition = null)
-
-    val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
-    assertThat(wct.hierarchyOps).hasSize(1)
-    wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
-  }
-
-  @Test
-  fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() {
-    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
-    val task = createTaskInfo(1001)
-    whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
-    whenever(desktopMixedTransitionHandler
-      .startLaunchTransition(eq(TRANSIT_OPEN), any(), eq(task.taskId), anyOrNull(), anyOrNull()))
-      .thenReturn(Binder())
-
-    controller.moveTaskToFront(task.taskId, remoteTransition = null)
-
-    val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
-    assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize
-    wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
-    wct.assertReorderAt(1, freeformTasks[0], toTop = false)
-  }
-
-  @Test
-  fun moveToNextDisplay_noOtherDisplays() {
-    whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
-    val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    controller.moveToNextDisplay(task.taskId)
-    verifyWCTNotExecuted()
-  }
-
-  @Test
-  fun moveToNextDisplay_moveFromFirstToSecondDisplay() {
-    // Set up two display ids
-    whenever(rootTaskDisplayAreaOrganizer.displayIds)
-        .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
-    // Create a mock for the target display area: second display
-    val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
-    whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
-        .thenReturn(secondDisplayArea)
-
-    val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    controller.moveToNextDisplay(task.taskId)
-    with(getLatestWct(type = TRANSIT_CHANGE)) {
-      assertThat(hierarchyOps).hasSize(1)
-      assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
-      assertThat(hierarchyOps[0].isReparent).isTrue()
-      assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder())
-      assertThat(hierarchyOps[0].toTop).isTrue()
-    }
-  }
-
-  @Test
-  fun moveToNextDisplay_moveFromSecondToFirstDisplay() {
-    // Set up two display ids
-    whenever(rootTaskDisplayAreaOrganizer.displayIds)
-        .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
-    // Create a mock for the target display area: default display
-    val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
-    whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
-        .thenReturn(defaultDisplayArea)
-
-    val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
-    controller.moveToNextDisplay(task.taskId)
-
-    with(getLatestWct(type = TRANSIT_CHANGE)) {
-      assertThat(hierarchyOps).hasSize(1)
-      assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
-      assertThat(hierarchyOps[0].isReparent).isTrue()
-      assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder())
-      assertThat(hierarchyOps[0].toTop).isTrue()
-    }
-  }
-
-  @Test
-  @EnableFlags(FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY)
-  fun moveToNextDisplay_removeWallpaper() {
-    // Set up two display ids
-    whenever(rootTaskDisplayAreaOrganizer.displayIds)
-      .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
-    // Create a mock for the target display area: second display
-    val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
-    whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
-      .thenReturn(secondDisplayArea)
-    // Add a task and a wallpaper
-    val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-
-    controller.moveToNextDisplay(task.taskId)
-
-    with(getLatestWct(type = TRANSIT_CHANGE)) {
-      val wallpaperChange = hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() }
-      assertThat(wallpaperChange).isNotNull()
-      assertThat(wallpaperChange!!.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
-    }
-  }
-
-  @Test
-  fun getTaskWindowingMode() {
-    val fullscreenTask = setUpFullscreenTask()
-    val freeformTask = setUpFreeformTask()
-
-    assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId))
-        .isEqualTo(WINDOWING_MODE_FULLSCREEN)
-    assertThat(controller.getTaskWindowingMode(freeformTask.taskId))
-        .isEqualTo(WINDOWING_MODE_FREEFORM)
-    assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED)
-  }
-
-  @Test
-  fun onDesktopWindowClose_noActiveTasks() {
-    val task = setUpFreeformTask(active = false)
-    val wct = WindowContainerTransaction()
-    controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
-    // Doesn't modify transaction
-    assertThat(wct.hierarchyOps).isEmpty()
-  }
-
-  @Test
-  fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() {
-    val task = setUpFreeformTask()
-    val wct = WindowContainerTransaction()
-    controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
-    // Doesn't modify transaction
-    assertThat(wct.hierarchyOps).isEmpty()
-  }
-
-  @Test
-  fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-
-    val wct = WindowContainerTransaction()
-    controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
-    // Adds remove wallpaper operation
-    wct.assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  fun onDesktopWindowClose_singleActiveTask_isClosing() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId)
-
-    val wct = WindowContainerTransaction()
-    controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
-    // Doesn't modify transaction
-    assertThat(wct.hierarchyOps).isEmpty()
-  }
-
-  @Test
-  fun onDesktopWindowClose_singleActiveTask_isMinimized() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
-
-    val wct = WindowContainerTransaction()
-    controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
-    // Doesn't modify transaction
-    assertThat(wct.hierarchyOps).isEmpty()
-  }
-
-  @Test
-  fun onDesktopWindowClose_multipleActiveTasks() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-
-    val wct = WindowContainerTransaction()
-    controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1)
-    // Doesn't modify transaction
-    assertThat(wct.hierarchyOps).isEmpty()
-  }
-
-  @Test
-  fun onDesktopWindowClose_multipleActiveTasks_isOnlyNonClosingTask() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId)
-
-    val wct = WindowContainerTransaction()
-    controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1)
-    // Adds remove wallpaper operation
-    wct.assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  fun onDesktopWindowClose_multipleActiveTasks_hasMinimized() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
-
-    val wct = WindowContainerTransaction()
-    controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1)
-    // Adds remove wallpaper operation
-    wct.assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() {
-    val task = setUpFreeformTask(active = false)
-    val transition = Binder()
-    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
-      .thenReturn(transition)
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-
-    controller.minimizeTask(task)
-
-    val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-    captor.value.hierarchyOps.none { hop ->
-      hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
-    }
-  }
-
-  @Test
-  fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() {
-    val task = setUpPipTask(autoEnterEnabled = true)
-    val handler = mock(TransitionHandler::class.java)
-    whenever(freeformTaskTransitionStarter.startPipTransition(any()))
-      .thenReturn(Binder())
-    whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
-      .thenReturn(android.util.Pair(handler, WindowContainerTransaction())
-    )
-
-    controller.minimizeTask(task)
-
-    verify(freeformTaskTransitionStarter).startPipTransition(any())
-    verify(freeformTaskTransitionStarter, never()).startMinimizedModeTransition(any())
-  }
-
-  @Test
-  fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() {
-    val task = setUpPipTask(autoEnterEnabled = false)
-    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
-      .thenReturn(Binder())
-
-    controller.minimizeTask(task)
-
-    verify(freeformTaskTransitionStarter).startMinimizedModeTransition(any())
-    verify(freeformTaskTransitionStarter, never()).startPipTransition(any())
-  }
-
-  @Test
-  fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
-    val task = setUpFreeformTask(active = true)
-    val transition = Binder()
-    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
-      .thenReturn(transition)
-
-    controller.minimizeTask(task)
-
-    val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-    captor.value.hierarchyOps.none { hop ->
-      hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK
-    }
-  }
-
-  @Test
-  fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() {
-    val task = setUpFreeformTask()
-    val transition = Binder()
-    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
-      .thenReturn(transition)
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-
-    // The only active task is being minimized.
-    controller.minimizeTask(task)
-
-    val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-    // Adds remove wallpaper operation
-    captor.value.assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() {
-    val task = setUpFreeformTask()
-    val transition = Binder()
-    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
-      .thenReturn(transition)
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
-
-    // The only active task is already minimized.
-    controller.minimizeTask(task)
-
-    val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-    captor.value.hierarchyOps.none { hop ->
-      hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
-    }
-  }
-
-  @Test
-  fun onDesktopWindowMinimize_multipleActiveTasks_doesntRemoveWallpaper() {
-    val task1 = setUpFreeformTask(active = true)
-    setUpFreeformTask(active = true)
-    val transition = Binder()
-    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
-      .thenReturn(transition)
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-
-    controller.minimizeTask(task1)
-
-    val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-    captor.value.hierarchyOps.none { hop ->
-      hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
-    }
-  }
-
-  @Test
-  fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() {
-    val task1 = setUpFreeformTask(active = true)
-    val task2 = setUpFreeformTask(active = true)
-    val transition = Binder()
-    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
-      .thenReturn(transition)
-    val wallpaperToken = MockToken().token()
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
-
-    // task1 is the only visible task as task2 is minimized.
-    controller.minimizeTask(task1)
-    // Adds remove wallpaper operation
-    val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
-    // Adds remove wallpaper operation
-    captor.value.assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  fun onDesktopWindowMinimize_triesToExitImmersive() {
-    val task = setUpFreeformTask()
-    val transition = Binder()
-    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
-      .thenReturn(transition)
-
-    controller.minimizeTask(task)
-
-    verify(mMockDesktopImmersiveController).exitImmersiveIfApplicable(any(), eq(task), any())
-  }
-
-  @Test
-  fun onDesktopWindowMinimize_invokesImmersiveTransitionStartCallback() {
-    val task = setUpFreeformTask()
-    val transition = Binder()
-    val runOnTransit = RunOnStartTransitionCallback()
-    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
-      .thenReturn(transition)
-    whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any()))
-      .thenReturn(
-        ExitResult.Exit(
-        exitingTask = task.taskId,
-        runOnTransitionStart = runOnTransit,
-      ))
-
-    controller.minimizeTask(task)
-
-    assertThat(runOnTransit.invocations).isEqualTo(1)
-    assertThat(runOnTransit.lastInvoked).isEqualTo(transition)
-  }
-
-  @Test
-  fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
-    val homeTask = setUpHomeTask()
-    val freeformTask = setUpFreeformTask()
-    markTaskVisible(freeformTask)
-    val fullscreenTask = createFullscreenTask()
-
-    val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
-    assertNotNull(wct, "should handle request")
-    assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode)
-        .isEqualTo(WINDOWING_MODE_FREEFORM)
-
-    assertThat(wct.hierarchyOps).hasSize(1)
-  }
-
-  @Test
-  fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() {
-    val homeTask = setUpHomeTask()
-    val freeformTask = setUpFreeformTask()
-    markTaskVisible(freeformTask)
-    val fullscreenTask = createFullscreenTask()
-    fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
-
-    val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
-    assertNotNull(wct, "should handle request")
-    assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode)
-      .isEqualTo(WINDOWING_MODE_FREEFORM)
-
-    // There are 5 hops that are happening in this case:
-    // 1. Moving the fullscreen task to top as we add moveToDesktop() changes
-    // 2. Bringing home task to front
-    // 3. Pending intent for the wallpaper
-    // 4. Bringing the existing freeform task to top
-    // 5. Bringing the fullscreen task back at the top
-    assertThat(wct.hierarchyOps).hasSize(5)
-    wct.assertReorderAt(1, homeTask, toTop = true)
-    wct.assertReorderAt(4, fullscreenTask, toTop = true)
-  }
-
-  @Test
-  fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() {
-    val freeformTask = setUpFreeformTask()
-    markTaskVisible(freeformTask)
-    val fullscreenTask = createFullscreenTask()
-
-    val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
-    // Make sure we only reorder the new task to top (we don't reorder the old task to bottom)
-    assertThat(wct?.hierarchyOps?.size).isEqualTo(1)
-    wct!!.assertReorderAt(0, fullscreenTask, toTop = true)
-  }
-
-  @Test
-  fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() {
-    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
-    freeformTasks.forEach { markTaskVisible(it) }
-    val fullscreenTask = createFullscreenTask()
-
-    val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
-    // Make sure we reorder the new task to top, and the back task to the bottom
-    assertThat(wct!!.hierarchyOps.size).isEqualTo(2)
-    wct.assertReorderAt(0, fullscreenTask, toTop = true)
-    wct.assertReorderAt(1, freeformTasks[0], toTop = false)
-  }
-
-  @Test
-  fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() {
-    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
-    freeformTasks.forEach { markTaskVisible(it) }
-    val fullscreenTask = createFullscreenTask()
-    fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
-
-    val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
-    // Make sure we reorder the new task to top, and the back task to the bottom
-    assertThat(wct!!.hierarchyOps.size).isEqualTo(9)
-    wct.assertReorderAt(0, fullscreenTask, toTop = true)
-    wct.assertReorderAt(8, freeformTasks[0], toTop = false)
-  }
-
-  @Test
-  fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() {
-    val minimizedTask = setUpFreeformTask()
-    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId)
-    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
-    freeformTasks.forEach { markTaskVisible(it) }
-    val homeTask = setUpHomeTask()
-    val fullscreenTask = createFullscreenTask()
-    fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
-
-    val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
-    assertThat(wct!!.hierarchyOps.size).isEqualTo(10)
-    wct.assertReorderAt(0, fullscreenTask, toTop = true)
-    // Make sure we reorder the home task to the top, desktop tasks to top of them and minimized
-    // task is under the home task.
-    wct.assertReorderAt(1, homeTask, toTop = true)
-    wct.assertReorderAt(9, freeformTasks[0], toTop = false)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() {
-    whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
-
-    val fullscreenTask = createFullscreenTask()
-    val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
-    assertNotNull(wct, "should handle request")
-    assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode)
-        .isEqualTo(WINDOWING_MODE_UNDEFINED)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    // There are 3 hops that are happening in this case:
-    // 1. Moving the fullscreen task to top as we add moveToDesktop() changes
-    // 2. Pending intent for the wallpaper
-    // 3. Bringing the fullscreen task back at the top
-    wct.assertPendingIntentAt(1, desktopWallpaperIntent)
-    wct.assertReorderAt(2, fullscreenTask, toTop = true)
-  }
-
-  @Test
-  fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() {
-    whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-
-    val fullscreenTask = createFullscreenTask()
-    val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
-    assertThat(wct).isNull()
-  }
-
-  @Test
-  fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
-    val freeformTask = setUpFreeformTask()
-    markTaskHidden(freeformTask)
-    val fullscreenTask = createFullscreenTask()
-    assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
-  }
-
-  @Test
-  fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
-    val fullscreenTask = createFullscreenTask()
-    assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
-  }
-
-  @Test
-  fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() {
-    val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
-    createFreeformTask(displayId = SECOND_DISPLAY)
-
-    val result = controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay))
-    assertThat(result).isNull()
-  }
-
-  @Test
-  fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() {
-    val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
-    freeformTasks.forEach { markTaskVisible(it) }
-    val newFreeformTask = createFreeformTask()
-
-    val wct = controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN))
-
-    assertThat(wct?.hierarchyOps?.size).isEqualTo(1)
-    wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom
-  }
-
-  @Test
-  fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() {
-    val freeformTask = setUpFreeformTask()
-    markTaskHidden(freeformTask)
-
-    val wct =
-      controller.handleRequest(Binder(), createTransition(freeformTask))
-
-    // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA.
-    assertNotNull(wct, "should handle request")
-    assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode)
-      .isEqualTo(WINDOWING_MODE_UNDEFINED)
-  }
-
-  @Test
-  fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() {
-    whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
-
-    val freeformTask = setUpFreeformTask()
-    markTaskHidden(freeformTask)
-    val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
-
-    assertNotNull(wct, "should handle request")
-    assertFalse(wct.anyWindowingModeChange(freeformTask.token))
-  }
-
-  @Test
-  fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() {
-    whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-
-    val freeformTask = setUpFreeformTask()
-    markTaskHidden(freeformTask)
-    val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
-
-    assertNotNull(wct, "should handle request")
-    assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode)
-      .isEqualTo(WINDOWING_MODE_UNDEFINED)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
-    val freeformTask1 = setUpFreeformTask()
-    val freeformTask2 = createFreeformTask()
-
-    markTaskHidden(freeformTask1)
-    val result =
-        controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT))
-
-    assertNotNull(result, "Should handle request")
-    assertThat(result.hierarchyOps?.size).isEqualTo(2)
-    result.assertReorderAt(1, freeformTask2, toTop = true)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
-    val freeformTask1 = setUpFreeformTask()
-    val freeformTask2 = createFreeformTask()
-
-    markTaskHidden(freeformTask1)
-    val result =
-      controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT))
-
-    assertNotNull(result, "Should handle request")
-    assertThat(result.hierarchyOps?.size).isEqualTo(3)
-    // Add desktop wallpaper activity
-    result.assertPendingIntentAt(0, desktopWallpaperIntent)
-    // Bring active desktop tasks to front
-    result.assertReorderAt(1, freeformTask1, toTop = true)
-    // Bring new task to front
-    result.assertReorderAt(2, freeformTask2, toTop = true)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() {
-    val task = createFreeformTask()
-    val result = controller.handleRequest(Binder(), createTransition(task))
-
-    assertNotNull(result, "Should handle request")
-    assertThat(result.hierarchyOps?.size).isEqualTo(1)
-    result.assertReorderAt(0, task, toTop = true)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
-    val task = createFreeformTask()
-    val result = controller.handleRequest(Binder(), createTransition(task))
-
-    assertNotNull(result, "Should handle request")
-    assertThat(result.hierarchyOps?.size).isEqualTo(2)
-    // Add desktop wallpaper activity
-    result.assertPendingIntentAt(0, desktopWallpaperIntent)
-    // Bring new task to front
-    result.assertReorderAt(1, task, toTop = true)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() {
-    val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
-    // Second display task
-    createFreeformTask(displayId = SECOND_DISPLAY)
-
-    val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
-
-    assertNotNull(result, "Should handle request")
-    assertThat(result.hierarchyOps?.size).isEqualTo(1)
-    result.assertReorderAt(0, taskDefaultDisplay, toTop = true)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
-    val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
-    // Second display task
-    createFreeformTask(displayId = SECOND_DISPLAY)
-
-    val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
-
-    assertNotNull(result, "Should handle request")
-    assertThat(result.hierarchyOps?.size).isEqualTo(2)
-    // Add desktop wallpaper activity
-    result.assertPendingIntentAt(0, desktopWallpaperIntent)
-    // Bring new task to front
-    result.assertReorderAt(1, taskDefaultDisplay, toTop = true)
-  }
-
-  @Test
-  fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() {
-    whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false)
-
-    val freeformTask1 = setUpFreeformTask()
-    markTaskVisible(freeformTask1)
-
-    val freeformTask2 = createFreeformTask()
-    val result =
-        controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2))
-    assertFalse(result.anyDensityConfigChange(freeformTask2.token))
-  }
-
-  @Test
-  fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() {
-    whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true)
-
-    val freeformTask1 = setUpFreeformTask()
-    markTaskVisible(freeformTask1)
-
-    val freeformTask2 = createFreeformTask()
-    val result =
-        controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2))
-    assertTrue(result.anyDensityConfigChange(freeformTask2.token))
-  }
-
-  @Test
-  fun handleRequest_freeformTask_keyguardLocked_returnNull() {
-    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() {
-    val task =
-        TestRunningTaskInfoBuilder()
-            .setActivityType(ACTIVITY_TYPE_STANDARD)
-            .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
-            .build()
-    val transition = createTransition(task = task, type = TRANSIT_CLOSE)
-    val result = controller.handleRequest(Binder(), transition)
-    assertThat(result).isNull()
-  }
-
-  @Test
-  fun handleRequest_noTriggerTask_returnNull() {
-    assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull()
-  }
-
-  @Test
-  fun handleRequest_triggerTaskNotStandard_returnNull() {
-    val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
-    assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
-  }
-
-  @Test
-  fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() {
-    val task =
-        TestRunningTaskInfoBuilder()
-            .setActivityType(ACTIVITY_TYPE_STANDARD)
-            .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
-            .build()
-    assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
-  }
-
-  @Test
-  fun handleRequest_recentsAnimationRunning_returnNull() {
-    // Set up a visible freeform task so a fullscreen task should be converted to freeform
-    val freeformTask = setUpFreeformTask()
-    markTaskVisible(freeformTask)
-
-    // Mark recents animation running
-    recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING)
-
-    // Open a fullscreen task, check that it does not result in a WCT with changes to it
-    val fullscreenTask = createFullscreenTask()
-    assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
-  }
-
-  @Test
-  fun handleRequest_recentsAnimationRunning_relaunchActiveTask_taskBecomesUndefined() {
-    // Set up a visible freeform task
-    val freeformTask = setUpFreeformTask()
-    markTaskVisible(freeformTask)
-
-    // Mark recents animation running
-    recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING)
-
-    // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA.
-    val result = controller.handleRequest(Binder(), createTransition(freeformTask))
-    assertThat(result?.changes?.get(freeformTask.token.asBinder())?.windowingMode)
-      .isEqualTo(WINDOWING_MODE_UNDEFINED)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
-  fun handleRequest_topActivityTransparentWithoutDisplay_returnSwitchToFreeformWCT() {
-    val freeformTask = setUpFreeformTask()
-    markTaskVisible(freeformTask)
-
-    val task =
-      setUpFullscreenTask().apply {
-        isActivityStackTransparent = true
-        isTopActivityNoDisplay = true
-        numActivities = 1
-      }
-
-    val result = controller.handleRequest(Binder(), createTransition(task))
-    assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
-            .isEqualTo(WINDOWING_MODE_FREEFORM)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
-  fun handleRequest_topActivityTransparentWithDisplay_returnSwitchToFullscreenWCT() {
-    val freeformTask = setUpFreeformTask()
-    markTaskVisible(freeformTask)
-
-    val task =
-      setUpFreeformTask().apply {
-        isActivityStackTransparent = true
-        isTopActivityNoDisplay = false
-        numActivities = 1
-      }
-
-    val result = controller.handleRequest(Binder(), createTransition(task))
-    assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
-            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
-  fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT() {
-    val freeformTask = setUpFreeformTask()
-    markTaskVisible(freeformTask)
-
-    // Set task as systemUI package
-    val systemUIPackageName = context.resources.getString(
-      com.android.internal.R.string.config_systemUi)
-    val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
-    val task =
-      setUpFreeformTask().apply {
-        baseActivity = baseComponent
-        isTopActivityNoDisplay = false
-      }
-
-    val result = controller.handleRequest(Binder(), createTransition(task))
-    assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
-            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
-  fun handleRequest_systemUIActivityWithoutDisplay_returnSwitchToFreeformWCT() {
-    val freeformTask = setUpFreeformTask()
-    markTaskVisible(freeformTask)
-
-    // Set task as systemUI package
-    val systemUIPackageName = context.resources.getString(
-      com.android.internal.R.string.config_systemUi)
-    val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
-    val task =
-      setUpFullscreenTask().apply {
-        baseActivity = baseComponent
-        isTopActivityNoDisplay = true
-      }
-
-    val result = controller.handleRequest(Binder(), createTransition(task))
-    assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
-      .isEqualTo(WINDOWING_MODE_FREEFORM)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
-  fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
-    val task = setUpFreeformTask()
-
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
-  fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() {
-    val task = setUpFreeformTask()
-
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-  )
-  fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_notInDesktop_doesNotHandle() {
-    val task = setUpFreeformTask()
-    markTaskHidden(task)
-
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_singleTaskNoToken_doesNotHandle() {
-    val task = setUpFreeformTask()
-
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
-  fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
-    val task = setUpFreeformTask()
-
-    taskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
-  fun handleRequest_backTransition_multipleTasks_noWallpaper_doesNotHandle() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-
-    taskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_multipleTasks_doesNotHandle() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-
-    taskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
-  )
-  fun handleRequest_backTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  @EnableFlags(
-    Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-  )
-  fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
-  fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_removesWallpaper() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    // Task is being minimized so mark it as not visible.
-    taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false)
-    val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
-  fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
-    val task = setUpFreeformTask()
-
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_singleTaskNoToken_doesNotHandle() {
-    val task = setUpFreeformTask()
-
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
-    val task = setUpFreeformTask()
-
-    taskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() {
-    val task = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-
-    taskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_multipleTasksFlagEnabled_doesNotHandle() {
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
-
-    taskRepository.wallpaperActivityToken = MockToken().token()
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
-    // Should create remove wallpaper transaction
-    assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
-  fun handleRequest_closeTransition_minimizadTask_withWallpaper_removesWallpaper() {
-    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
-    val wallpaperToken = MockToken().token()
-
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
-    // Task is being minimized so mark it as not visible.
-    taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false)
-    val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK))
-
-    assertNull(result, "Should not handle request")
-  }
-
-  @Test
-  fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() {
-    val task1 = setUpFullscreenTask()
-    val task2 = setUpFullscreenTask()
-    val task3 = setUpFullscreenTask()
-
-    task1.isFocused = true
-    task2.isFocused = false
-    task3.isFocused = false
-
-    controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
-
-    val wct = getLatestEnterDesktopWct()
-    assertThat(wct.changes[task1.token.asBinder()]?.windowingMode)
-        .isEqualTo(WINDOWING_MODE_FREEFORM)
-  }
-
-  @Test
-  fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() {
-    val task1 = setUpSplitScreenTask()
-    val task2 = setUpFullscreenTask()
-    val task3 = setUpFullscreenTask()
-    val task4 = setUpSplitScreenTask()
-
-    task1.isFocused = true
-    task2.isFocused = false
-    task3.isFocused = false
-    task4.isFocused = true
-
-    task4.parentTaskId = task1.taskId
-
-    controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
-
-    val wct = getLatestEnterDesktopWct()
-    assertThat(wct.changes[task4.token.asBinder()]?.windowingMode)
-        .isEqualTo(WINDOWING_MODE_FREEFORM)
-    verify(splitScreenController)
-        .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE))
-  }
-
-  @Test
-  fun moveFocusedTaskToFullscreen() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val task3 = setUpFreeformTask()
-
-    task1.isFocused = false
-    task2.isFocused = true
-    task3.isFocused = false
-
-    controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
-
-    val wct = getLatestExitDesktopWct()
-    assertThat(wct.changes[task2.token.asBinder()]?.windowingMode)
-        .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
-  }
-
-  @Test
-  fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val task3 = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    task1.isFocused = false
-    task2.isFocused = true
-    task3.isFocused = false
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
-    taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false)
-
-    controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
-
-    val wct = getLatestExitDesktopWct()
-    val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
-    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
-    wct.assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val task3 = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    task1.isFocused = false
-    task2.isFocused = true
-    task3.isFocused = false
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
-
-    val wct = getLatestExitDesktopWct()
-    val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
-    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
-    // Does not remove wallpaper activity, as desktop still has visible desktop tasks
-    assertThat(wct.hierarchyOps).isEmpty()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun removeDesktop_multipleTasks_removesAll() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val task3 = setUpFreeformTask()
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
-
-    controller.removeDesktop(displayId = DEFAULT_DISPLAY)
-
-    val wct = getLatestWct(TRANSIT_CLOSE)
-    assertThat(wct.hierarchyOps).hasSize(3)
-    wct.assertRemoveAt(index = 0, task1.token)
-    wct.assertRemoveAt(index = 1, task2.token)
-    wct.assertRemoveAt(index = 2, task3.token)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun removeDesktop_multipleTasksWithBackgroundTask_removesAll() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val task3 = setUpFreeformTask()
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
-    whenever(shellTaskOrganizer.getRunningTaskInfo(task3.taskId)).thenReturn(null)
-
-    controller.removeDesktop(displayId = DEFAULT_DISPLAY)
-
-    val wct = getLatestWct(TRANSIT_CLOSE)
-    assertThat(wct.hierarchyOps).hasSize(2)
-    wct.assertRemoveAt(index = 0, task1.token)
-    wct.assertRemoveAt(index = 1, task2.token)
-    verify(recentTasksController).removeBackgroundTask(task3.taskId)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task = setUpFullscreenTask()
-    setUpLandscapeDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
-    setUpLandscapeDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task =
-        setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
-    setUpLandscapeDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task =
-        setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
-    setUpLandscapeDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
-            shouldLetterbox = true)
-    setUpLandscapeDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task =
-        setUpFullscreenTask(
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task =
-        setUpFullscreenTask(
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
-            shouldLetterbox = true)
-    setUpPortraitDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
-    val spyController = spy(controller)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-        .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
-            shouldLetterbox = true)
-    setUpPortraitDisplay()
-
-    spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface)
-    val wct = getLatestDragToDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() {
-    val task = setUpFreeformTask()
-    val spyController = spy(controller)
-    val mockSurface = mock(SurfaceControl::class.java)
-    val mockDisplayLayout = mock(DisplayLayout::class.java)
-    whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
-    whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
-    spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, -100, 500, 1000))
-
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-      .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
-    spyController.onDragPositioningEnd(
-        task,
-        mockSurface,
-        Point(100, -100), /* position */
-        PointF(200f, -200f), /* inputCoordinate */
-        Rect(100, -100, 500, 1000), /* currentDragBounds */
-        Rect(0, 50, 2000, 2000), /* validDragArea */
-        Rect() /* dragStartBounds */,
-        motionEvent,
-        desktopWindowDecoration,
-        )
-    val rectAfterEnd = Rect(100, 50, 500, 1150)
-    verify(transitions)
-        .startTransition(
-            eq(TRANSIT_CHANGE),
-            Mockito.argThat { wct ->
-              return@argThat wct.changes.any { (token, change) ->
-                change.configuration.windowConfiguration.bounds == rectAfterEnd
-              }
-            },
-            eq(null))
-  }
-
-  @Test
-  fun onDesktopDragEnd_noIndicator_updatesTaskBounds() {
-    val task = setUpFreeformTask()
-    val spyController = spy(controller)
-    val mockSurface = mock(SurfaceControl::class.java)
-    val mockDisplayLayout = mock(DisplayLayout::class.java)
-    whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
-    whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
-    spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
-
-    val currentDragBounds = Rect(100, 200, 500, 1000)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-      .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
-
-    spyController.onDragPositioningEnd(
-      task,
-      mockSurface,
-      Point(100, 200), /* position */
-      PointF(200f, 300f), /* inputCoordinate */
-      currentDragBounds, /* currentDragBounds */
-      Rect(0, 50, 2000, 2000) /* validDragArea */,
-      Rect() /* dragStartBounds */,
-      motionEvent,
-      desktopWindowDecoration,
-      )
-
-
-    verify(transitions)
-      .startTransition(
-        eq(TRANSIT_CHANGE),
-        Mockito.argThat { wct ->
-          return@argThat wct.changes.any { (token, change) ->
-            change.configuration.windowConfiguration.bounds == currentDragBounds
-          }
-        },
-        eq(null))
-  }
-
-  @Test
-  fun onDesktopDragEnd_fullscreenIndicator_dragToExitDesktop() {
-    val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100))
-    val spyController = spy(controller)
-    val mockSurface = mock(SurfaceControl::class.java)
-    val mockDisplayLayout = mock(DisplayLayout::class.java)
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-    whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
-    whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
-    whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
-      (i.arguments.first() as Rect).set(STABLE_BOUNDS)
-    }
-    whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(false)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-      .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
-
-    // Drag move the task to the top edge
-    spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
-    spyController.onDragPositioningEnd(
-      task,
-      mockSurface,
-      Point(100, 50), /* position */
-      PointF(200f, 300f), /* inputCoordinate */
-      Rect(100, 50, 500, 1000), /* currentDragBounds */
-      Rect(0, 50, 2000, 2000) /* validDragArea */,
-      Rect() /* dragStartBounds */,
-      motionEvent,
-      desktopWindowDecoration)
-
-    // Assert the task exits desktop mode
-    val wct = getLatestExitDesktopWct()
-    assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
-        .isEqualTo(WINDOWING_MODE_UNDEFINED)
-  }
-
-  @Test
-  fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize() {
-    val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100))
-    val spyController = spy(controller)
-    val mockSurface = mock(SurfaceControl::class.java)
-    val mockDisplayLayout = mock(DisplayLayout::class.java)
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-    whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
-    whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
-    whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
-      (i.arguments.first() as Rect).set(STABLE_BOUNDS)
-    }
-    whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-      .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
-
-    // Drag move the task to the top edge
-    val currentDragBounds = Rect(100, 50, 500, 1000)
-    spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
-    spyController.onDragPositioningEnd(
-      task,
-      mockSurface,
-      Point(100, 50), /* position */
-      PointF(200f, 300f), /* inputCoordinate */
-      currentDragBounds,
-      Rect(0, 50, 2000, 2000) /* validDragArea */,
-      Rect() /* dragStartBounds */,
-      motionEvent,
-      desktopWindowDecoration)
-
-    // Assert bounds set to stable bounds
-    val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
-    assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
-    // Assert event is properly logged
-    verify(desktopModeEventLogger, times(1)).logTaskResizingStarted(
-      ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER,
-      InputMethod.UNKNOWN_INPUT_METHOD,
-      task,
-      task.configuration.windowConfiguration.bounds.width(),
-      task.configuration.windowConfiguration.bounds.height(),
-      displayController
-    )
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER,
-      InputMethod.UNKNOWN_INPUT_METHOD,
-      task,
-      STABLE_BOUNDS.width(),
-      STABLE_BOUNDS.height(),
-      displayController
-    )
-  }
-
-  @Test
-  fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize_noBoundsChange() {
-    val task = setUpFreeformTask(bounds = STABLE_BOUNDS)
-    val spyController = spy(controller)
-    val mockSurface = mock(SurfaceControl::class.java)
-    val mockDisplayLayout = mock(DisplayLayout::class.java)
-    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
-    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-    whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
-    whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
-    whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
-      (i.arguments.first() as Rect).set(STABLE_BOUNDS)
-    }
-    whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
-      .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
-
-    // Drag move the task to the top edge
-    val currentDragBounds = Rect(100, 50, 500, 1000)
-    spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
-    spyController.onDragPositioningEnd(
-      task,
-      mockSurface,
-      Point(100, 50), /* position */
-      PointF(200f, 300f), /* inputCoordinate */
-      currentDragBounds, /* currentDragBounds */
-      Rect(0, 50, 2000, 2000) /* validDragArea */,
-      Rect() /* dragStartBounds */,
-      motionEvent,
-      desktopWindowDecoration)
-
-    // Assert that task is NOT updated via WCT
-    verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
-    // Assert that task leash is updated via Surface Animations
-    verify(mReturnToDragStartAnimator).start(
-      eq(task.taskId),
-      eq(mockSurface),
-      eq(currentDragBounds),
-      eq(STABLE_BOUNDS),
-      anyOrNull(),
-    )
-    // Assert no event is logged
-    verify(desktopModeEventLogger, never()).logTaskResizingStarted(
-      any(), any(), any(), any(), any(), any(), any()
-    )
-    verify(desktopModeEventLogger, never()).logTaskResizingEnded(
-      any(), any(), any(), any(), any(), any(), any()
-    )
-  }
-
-  @Test
-  fun enterSplit_freeformTaskIsMovedToSplit() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val task3 = setUpFreeformTask()
-
-    task1.isFocused = false
-    task2.isFocused = true
-    task3.isFocused = false
-
-    controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
-
-    verify(splitScreenController)
-        .requestEnterSplitSelect(
-            eq(task2),
-            any(),
-            eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
-            eq(task2.configuration.windowConfiguration.bounds))
-  }
-
-  @Test
-  fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val task3 = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    task1.isFocused = false
-    task2.isFocused = true
-    task3.isFocused = false
-    taskRepository.wallpaperActivityToken = wallpaperToken
-    taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
-    taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false)
-
-    controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
-
-    val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(splitScreenController)
-      .requestEnterSplitSelect(
-        eq(task2),
-        wctArgument.capture(),
-        eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
-        eq(task2.configuration.windowConfiguration.bounds))
-    // Removes wallpaper activity when leaving desktop
-    wctArgument.value.assertRemoveAt(index = 0, wallpaperToken)
-  }
-
-  @Test
-  fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() {
-    val task1 = setUpFreeformTask()
-    val task2 = setUpFreeformTask()
-    val task3 = setUpFreeformTask()
-    val wallpaperToken = MockToken().token()
-
-    task1.isFocused = false
-    task2.isFocused = true
-    task3.isFocused = false
-    taskRepository.wallpaperActivityToken = wallpaperToken
-
-    controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
-
-    val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(splitScreenController)
-      .requestEnterSplitSelect(
-        eq(task2),
-        wctArgument.capture(),
-        eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
-        eq(task2.configuration.windowConfiguration.bounds))
-    // Does not remove wallpaper activity, as desktop still has visible desktop tasks
-    assertThat(wctArgument.value.hierarchyOps).isEmpty()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
-  fun newWindow_fromFullscreenOpensInSplit() {
-    setUpLandscapeDisplay()
-    val task = setUpFullscreenTask()
-    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
-    runOpenNewWindow(task)
-    verify(splitScreenController)
-      .startIntent(any(), anyInt(), any(), any(),
-        optionsCaptor.capture(), anyOrNull(), eq(true), eq(SPLIT_INDEX_UNDEFINED)
-      )
-    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
-      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
-  fun newWindow_fromSplitOpensInSplit() {
-    setUpLandscapeDisplay()
-    val task = setUpSplitScreenTask()
-    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
-    runOpenNewWindow(task)
-    verify(splitScreenController)
-      .startIntent(
-        any(), anyInt(), any(), any(),
-        optionsCaptor.capture(), anyOrNull(), eq(true), eq(SPLIT_INDEX_UNDEFINED)
-      )
-    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
-      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
-  fun newWindow_fromFreeformAddsNewWindow() {
-    setUpLandscapeDisplay()
-    val task = setUpFreeformTask()
-    val wctCaptor = argumentCaptor<WindowContainerTransaction>()
-    val transition = Binder()
-    whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull(), any()))
-      .thenReturn(ExitResult.NoExit)
-    whenever(desktopMixedTransitionHandler
-      .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull()))
-      .thenReturn(transition)
-
-    runOpenNewWindow(task)
-
-    verify(desktopMixedTransitionHandler)
-      .startLaunchTransition(anyInt(), wctCaptor.capture(), anyOrNull(), anyOrNull(), anyOrNull())
-    assertThat(ActivityOptions.fromBundle(wctCaptor.firstValue.hierarchyOps[0].launchOptions)
-      .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
-  fun newWindow_fromFreeform_exitsImmersiveIfNeeded() {
-    setUpLandscapeDisplay()
-    val immersiveTask = setUpFreeformTask()
-    val task = setUpFreeformTask()
-    val runOnStart = RunOnStartTransitionCallback()
-    val transition = Binder()
-    whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull(), any()))
-      .thenReturn(ExitResult.Exit(immersiveTask.taskId, runOnStart))
-    whenever(desktopMixedTransitionHandler
-      .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull()))
-      .thenReturn(transition)
-
-    runOpenNewWindow(task)
-
-    runOnStart.assertOnlyInvocation(transition)
-  }
-
-  private fun runOpenNewWindow(task: RunningTaskInfo) {
-    markTaskVisible(task)
-    task.baseActivity = mock(ComponentName::class.java)
-    task.isFocused = true
-    runningTasks.add(task)
-    controller.openNewWindow(task)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
-  fun openInstance_fromFullscreenOpensInSplit() {
-    setUpLandscapeDisplay()
-    val task = setUpFullscreenTask()
-    val taskToRequest = setUpFreeformTask()
-    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
-    runOpenInstance(task, taskToRequest.taskId)
-    verify(splitScreenController)
-      .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
-    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
-      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
-  fun openInstance_fromSplitOpensInSplit() {
-    setUpLandscapeDisplay()
-    val task = setUpSplitScreenTask()
-    val taskToRequest = setUpFreeformTask()
-    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
-    runOpenInstance(task, taskToRequest.taskId)
-    verify(splitScreenController)
-      .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
-    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
-      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
-  fun openInstance_fromFreeformAddsNewWindow() {
-    setUpLandscapeDisplay()
-    val task = setUpFreeformTask()
-    val taskToRequest = setUpFreeformTask()
-    runOpenInstance(task, taskToRequest.taskId)
-    verify(desktopMixedTransitionHandler).startLaunchTransition(anyInt(), any(), anyInt(),
-      anyOrNull(), anyOrNull())
-    val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
-    assertThat(wct.hierarchyOps).hasSize(1)
-    wct.assertReorderAt(index = 0, taskToRequest)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
-  fun openInstance_fromFreeform_minimizesIfNeeded() {
-    setUpLandscapeDisplay()
-    val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() }
-    val oldestTask = freeformTasks.first()
-    val newestTask = freeformTasks.last()
-
-    val transition = Binder()
-    val wctCaptor = argumentCaptor<WindowContainerTransaction>()
-    whenever(desktopMixedTransitionHandler.startLaunchTransition(anyInt(), wctCaptor.capture(),
-      anyInt(), anyOrNull(), anyOrNull()
-    ))
-      .thenReturn(transition)
-
-    runOpenInstance(newestTask, freeformTasks[1].taskId)
-
-    val wct = wctCaptor.firstValue
-    assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize
-    wct.assertReorderAt(0, freeformTasks[1], toTop = true)
-    wct.assertReorderAt(1, oldestTask, toTop = false)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
-  fun openInstance_fromFreeform_exitsImmersiveIfNeeded() {
-    setUpLandscapeDisplay()
-    val freeformTask = setUpFreeformTask()
-    val immersiveTask = setUpFreeformTask()
-    taskRepository.setTaskInFullImmersiveState(
-      displayId = immersiveTask.displayId,
-      taskId = immersiveTask.taskId,
-      immersive = true
-    )
-    val runOnStartTransit = RunOnStartTransitionCallback()
-    val transition = Binder()
-    whenever(desktopMixedTransitionHandler.startLaunchTransition(anyInt(), any(), anyInt(),
-      anyOrNull(), anyOrNull()
-    ))
-      .thenReturn(transition)
-    whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(
-        any(), eq(DEFAULT_DISPLAY), eq(freeformTask.taskId), any()))
-      .thenReturn(
-        ExitResult.Exit(
-        exitingTask = immersiveTask.taskId,
-        runOnTransitionStart = runOnStartTransit,
-      ))
-
-    runOpenInstance(immersiveTask, freeformTask.taskId)
-
-    verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId), any())
-    runOnStartTransit.assertOnlyInvocation(transition)
-  }
-
-  private fun runOpenInstance(
-    callingTask: RunningTaskInfo,
-    requestedTaskId: Int
-  ) {
-    markTaskVisible(callingTask)
-    callingTask.baseActivity = mock(ComponentName::class.java)
-    callingTask.isFocused = true
-    runningTasks.add(callingTask)
-    controller.openInstance(callingTask, requestedTaskId)
-  }
-
-  @Test
-  fun toggleBounds_togglesToStableBounds() {
-    val bounds = Rect(0, 0, 100, 100)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
-
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.MAXIMIZE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
-        InputMethod.TOUCH
-      )
-    )
-
-    // Assert bounds set to stable bounds
-    val wct = getLatestToggleResizeDesktopTaskWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.MAXIMIZE_BUTTON,
-      InputMethod.TOUCH,
-      task,
-      STABLE_BOUNDS.width(),
-      STABLE_BOUNDS.height(),
-      displayController
-    )
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
-  fun snapToHalfScreen_getSnapBounds_calculatesBoundsForResizable() {
-    val bounds = Rect(100, 100, 300, 300)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
-      topActivityInfo = ActivityInfo().apply {
-        screenOrientation = SCREEN_ORIENTATION_LANDSCAPE
-        configuration.windowConfiguration.appBounds = bounds
-      }
-      isResizeable = true
-    }
-
-    val currentDragBounds = Rect(0, 100, 200, 300)
-    val expectedBounds = Rect(
-      STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
-    )
-
-    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT,
-      ResizeTrigger.SNAP_LEFT_MENU, InputMethod.TOUCH, desktopWindowDecoration)
-    // Assert bounds set to stable bounds
-    val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
-    assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.SNAP_LEFT_MENU,
-      InputMethod.TOUCH,
-      task,
-      expectedBounds.width(),
-      expectedBounds.height(),
-      displayController
-    )
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
-  fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() {
-    // Set up task to already be in snapped-left bounds
-    val bounds = Rect(
-      STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
-    )
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
-      topActivityInfo = ActivityInfo().apply {
-        screenOrientation = SCREEN_ORIENTATION_LANDSCAPE
-        configuration.windowConfiguration.appBounds = bounds
-      }
-      isResizeable = true
-    }
-
-    // Attempt to snap left again
-    val currentDragBounds = Rect(bounds).apply { offset(-100, 0) }
-    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT,
-      ResizeTrigger.SNAP_LEFT_MENU, InputMethod.TOUCH, desktopWindowDecoration)
-    // Assert that task is NOT updated via WCT
-    verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
-
-    // Assert that task leash is updated via Surface Animations
-    verify(mReturnToDragStartAnimator).start(
-      eq(task.taskId),
-      eq(mockSurface),
-      eq(currentDragBounds),
-      eq(bounds),
-      anyOrNull(),
-    )
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.SNAP_LEFT_MENU,
-      InputMethod.TOUCH,
-      task,
-      bounds.width(),
-      bounds.height(),
-      displayController
-    )
-  }
-
-  @Test
-  @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, Flags.FLAG_ENABLE_TILE_RESIZING)
-  fun handleSnapResizingTaskOnDrag_nonResizable_snapsToHalfScreen() {
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply {
-      isResizeable = false
-    }
-    val preDragBounds = Rect(100, 100, 400, 500)
-    val currentDragBounds = Rect(0, 100, 300, 500)
-    val expectedBounds =
-      Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom)
-
-    controller.handleSnapResizingTaskOnDrag(
-
-      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent,
-      desktopWindowDecoration
-    )
-    val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
-    assertThat(findBoundsChange(wct, task)).isEqualTo(
-      expectedBounds
-    )
-    verify(desktopModeEventLogger, times(1)).logTaskResizingStarted(
-      ResizeTrigger.DRAG_LEFT,
-      InputMethod.UNKNOWN_INPUT_METHOD,
-      task,
-      preDragBounds.width(),
-      preDragBounds.height(),
-      displayController
-    )
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING)
-  fun handleSnapResizingTaskOnDrag_nonResizable_startsRepositionAnimation() {
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply {
-      isResizeable = false
-    }
-    val preDragBounds = Rect(100, 100, 400, 500)
-    val currentDragBounds = Rect(0, 100, 300, 500)
-
-    controller.handleSnapResizingTaskOnDrag(
-      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent,
-      desktopWindowDecoration)
-    verify(mReturnToDragStartAnimator).start(
-      eq(task.taskId),
-      eq(mockSurface),
-      eq(currentDragBounds),
-      eq(preDragBounds),
-      any(),
-    )
-    verify(desktopModeEventLogger, never()).logTaskResizingStarted(
-      any(),
-      any(),
-      any(),
-      any(),
-      any(),
-      any(),
-      any()
-    )
-  }
-
-  @Test
-  @EnableFlags(
-    Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING
-  )
-  fun handleInstantSnapResizingTask_nonResizable_animatorNotStartedAndShowsToast() {
-    val taskBounds = Rect(0, 0, 200, 100)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply {
-      isResizeable = false
-    }
-
-    controller.handleInstantSnapResizingTask(
-      task,
-      SnapPosition.LEFT,
-      ResizeTrigger.SNAP_LEFT_MENU,
-      InputMethod.MOUSE,
-      desktopWindowDecoration
-    )
-
-    // Assert that task is NOT updated via WCT
-    verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
-    verify(mockToast).show()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING)
-  @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
-  fun handleInstantSnapResizingTask_resizable_snapsToHalfScreenAndNotShowToast() {
-    val taskBounds = Rect(0, 0, 200, 100)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply {
-      isResizeable = true
-    }
-    val expectedBounds = Rect(
-      STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
-    )
-
-    controller.handleInstantSnapResizingTask(
-      task, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.MOUSE,
-      desktopWindowDecoration
-    )
-
-    // Assert bounds set to half of the stable bounds
-    val wct = getLatestToggleResizeDesktopTaskWct(taskBounds)
-    assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
-    verify(mockToast, never()).show()
-    verify(desktopModeEventLogger, times(1)).logTaskResizingStarted(
-      ResizeTrigger.SNAP_LEFT_MENU,
-      InputMethod.MOUSE,
-      task,
-      taskBounds.width(),
-      taskBounds.height(),
-      displayController
-    )
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.SNAP_LEFT_MENU,
-      InputMethod.MOUSE,
-      task,
-      expectedBounds.width(),
-      expectedBounds.height(),
-      displayController
-    )
-  }
-
-  @Test
-  fun toggleBounds_togglesToCalculatedBoundsForNonResizable() {
-    val bounds = Rect(0, 0, 200, 100)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
-      topActivityInfo = ActivityInfo().apply {
-        screenOrientation = SCREEN_ORIENTATION_LANDSCAPE
-        configuration.windowConfiguration.appBounds = bounds
-      }
-      appCompatTaskInfo.topActivityLetterboxAppWidth = bounds.width()
-      appCompatTaskInfo.topActivityLetterboxAppHeight = bounds.height()
-      isResizeable = false
-    }
-
-    // Bounds should be 1000 x 500, vertically centered in the 1000 x 1000 stable bounds
-    val expectedBounds = Rect(STABLE_BOUNDS.left, 250, STABLE_BOUNDS.right, 750)
-
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.MAXIMIZE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
-        InputMethod.TOUCH
-      )
-    )
-
-    // Assert bounds set to stable bounds
-    val wct = getLatestToggleResizeDesktopTaskWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.MAXIMIZE_BUTTON,
-      InputMethod.TOUCH,
-      task,
-      expectedBounds.width(),
-      expectedBounds.height(),
-      displayController
-    )
-  }
-
-  @Test
-  fun toggleBounds_lastBoundsBeforeMaximizeSaved() {
-    val bounds = Rect(0, 0, 100, 100)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
-
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.MAXIMIZE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
-        InputMethod.TOUCH
-      )
-    )
-
-    assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds)
-    verify(desktopModeEventLogger, never()).logTaskResizingEnded(
-      any(), any(), any(), any(),
-      any(), any(), any()
-    )
-  }
-
-  @Test
-  fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() {
-    val boundsBeforeMaximize = Rect(0, 0, 100, 100)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
-
-    // Maximize
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.MAXIMIZE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
-        InputMethod.TOUCH
-      )
-    )
-    task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
-
-    // Restore
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.RESTORE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
-        InputMethod.TOUCH
-      )
-    )
-
-    // Assert bounds set to last bounds before maximize
-    val wct = getLatestToggleResizeDesktopTaskWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.MAXIMIZE_BUTTON,
-      InputMethod.TOUCH,
-      task,
-      boundsBeforeMaximize.width(),
-      boundsBeforeMaximize.height(),
-      displayController
-    )
-  }
-
-  @Test
-  fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualWidth() {
-    val boundsBeforeMaximize = Rect(0, 0, 100, 100)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply {
-      isResizeable = false
-    }
-
-    // Maximize
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.MAXIMIZE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
-        InputMethod.TOUCH
-      )
-    )
-    task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS.left,
-      boundsBeforeMaximize.top, STABLE_BOUNDS.right, boundsBeforeMaximize.bottom)
-
-    // Restore
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.RESTORE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
-        InputMethod.TOUCH
-      )
-    )
-
-    // Assert bounds set to last bounds before maximize
-    val wct = getLatestToggleResizeDesktopTaskWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.MAXIMIZE_BUTTON,
-      InputMethod.TOUCH,
-      task,
-      boundsBeforeMaximize.width(),
-      boundsBeforeMaximize.height(),
-      displayController
-    )
-  }
-
-  @Test
-  fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualHeight() {
-    val boundsBeforeMaximize = Rect(0, 0, 100, 100)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply {
-      isResizeable = false
-    }
-
-    // Maximize
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.MAXIMIZE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
-        InputMethod.TOUCH
-      )
-    )
-    task.configuration.windowConfiguration.bounds.set(boundsBeforeMaximize.left,
-      STABLE_BOUNDS.top, boundsBeforeMaximize.right, STABLE_BOUNDS.bottom)
-
-    // Restore
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.RESTORE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
-        InputMethod.TOUCH
-      )
-    )
-
-    // Assert bounds set to last bounds before maximize
-    val wct = getLatestToggleResizeDesktopTaskWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.MAXIMIZE_BUTTON,
-      InputMethod.TOUCH,
-      task,
-      boundsBeforeMaximize.width(),
-      boundsBeforeMaximize.height(),
-      displayController
-    )
-  }
-
-  @Test
-  fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() {
-    val boundsBeforeMaximize = Rect(0, 0, 100, 100)
-    val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
-
-    // Maximize
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.MAXIMIZE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
-        InputMethod.TOUCH
-      )
-    )
-    task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
-
-    // Restore
-    controller.toggleDesktopTaskSize(
-      task,
-      ToggleTaskSizeInteraction(
-        ToggleTaskSizeInteraction.Direction.RESTORE,
-        ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
-        InputMethod.TOUCH
-      )
-    )
-
-    // Assert last bounds before maximize removed after use
-    assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
-    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
-      ResizeTrigger.MAXIMIZE_BUTTON,
-      InputMethod.TOUCH,
-      task,
-      boundsBeforeMaximize.width(),
-      boundsBeforeMaximize.height(),
-      displayController
-    )
-  }
-
-  @Test
-  fun onUnhandledDrag_newFreeformIntent() {
-    testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
-      PointF(1200f, 700f),
-      Rect(240, 700, 2160, 1900))
-  }
-
-  @Test
-  fun onUnhandledDrag_newFreeformIntentSplitLeft() {
-    testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
-      PointF(50f, 700f),
-      Rect(0, 0, 500, 1000))
-  }
-
-  @Test
-  fun onUnhandledDrag_newFreeformIntentSplitRight() {
-    testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
-      PointF(2500f, 700f),
-      Rect(500, 0, 1000, 1000))
-  }
-
-  @Test
-  fun onUnhandledDrag_newFullscreenIntent() {
-    testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
-      PointF(1200f, 50f),
-      Rect())
-  }
-
-  @Test
-  fun shellController_registersUserChangeListener() {
-      verify(shellController, times(2)).addUserChangeListener(any())
-  }
-
-  @Test
-  @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun onTaskInfoChanged_inImmersiveUnrequestsImmersive_exits() {
-    val task = setUpFreeformTask(DEFAULT_DISPLAY)
-    taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = true)
-
-    task.requestedVisibleTypes = WindowInsets.Type.statusBars()
-    controller.onTaskInfoChanged(task)
-
-    verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(eq(task), any())
-  }
-
-  @Test
-  @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun onTaskInfoChanged_notInImmersiveUnrequestsImmersive_noReExit() {
-    val task = setUpFreeformTask(DEFAULT_DISPLAY)
-    taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = false)
-
-    task.requestedVisibleTypes = WindowInsets.Type.statusBars()
-    controller.onTaskInfoChanged(task)
-
-    verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any())
-  }
-
-  @Test
-  @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun onTaskInfoChanged_inImmersiveUnrequestsImmersive_inRecentsTransition_noExit() {
-    val task = setUpFreeformTask(DEFAULT_DISPLAY)
-    taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = true)
-    recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_REQUESTED)
-
-    task.requestedVisibleTypes = WindowInsets.Type.statusBars()
-    controller.onTaskInfoChanged(task)
-
-    verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any())
-  }
-
-  @Test
-  fun moveTaskToDesktop_background_attemptsImmersiveExit() {
-    val task = setUpFreeformTask(background = true)
-    val wct = WindowContainerTransaction()
-    val runOnStartTransit = RunOnStartTransitionCallback()
-    val transition = Binder()
-    whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()))
-      .thenReturn(
-        ExitResult.Exit(
-        exitingTask = 5,
-        runOnTransitionStart = runOnStartTransit,
-      ))
-    whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
-
-    controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
-
-    verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())
-    runOnStartTransit.assertOnlyInvocation(transition)
-  }
-
-  @Test
-  fun moveTaskToDesktop_foreground_attemptsImmersiveExit() {
-    val task = setUpFreeformTask(background = false)
-    val wct = WindowContainerTransaction()
-    val runOnStartTransit = RunOnStartTransitionCallback()
-    val transition = Binder()
-    whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()))
-      .thenReturn(
-        ExitResult.Exit(
-        exitingTask = 5,
-        runOnTransitionStart = runOnStartTransit,
-      ))
-    whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
-
-    controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
-
-    verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())
-    runOnStartTransit.assertOnlyInvocation(transition)
-  }
-
-  @Test
-  fun moveTaskToFront_background_attemptsImmersiveExit() {
-    val task = setUpFreeformTask(background = true)
-    val runOnStartTransit = RunOnStartTransitionCallback()
-    val transition = Binder()
-    whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any()))
-      .thenReturn(
-        ExitResult.Exit(
-        exitingTask = 5,
-        runOnTransitionStart = runOnStartTransit,
-      ))
-    whenever(desktopMixedTransitionHandler
-      .startLaunchTransition(any(), any(), anyInt(), anyOrNull(), anyOrNull()))
-      .thenReturn(transition)
-
-    controller.moveTaskToFront(task.taskId, remoteTransition = null)
-
-    verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any())
-    runOnStartTransit.assertOnlyInvocation(transition)
-  }
-
-  @Test
-  fun moveTaskToFront_foreground_attemptsImmersiveExit() {
-    val task = setUpFreeformTask(background = false)
-    val runOnStartTransit = RunOnStartTransitionCallback()
-    val transition = Binder()
-    whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any()))
-      .thenReturn(
-        ExitResult.Exit(
-        exitingTask = 5,
-        runOnTransitionStart = runOnStartTransit,
-      ))
-    whenever(desktopMixedTransitionHandler
-      .startLaunchTransition(any(), any(), eq(task.taskId), anyOrNull(), anyOrNull()))
-      .thenReturn(transition)
-
-    controller.moveTaskToFront(task.taskId, remoteTransition = null)
-
-    verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any())
-    runOnStartTransit.assertOnlyInvocation(transition)
-  }
-
-  @Test
-  fun handleRequest_freeformLaunchToDesktop_attemptsImmersiveExit() {
-    markTaskVisible(setUpFreeformTask())
-    val task = setUpFreeformTask()
-    markTaskVisible(task)
-    val binder = Binder()
-
-    controller.handleRequest(binder, createTransition(task))
-
-    verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any())
-  }
-
-  @Test
-  fun handleRequest_fullscreenLaunchToDesktop_attemptsImmersiveExit() {
-    setUpFreeformTask()
-    val task = setUpFullscreenTask()
-    val binder = Binder()
-
-    controller.handleRequest(binder, createTransition(task))
-
-    verify(mMockDesktopImmersiveController)
-      .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any())
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() {
-    val triggerTask = setUpFullscreenTask(displayId = 5)
-    taskRepository.setTaskInFullImmersiveState(
-      displayId = triggerTask.displayId,
-      taskId = triggerTask.taskId,
-      immersive = true
-    )
-
-    assertThat(controller.shouldPlayDesktopAnimation(
-      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
-    )).isFalse()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() {
-    val triggerTask = setUpFreeformTask(displayId = 5)
-    taskRepository.setTaskInFullImmersiveState(
-      displayId = triggerTask.displayId,
-      taskId = triggerTask.taskId,
-      immersive = true
-    )
-
-    assertThat(controller.shouldPlayDesktopAnimation(
-      TransitionRequestInfo(TRANSIT_CHANGE, triggerTask, /* remoteTransition= */ null)
-    )).isFalse()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() {
-    val triggerTask = setUpFreeformTask(displayId = 5)
-    taskRepository.setTaskInFullImmersiveState(
-      displayId = triggerTask.displayId,
-      taskId = triggerTask.taskId,
-      immersive = false
-    )
-
-    assertThat(controller.shouldPlayDesktopAnimation(
-      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
-    )).isFalse()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() {
-    // At least one freeform task to be in a desktop.
-    val existingTask = setUpFreeformTask(displayId = 5)
-    val triggerTask = setUpFullscreenTask(displayId = 5)
-    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
-    taskRepository.setTaskInFullImmersiveState(
-      displayId = existingTask.displayId,
-      taskId = existingTask.taskId,
-      immersive = true
-    )
-
-    assertThat(
-      controller.shouldPlayDesktopAnimation(
-        TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
-      )
-    ).isTrue()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() {
-    val triggerTask = setUpFullscreenTask(displayId = 5)
-    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
-
-    assertThat(controller.shouldPlayDesktopAnimation(
-      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
-    )).isFalse()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() {
-    // At least one freeform task to be in a desktop.
-    val existingTask = setUpFreeformTask(displayId = 5)
-    val triggerTask = setUpFreeformTask(displayId = 5, active = false)
-    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
-    taskRepository.setTaskInFullImmersiveState(
-      displayId = existingTask.displayId,
-      taskId = existingTask.taskId,
-      immersive = true
-    )
-
-    assertThat(
-      controller.shouldPlayDesktopAnimation(
-        TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
-      )
-    ).isTrue()
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-  fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() {
-    val triggerTask = setUpFreeformTask(displayId = 5, active = false)
-    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
-
-    assertThat(controller.shouldPlayDesktopAnimation(
-      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
-    )).isFalse()
-  }
-
-  private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
-    var invocations = 0
-      private set
-    var lastInvoked: IBinder? = null
-      private set
-
-    override fun invoke(transition: IBinder) {
-      invocations++
-      lastInvoked = transition
-    }
-  }
-
-  private fun RunOnStartTransitionCallback.assertOnlyInvocation(transition: IBinder) {
-    assertThat(invocations).isEqualTo(1)
-    assertThat(lastInvoked).isEqualTo(transition)
-  }
-
-  /**
-   * Assert that an unhandled drag event launches a PendingIntent with the
-   * windowing mode and bounds we are expecting.
-   */
-  private fun testOnUnhandledDrag(
-    indicatorType: DesktopModeVisualIndicator.IndicatorType,
-    inputCoordinate: PointF,
-    expectedBounds: Rect
-  ) {
-    setUpLandscapeDisplay()
-    val task = setUpFreeformTask()
-    markTaskVisible(task)
-    task.isFocused = true
-    val runningTasks = ArrayList<RunningTaskInfo>()
-    runningTasks.add(task)
-    val spyController = spy(controller)
-    val mockPendingIntent = mock(PendingIntent::class.java)
-    val mockDragEvent = mock(DragEvent::class.java)
-    val mockCallback = mock(Consumer::class.java)
-    val b = SurfaceControl.Builder()
-    b.setName("test surface")
-    val dragSurface = b.build()
-    whenever(shellTaskOrganizer.runningTasks).thenReturn(runningTasks)
-    whenever(mockDragEvent.dragSurface).thenReturn(dragSurface)
-    whenever(mockDragEvent.x).thenReturn(inputCoordinate.x)
-    whenever(mockDragEvent.y).thenReturn(inputCoordinate.y)
-    whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true)
-    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
-    doReturn(indicatorType)
-      .whenever(spyController).updateVisualIndicator(
-        eq(task),
-        anyOrNull(),
-        anyOrNull(),
-        anyOrNull(),
-        eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
-      )
-
-    spyController.onUnhandledDrag(
-      mockPendingIntent,
-      mockDragEvent,
-      mockCallback as Consumer<Boolean>
-    )
-    val arg: ArgumentCaptor<WindowContainerTransaction> =
-      ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    var expectedWindowingMode: Int
-      if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) {
-        expectedWindowingMode = WINDOWING_MODE_FULLSCREEN
-        // Fullscreen launches currently use default transitions
-        verify(transitions).startTransition(any(), capture(arg), anyOrNull())
-      } else {
-        expectedWindowingMode = WINDOWING_MODE_FREEFORM
-        // All other launches use a special handler.
-        verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg))
-      }
-    assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
-      .launchWindowingMode).isEqualTo(expectedWindowingMode)
-    assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
-      .launchBounds).isEqualTo(expectedBounds)
-  }
-
-  private val desktopWallpaperIntent: Intent
-    get() = Intent(context, DesktopWallpaperActivity::class.java)
-
-  private fun addFreeformTaskAtPosition(
-    pos: DesktopTaskPosition,
-    stableBounds: Rect,
-    bounds: Rect = DEFAULT_LANDSCAPE_BOUNDS,
-    offsetPos: Point = Point(0, 0)
-  ): RunningTaskInfo {
-    val offset = pos.getTopLeftCoordinates(stableBounds, bounds)
-    val prevTaskBounds = Rect(bounds)
-    prevTaskBounds.offsetTo(offset.x + offsetPos.x, offset.y + offsetPos.y)
-    return setUpFreeformTask(bounds = prevTaskBounds)
-  }
-
-  private fun setUpFreeformTask(
-      displayId: Int = DEFAULT_DISPLAY,
-      bounds: Rect? = null,
-      active: Boolean = true,
-      background: Boolean = false,
-  ): RunningTaskInfo {
-    val task = createFreeformTask(displayId, bounds)
-    val activityInfo = ActivityInfo()
-    task.topActivityInfo = activityInfo
-    if (background) {
-      whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
-      whenever(recentTasksController.findTaskInBackground(task.taskId))
-        .thenReturn(createTaskInfo(task.taskId))
-    } else {
-      whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
-    }
-    taskRepository.addTask(displayId, task.taskId, isVisible = active)
-    if (!background) {
-      runningTasks.add(task)
-    }
-    return task
-  }
-
-  private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo {
-    return setUpFreeformTask().apply {
-      pictureInPictureParams = PictureInPictureParams.Builder()
-        .setAutoEnterEnabled(autoEnterEnabled)
-        .build()
-    }
-  }
-
-  private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
-    val task = createHomeTask(displayId)
-    whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
-    runningTasks.add(task)
-    return task
-  }
-
-  private fun setUpFullscreenTask(
-    displayId: Int = DEFAULT_DISPLAY,
-    isResizable: Boolean = true,
-    windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
-    deviceOrientation: Int = ORIENTATION_LANDSCAPE,
-    screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED,
-    shouldLetterbox: Boolean = false,
-    gravity: Int = Gravity.NO_GRAVITY,
-    enableUserFullscreenOverride: Boolean = false,
-    enableSystemFullscreenOverride: Boolean = false,
-    aspectRatioOverrideApplied: Boolean = false
-  ): RunningTaskInfo {
-    val task = createFullscreenTask(displayId)
-    val activityInfo = ActivityInfo()
-    activityInfo.screenOrientation = screenOrientation
-    activityInfo.windowLayout = ActivityInfo.WindowLayout(0, 0F, 0, 0F, gravity, 0, 0)
-    with(task) {
-      topActivityInfo = activityInfo
-      isResizeable = isResizable
-      configuration.orientation = deviceOrientation
-      configuration.windowConfiguration.windowingMode = windowingMode
-      appCompatTaskInfo.isUserFullscreenOverrideEnabled = enableUserFullscreenOverride
-      appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride
-
-      if (deviceOrientation == ORIENTATION_LANDSCAPE) {
-        configuration.windowConfiguration.appBounds =
-          Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
-        appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_LONG
-        appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_SHORT
-      } else {
-        configuration.windowConfiguration.appBounds =
-          Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG)
-        appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_SHORT
-        appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_LONG
-      }
-
-      if (shouldLetterbox) {
-        appCompatTaskInfo.setHasMinAspectRatioOverride(aspectRatioOverrideApplied)
-        if (deviceOrientation == ORIENTATION_LANDSCAPE &&
-            screenOrientation == SCREEN_ORIENTATION_PORTRAIT) {
-          // Letterbox to portrait size
-          appCompatTaskInfo.setTopActivityLetterboxed(true)
-          appCompatTaskInfo.topActivityLetterboxAppWidth = 1200
-          appCompatTaskInfo.topActivityLetterboxAppHeight = 1600
-        } else if (deviceOrientation == ORIENTATION_PORTRAIT &&
-            screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) {
-          // Letterbox to landscape size
-          appCompatTaskInfo.setTopActivityLetterboxed(true)
-          appCompatTaskInfo.topActivityLetterboxAppWidth = 1600
-          appCompatTaskInfo.topActivityLetterboxAppHeight = 1200
+        whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
+        whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
+        whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() }
+        whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(STABLE_BOUNDS)
         }
-      }
-    }
-    whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
-    runningTasks.add(task)
-    return task
-  }
+        whenever(runBlocking { persistentRepository.readDesktop(any(), any()) })
+            .thenReturn(Desktop.getDefaultInstance())
+        doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) }
 
-  private fun setUpLandscapeDisplay() {
-    whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG)
-    whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT)
-    val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG,
-      DISPLAY_DIMENSION_SHORT - Companion.TASKBAR_FRAME_HEIGHT
+        val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
+        whenever(
+                mMockDesktopImmersiveController.exitImmersiveIfApplicable(
+                    any(),
+                    any<RunningTaskInfo>(),
+                    any(),
+                )
+            )
+            .thenReturn(ExitResult.NoExit)
+        whenever(
+                mMockDesktopImmersiveController.exitImmersiveIfApplicable(
+                    any(),
+                    anyInt(),
+                    anyOrNull(),
+                    any(),
+                )
+            )
+            .thenReturn(ExitResult.NoExit)
+
+        controller = createController()
+        controller.setSplitScreenController(splitScreenController)
+        controller.freeformTaskTransitionStarter = freeformTaskTransitionStarter
+        controller.desktopModeEnterExitTransitionListener = desktopModeEnterExitTransitionListener
+
+        shellInit.init()
+
+        val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java)
+        verify(recentsTransitionHandler).addTransitionStateListener(captor.capture())
+        recentsTransitionStateListener = captor.value
+
+        controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener
+
+        assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+        taskRepository = userRepositories.current
+    }
+
+    private fun createController(): DesktopTasksController {
+        return DesktopTasksController(
+            context,
+            shellInit,
+            shellCommandHandler,
+            shellController,
+            displayController,
+            shellTaskOrganizer,
+            syncQueue,
+            rootTaskDisplayAreaOrganizer,
+            dragAndDropController,
+            transitions,
+            keyguardManager,
+            mReturnToDragStartAnimator,
+            desktopMixedTransitionHandler,
+            enterDesktopTransitionHandler,
+            exitDesktopTransitionHandler,
+            dragAndDropTransitionHandler,
+            toggleResizeDesktopTaskTransitionHandler,
+            dragToDesktopTransitionHandler,
+            mMockDesktopImmersiveController,
+            userRepositories,
+            recentsTransitionHandler,
+            multiInstanceHelper,
+            shellExecutor,
+            Optional.of(desktopTasksLimiter),
+            recentTasksController,
+            mockInteractionJankMonitor,
+            mockHandler,
+            desktopModeEventLogger,
+            desktopModeUiEventLogger,
+            desktopTilingDecorViewModel,
+        )
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+
+        runningTasks.clear()
+        testScope.cancel()
+    }
+
+    @Test
+    fun instantiate_addInitCallback() {
+        verify(shellInit).addInitCallback(any(), any<DesktopTasksController>())
+    }
+
+    @Test
+    fun doesAnyTaskRequireTaskbarRounding_onlyFreeFormTaskIsRunning_returnFalse() {
+        setUpFreeformTask()
+
+        assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isFalse()
+    }
+
+    @Test
+    fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() {
+        val task1 = setUpFreeformTask()
+
+        val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+        controller.toggleDesktopTaskSize(
+            task1,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.MAXIMIZE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
+                InputMethod.TOUCH,
+            ),
+        )
+
+        verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.MAXIMIZE_BUTTON,
+                InputMethod.TOUCH,
+                task1,
+                STABLE_BOUNDS.width(),
+                STABLE_BOUNDS.height(),
+                displayController,
+            )
+        assertThat(argumentCaptor.value).isTrue()
+    }
+
+    @Test
+    fun doesAnyTaskRequireTaskbarRounding_fullScreenTaskIsRunning_returnTrue() {
+        val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
+        setUpFreeformTask(bounds = stableBounds, active = true)
+        assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue()
+    }
+
+    @Test
+    fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfMaximizedTask_returnFalse() {
+        val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
+        val task1 = setUpFreeformTask(bounds = stableBounds, active = true)
+
+        val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+        controller.toggleDesktopTaskSize(
+            task1,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.RESTORE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
+                InputMethod.TOUCH,
+            ),
+        )
+
+        verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                eq(ResizeTrigger.MAXIMIZE_BUTTON),
+                eq(InputMethod.TOUCH),
+                eq(task1),
+                anyOrNull(),
+                anyOrNull(),
+                eq(displayController),
+                anyOrNull(),
+            )
+        assertThat(argumentCaptor.value).isFalse()
+    }
+
+    @Test
+    fun doesAnyTaskRequireTaskbarRounding_splitScreenTaskIsRunning_returnTrue() {
+        val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
+        setUpFreeformTask(
+            bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom)
+        )
+
+        assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue()
+    }
+
+    @Test
+    fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() {
+        whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
+        clearInvocations(shellInit)
+
+        createController()
+
+        verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() {
+        val homeTask = setUpHomeTask()
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        markTaskHidden(task1)
+        markTaskHidden(task2)
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: home, task1, task2
+        wct.assertReorderAt(index = 0, homeTask)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        markTaskHidden(task1)
+        markTaskHidden(task2)
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: wallpaper intent, task1, task2
+        wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
+    }
+
+    @Test
+    fun isDesktopModeShowing_noTasks_returnsFalse() {
+        assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
+    }
+
+    @Test
+    fun isDesktopModeShowing_noTasksVisible_returnsFalse() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        markTaskHidden(task1)
+        markTaskHidden(task2)
+
+        assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
+    }
+
+    @Test
+    fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        markTaskVisible(task1)
+        markTaskHidden(task2)
+
+        assertThat(controller.isDesktopModeShowing(displayId = 0)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() {
+        val homeTask = setUpHomeTask(SECOND_DISPLAY)
+        val task1 = setUpFreeformTask(SECOND_DISPLAY)
+        val task2 = setUpFreeformTask(SECOND_DISPLAY)
+        markTaskHidden(task1)
+        markTaskHidden(task2)
+
+        controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: home, task1, task2 (no wallpaper intent)
+        wct.assertReorderAt(index = 0, homeTask)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() {
+        val homeTask = setUpHomeTask()
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        markTaskVisible(task1)
+        markTaskVisible(task2)
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: home, task1, task2
+        wct.assertReorderAt(index = 0, homeTask)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() {
+        val homeTask = setUpHomeTask(SECOND_DISPLAY)
+        val task1 = setUpFreeformTask(SECOND_DISPLAY)
+        val task2 = setUpFreeformTask(SECOND_DISPLAY)
+        markTaskHidden(task1)
+        markTaskHidden(task2)
+
+        controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: home, task1, task2
+        wct.assertReorderAt(index = 0, homeTask)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        markTaskVisible(task1)
+        markTaskVisible(task2)
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: wallpaper intent, task1, task2
+        wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() {
+        val homeTask = setUpHomeTask()
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        markTaskHidden(task1)
+        markTaskVisible(task2)
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: home, task1, task2
+        wct.assertReorderAt(index = 0, homeTask)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        markTaskHidden(task1)
+        markTaskVisible(task2)
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Expect order to be from bottom: wallpaper intent, task1, task2
+        wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+        wct.assertReorderAt(index = 1, task1)
+        wct.assertReorderAt(index = 2, task2)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() {
+        val homeTask = setUpHomeTask()
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(1)
+        wct.assertReorderAt(index = 0, homeTask)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() {
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
+        val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
+        val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
+        setUpHomeTask(SECOND_DISPLAY)
+        val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
+        markTaskHidden(taskDefaultDisplay)
+        markTaskHidden(taskSecondDisplay)
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(2)
+        // Expect order to be from bottom: home, task
+        wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
+        wct.assertReorderAt(index = 1, taskDefaultDisplay)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() {
+        val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
+        val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
+        setUpHomeTask(SECOND_DISPLAY)
+        val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
+        markTaskHidden(taskDefaultDisplay)
+        markTaskHidden(taskSecondDisplay)
+
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Move home to front
+        wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
+        // Add desktop wallpaper activity
+        wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent)
+        // Move freeform task to front
+        wct.assertReorderAt(index = 2, taskDefaultDisplay)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() {
+        val homeTask = setUpHomeTask()
+        val freeformTask = setUpFreeformTask()
+        val minimizedTask = setUpFreeformTask()
+
+        markTaskHidden(freeformTask)
+        markTaskHidden(minimizedTask)
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(2)
+        // Reorder home and freeform task to top, don't reorder the minimized task
+        wct.assertReorderAt(index = 0, homeTask, toTop = true)
+        wct.assertReorderAt(index = 1, freeformTask, toTop = true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() {
+        val homeTask = setUpHomeTask()
+        val freeformTask = setUpFreeformTask()
+        val minimizedTask = setUpFreeformTask()
+
+        markTaskHidden(freeformTask)
+        markTaskHidden(minimizedTask)
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+        val wct =
+            getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // Move home to front
+        wct.assertReorderAt(index = 0, homeTask, toTop = true)
+        // Add desktop wallpaper activity
+        wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent)
+        // Reorder freeform task to top, don't reorder the minimized task
+        wct.assertReorderAt(index = 2, freeformTask, toTop = true)
+    }
+
+    @Test
+    fun visibleTaskCount_noTasks_returnsZero() {
+        assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+    }
+
+    @Test
+    fun visibleTaskCount_twoTasks_bothVisible_returnsTwo() {
+        setUpHomeTask()
+        setUpFreeformTask().also(::markTaskVisible)
+        setUpFreeformTask().also(::markTaskVisible)
+        assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
+    }
+
+    @Test
+    fun visibleTaskCount_twoTasks_oneVisible_returnsOne() {
+        setUpHomeTask()
+        setUpFreeformTask().also(::markTaskVisible)
+        setUpFreeformTask().also(::markTaskHidden)
+        assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+    }
+
+    @Test
+    fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() {
+        setUpHomeTask()
+        setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible)
+        setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible)
+        assertThat(controller.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
+    }
+
+    @Test
+    fun addMoveToDesktopChanges_gravityLeft_noBoundsApplied() {
+        setUpLandscapeDisplay()
+        val task = setUpFullscreenTask(gravity = Gravity.LEFT)
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(finalBounds).isEqualTo(Rect())
+    }
+
+    @Test
+    fun addMoveToDesktopChanges_gravityRight_noBoundsApplied() {
+        setUpLandscapeDisplay()
+        val task = setUpFullscreenTask(gravity = Gravity.RIGHT)
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(finalBounds).isEqualTo(Rect())
+    }
+
+    @Test
+    fun addMoveToDesktopChanges_gravityTop_noBoundsApplied() {
+        setUpLandscapeDisplay()
+        val task = setUpFullscreenTask(gravity = Gravity.TOP)
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(finalBounds).isEqualTo(Rect())
+    }
+
+    @Test
+    fun addMoveToDesktopChanges_gravityBottom_noBoundsApplied() {
+        setUpLandscapeDisplay()
+        val task = setUpFullscreenTask(gravity = Gravity.BOTTOM)
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(finalBounds).isEqualTo(Rect())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun handleRequest_newFreeformTaskLaunch_cascadeApplied() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
+        val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS, active = false)
+
+        val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+        assertNotNull(wct, "should handle request")
+        val finalBounds = findBoundsChange(wct, freeformTask)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.BottomRight)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun handleRequest_freeformTaskAlreadyExistsInDesktopMode_cascadeNotApplied() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
+        val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
+
+        val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+        assertNull(wct, "should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun addMoveToDesktopChanges_positionBottomRight() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)
+
+        val task = setUpFullscreenTask()
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.BottomRight)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun addMoveToDesktopChanges_positionTopLeft() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        addFreeformTaskAtPosition(DesktopTaskPosition.BottomRight, stableBounds)
+
+        val task = setUpFullscreenTask()
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.TopLeft)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun addMoveToDesktopChanges_positionBottomLeft() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        addFreeformTaskAtPosition(DesktopTaskPosition.TopLeft, stableBounds)
+
+        val task = setUpFullscreenTask()
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.BottomLeft)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun addMoveToDesktopChanges_positionTopRight() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        addFreeformTaskAtPosition(DesktopTaskPosition.BottomLeft, stableBounds)
+
+        val task = setUpFullscreenTask()
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.TopRight)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun addMoveToDesktopChanges_positionResetsToCenter() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        addFreeformTaskAtPosition(DesktopTaskPosition.TopRight, stableBounds)
+
+        val task = setUpFullscreenTask()
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.Center)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun addMoveToDesktopChanges_lastWindowSnapLeft_positionResetsToCenter() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        // Add freeform task with half display size snap bounds at left side.
+        setUpFreeformTask(
+            bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom)
+        )
+
+        val task = setUpFullscreenTask()
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.Center)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun addMoveToDesktopChanges_lastWindowSnapRight_positionResetsToCenter() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        // Add freeform task with half display size snap bounds at right side.
+        setUpFreeformTask(
+            bounds =
+                Rect(
+                    stableBounds.right - 500,
+                    stableBounds.top,
+                    stableBounds.right,
+                    stableBounds.bottom,
+                )
+        )
+
+        val task = setUpFullscreenTask()
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.Center)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun addMoveToDesktopChanges_lastWindowMaximised_positionResetsToCenter() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        // Add maximised freeform task.
+        setUpFreeformTask(bounds = Rect(stableBounds))
+
+        val task = setUpFullscreenTask()
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.Center)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+    fun addMoveToDesktopChanges_defaultToCenterIfFree() {
+        setUpLandscapeDisplay()
+        val stableBounds = Rect()
+        displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+        val minTouchTarget =
+            context.resources.getDimensionPixelSize(
+                R.dimen.freeform_required_visible_empty_space_in_header
+            )
+        addFreeformTaskAtPosition(
+            DesktopTaskPosition.Center,
+            stableBounds,
+            Rect(0, 0, 1600, 1200),
+            Point(0, minTouchTarget + 1),
+        )
+
+        val task = setUpFullscreenTask()
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        val finalBounds = findBoundsChange(wct, task)
+        assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+            .isEqualTo(DesktopTaskPosition.Center)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun addMoveToDesktopChanges_landscapeDevice_userFullscreenOverride_defaultPortraitBounds() {
+        setUpLandscapeDisplay()
+        val task = setUpFullscreenTask(enableUserFullscreenOverride = true)
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun addMoveToDesktopChanges_landscapeDevice_systemFullscreenOverride_defaultPortraitBounds() {
+        setUpLandscapeDisplay()
+        val task = setUpFullscreenTask(enableSystemFullscreenOverride = true)
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun addMoveToDesktopChanges_landscapeDevice_portraitResizableApp_aspectRatioOverridden() {
+        setUpLandscapeDisplay()
+        val task =
+            setUpFullscreenTask(
+                screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+                shouldLetterbox = true,
+                aspectRatioOverrideApplied = true,
+            )
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun addMoveToDesktopChanges_portraitDevice_userFullscreenOverride_defaultPortraitBounds() {
+        setUpPortraitDisplay()
+        val task = setUpFullscreenTask(enableUserFullscreenOverride = true)
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun addMoveToDesktopChanges_portraitDevice_systemFullscreenOverride_defaultPortraitBounds() {
+        setUpPortraitDisplay()
+        val task = setUpFullscreenTask(enableSystemFullscreenOverride = true)
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun addMoveToDesktopChanges_portraitDevice_landscapeResizableApp_aspectRatioOverridden() {
+        setUpPortraitDisplay()
+        val task =
+            setUpFullscreenTask(
+                screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
+                deviceOrientation = ORIENTATION_PORTRAIT,
+                shouldLetterbox = true,
+                aspectRatioOverrideApplied = true,
+            )
+        val wct = WindowContainerTransaction()
+        controller.addMoveToDesktopChanges(wct, task)
+
+        assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
+        val task = setUpFullscreenTask()
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+        controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+        val wct = getLatestEnterDesktopWct()
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+    }
+
+    @Test
+    fun moveRunningTaskToDesktop_tdaFreeform_windowingModeSetToUndefined() {
+        val task = setUpFullscreenTask()
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+        controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+        val wct = getLatestEnterDesktopWct()
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED)
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+    }
+
+    @Test
+    fun moveTaskToDesktop_nonExistentTask_doesNothing() {
+        controller.moveTaskToDesktop(999, transitionSource = UNKNOWN)
+        verifyEnterDesktopWCTNotExecuted()
+        verify(desktopModeEnterExitTransitionListener, times(0))
+            .onEnterDesktopModeTransitionStarted(anyInt())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun moveTaskToDesktop_desktopWallpaperDisabled_nonRunningTask_launchesInFreeform() {
+        val task = createTaskInfo(1)
+        whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+        whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
+
+        controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN)
+
+        with(getLatestEnterDesktopWct()) {
+            assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() {
+        val task = createTaskInfo(1)
+        whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+        whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
+
+        controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN)
+
+        with(getLatestEnterDesktopWct()) {
+            // Add desktop wallpaper activity
+            assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+            // Launch task
+            assertLaunchTaskAt(index = 1, task.taskId, WINDOWING_MODE_FREEFORM)
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop() {
+        val task =
+            setUpFullscreenTask().apply {
+                isActivityStackTransparent = true
+                isTopActivityNoDisplay = true
+                numActivities = 1
+            }
+
+        controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+        val wct = getLatestEnterDesktopWct()
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    fun moveRunningTaskToDesktop_topActivityTranslucentWithDisplay_doesNothing() {
+        val task =
+            setUpFullscreenTask().apply {
+                isActivityStackTransparent = true
+                isTopActivityNoDisplay = false
+                numActivities = 1
+            }
+
+        controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+        verifyEnterDesktopWCTNotExecuted()
+        verify(desktopModeEnterExitTransitionListener, times(0))
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    fun moveRunningTaskToDesktop_systemUIActivityWithDisplay_doesNothing() {
+        // Set task as systemUI package
+        val systemUIPackageName =
+            context.resources.getString(com.android.internal.R.string.config_systemUi)
+        val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+        val task =
+            setUpFullscreenTask().apply {
+                baseActivity = baseComponent
+                isTopActivityNoDisplay = false
+            }
+
+        controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+        verifyEnterDesktopWCTNotExecuted()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing() {
+        // Set task as systemUI package
+        val systemUIPackageName =
+            context.resources.getString(com.android.internal.R.string.config_systemUi)
+        val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+        val task =
+            setUpFullscreenTask().apply {
+                baseActivity = baseComponent
+                isTopActivityNoDisplay = true
+            }
+
+        controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+
+        val wct = getLatestEnterDesktopWct()
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() {
+        val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+        whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
+            .thenReturn(Binder())
+
+        val task = createTaskInfo(1)
+        whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+        whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
+        controller.moveTaskToDesktop(
+            taskId = task.taskId,
+            transitionSource = UNKNOWN,
+            remoteTransition = RemoteTransition(spy(TestRemoteTransition())),
+        )
+
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+        assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
+    }
+
+    @Test
+    fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() {
+        val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+        whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
+            .thenReturn(Binder())
+
+        controller.moveRunningTaskToDesktop(
+            task = setUpFullscreenTask(),
+            transitionSource = UNKNOWN,
+            remoteTransition = RemoteTransition(spy(TestRemoteTransition())),
+        )
+
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+        assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() {
+        val homeTask = setUpHomeTask()
+        val freeformTask = setUpFreeformTask()
+        val fullscreenTask = setUpFullscreenTask()
+        markTaskHidden(freeformTask)
+
+        controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN)
+
+        with(getLatestEnterDesktopWct()) {
+            // Operations should include home task, freeform task
+            assertThat(hierarchyOps).hasSize(3)
+            assertReorderSequence(homeTask, freeformTask, fullscreenTask)
+            assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
+                .isEqualTo(WINDOWING_MODE_FREEFORM)
+        }
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() {
+        val freeformTask = setUpFreeformTask()
+        val fullscreenTask = setUpFullscreenTask()
+        markTaskHidden(freeformTask)
+
+        controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN)
+
+        with(getLatestEnterDesktopWct()) {
+            // Operations should include wallpaper intent, freeform task, fullscreen task
+            assertThat(hierarchyOps).hasSize(3)
+            assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+            assertReorderAt(index = 1, freeformTask)
+            assertReorderAt(index = 2, fullscreenTask)
+            assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
+                .isEqualTo(WINDOWING_MODE_FREEFORM)
+        }
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+    }
+
+    @Test
+    fun moveRunningTaskToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() {
+        setUpHomeTask(displayId = DEFAULT_DISPLAY)
+        val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+        markTaskHidden(freeformTaskDefault)
+
+        val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY)
+        val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY)
+        markTaskHidden(freeformTaskSecond)
+
+        controller.moveRunningTaskToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN)
+
+        with(getLatestEnterDesktopWct()) {
+            // Check that hierarchy operations do not include tasks from second display
+            assertThat(hierarchyOps.map { it.container })
+                .doesNotContain(homeTaskSecond.token.asBinder())
+            assertThat(hierarchyOps.map { it.container })
+                .doesNotContain(freeformTaskSecond.token.asBinder())
+        }
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+    }
+
+    @Test
+    fun moveRunningTaskToDesktop_splitTaskExitsSplit() {
+        val task = setUpSplitScreenTask()
+        controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+        val wct = getLatestEnterDesktopWct()
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+        verify(splitScreenController)
+            .prepareExitSplitScreen(
+                any(),
+                anyInt(),
+                eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE),
+            )
+    }
+
+    @Test
+    fun moveRunningTaskToDesktop_fullscreenTaskDoesNotExitSplit() {
+        val task = setUpFullscreenTask()
+        controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+        val wct = getLatestEnterDesktopWct()
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+        verify(splitScreenController, never())
+            .prepareExitSplitScreen(
+                any(),
+                anyInt(),
+                eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE),
+            )
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun moveRunningTaskToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() {
+        val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+        val newTask = setUpFullscreenTask()
+        val homeTask = setUpHomeTask()
+
+        controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN)
+
+        val wct = getLatestEnterDesktopWct()
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+        assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 1) // visible tasks + home
+        wct.assertReorderAt(0, homeTask)
+        wct.assertReorderSequenceInRange(
+            range = 1..<(MAX_TASK_LIMIT + 1),
+            *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
+            newTask,
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() {
+        val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+        val newTask = setUpFullscreenTask()
+        val homeTask = setUpHomeTask()
+
+        controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN)
+
+        val wct = getLatestEnterDesktopWct()
+        verify(desktopModeEnterExitTransitionListener)
+            .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+        assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 2) // tasks + home + wallpaper
+        // Move home to front
+        wct.assertReorderAt(0, homeTask)
+        // Add desktop wallpaper activity
+        wct.assertPendingIntentAt(1, desktopWallpaperIntent)
+        // Bring freeform tasks to front
+        wct.assertReorderSequenceInRange(
+            range = 2..<(MAX_TASK_LIMIT + 2),
+            *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
+            newTask,
+        )
+    }
+
+    @Test
+    fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
+        val task = setUpFreeformTask()
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+        val wct = getLatestExitDesktopWct()
+        verify(desktopModeEnterExitTransitionListener, times(1))
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() {
+        val task = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+            .configuration
+            .windowConfiguration
+            .windowingMode = WINDOWING_MODE_FULLSCREEN
+
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+        assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+        // Removes wallpaper activity when leaving desktop
+        wct.assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() {
+        val task = setUpFreeformTask()
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+        val wct = getLatestExitDesktopWct()
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+    }
+
+    @Test
+    fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() {
+        val task = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+            .configuration
+            .windowConfiguration
+            .windowingMode = WINDOWING_MODE_FREEFORM
+
+        controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+        assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN)
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+        // Removes wallpaper activity when leaving desktop
+        wct.assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
+        val task1 = setUpFreeformTask()
+        // Setup task2
+        setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+            .configuration
+            .windowConfiguration
+            .windowingMode = WINDOWING_MODE_FULLSCREEN
+
+        controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
+        assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+        // Does not remove wallpaper activity, as desktop still has a visible desktop task
+        assertThat(wct.hierarchyOps).isEmpty()
+    }
+
+    @Test
+    fun moveToFullscreen_nonExistentTask_doesNothing() {
+        controller.moveToFullscreen(999, transitionSource = UNKNOWN)
+        verifyExitDesktopWCTNotExecuted()
+    }
+
+    @Test
+    fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() {
+        val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY)
+        controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN)
+
+        with(getLatestExitDesktopWct()) {
+            assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder())
+            assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder())
+        }
+        verify(desktopModeEnterExitTransitionListener)
+            .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+    }
+
+    @Test
+    fun moveTaskToFront_postsWctWithReorderOp() {
+        val task1 = setUpFreeformTask()
+        setUpFreeformTask()
+
+        controller.moveTaskToFront(task1, remoteTransition = null)
+
+        val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
+        assertThat(wct.hierarchyOps).hasSize(1)
+        wct.assertReorderAt(index = 0, task1)
+    }
+
+    @Test
+    fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() {
+        setUpHomeTask()
+        val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() }
+        whenever(
+                desktopMixedTransitionHandler.startLaunchTransition(
+                    eq(TRANSIT_TO_FRONT),
+                    any(),
+                    eq(freeformTasks[0].taskId),
+                    anyOrNull(),
+                    anyOrNull(),
+                )
+            )
+            .thenReturn(Binder())
+
+        controller.moveTaskToFront(freeformTasks[0], remoteTransition = null)
+
+        val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
+        assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize
+        wct.assertReorderAt(0, freeformTasks[0], toTop = true)
+        wct.assertReorderAt(1, freeformTasks[1], toTop = false)
+    }
+
+    @Test
+    fun moveTaskToFront_remoteTransition_usesOneshotHandler() {
+        setUpHomeTask()
+        val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() }
+        val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+        whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
+            .thenReturn(Binder())
+
+        controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition()))
+
+        assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
+    }
+
+    @Test
+    fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() {
+        setUpHomeTask()
+        val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() }
+        val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+        whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
+            .thenReturn(Binder())
+
+        controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition()))
+
+        assertThat(transitionHandlerArgCaptor.value)
+            .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java)
+    }
+
+    @Test
+    fun moveTaskToFront_backgroundTask_launchesTask() {
+        val task = createTaskInfo(1)
+        whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+
+        controller.moveTaskToFront(task.taskId, remoteTransition = null)
+
+        val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
+        assertThat(wct.hierarchyOps).hasSize(1)
+        wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
+    }
+
+    @Test
+    fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() {
+        val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+        val task = createTaskInfo(1001)
+        whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+        whenever(
+                desktopMixedTransitionHandler.startLaunchTransition(
+                    eq(TRANSIT_OPEN),
+                    any(),
+                    eq(task.taskId),
+                    anyOrNull(),
+                    anyOrNull(),
+                )
+            )
+            .thenReturn(Binder())
+
+        controller.moveTaskToFront(task.taskId, remoteTransition = null)
+
+        val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
+        assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize
+        wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
+        wct.assertReorderAt(1, freeformTasks[0], toTop = false)
+    }
+
+    @Test
+    fun moveToNextDisplay_noOtherDisplays() {
+        whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
+        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        controller.moveToNextDisplay(task.taskId)
+        verifyWCTNotExecuted()
+    }
+
+    @Test
+    fun moveToNextDisplay_moveFromFirstToSecondDisplay() {
+        // Set up two display ids
+        whenever(rootTaskDisplayAreaOrganizer.displayIds)
+            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+        // Create a mock for the target display area: second display
+        val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
+        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
+            .thenReturn(secondDisplayArea)
+
+        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        controller.moveToNextDisplay(task.taskId)
+        with(getLatestWct(type = TRANSIT_CHANGE)) {
+            assertThat(hierarchyOps).hasSize(1)
+            assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
+            assertThat(hierarchyOps[0].isReparent).isTrue()
+            assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder())
+            assertThat(hierarchyOps[0].toTop).isTrue()
+        }
+    }
+
+    @Test
+    fun moveToNextDisplay_moveFromSecondToFirstDisplay() {
+        // Set up two display ids
+        whenever(rootTaskDisplayAreaOrganizer.displayIds)
+            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+        // Create a mock for the target display area: default display
+        val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+            .thenReturn(defaultDisplayArea)
+
+        val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
+        controller.moveToNextDisplay(task.taskId)
+
+        with(getLatestWct(type = TRANSIT_CHANGE)) {
+            assertThat(hierarchyOps).hasSize(1)
+            assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
+            assertThat(hierarchyOps[0].isReparent).isTrue()
+            assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder())
+            assertThat(hierarchyOps[0].toTop).isTrue()
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY)
+    fun moveToNextDisplay_removeWallpaper() {
+        // Set up two display ids
+        whenever(rootTaskDisplayAreaOrganizer.displayIds)
+            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+        // Create a mock for the target display area: second display
+        val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
+        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
+            .thenReturn(secondDisplayArea)
+        // Add a task and a wallpaper
+        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+
+        controller.moveToNextDisplay(task.taskId)
+
+        with(getLatestWct(type = TRANSIT_CHANGE)) {
+            val wallpaperChange =
+                hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() }
+            assertThat(wallpaperChange).isNotNull()
+            assertThat(wallpaperChange!!.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+        }
+    }
+
+    @Test
+    fun getTaskWindowingMode() {
+        val fullscreenTask = setUpFullscreenTask()
+        val freeformTask = setUpFreeformTask()
+
+        assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId))
+            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+        assertThat(controller.getTaskWindowingMode(freeformTask.taskId))
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+        assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    fun onDesktopWindowClose_noActiveTasks() {
+        val task = setUpFreeformTask(active = false)
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
+        // Doesn't modify transaction
+        assertThat(wct.hierarchyOps).isEmpty()
+    }
+
+    @Test
+    fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() {
+        val task = setUpFreeformTask()
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
+        // Doesn't modify transaction
+        assertThat(wct.hierarchyOps).isEmpty()
+    }
+
+    @Test
+    fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() {
+        val task = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
+        // Adds remove wallpaper operation
+        wct.assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    fun onDesktopWindowClose_singleActiveTask_isClosing() {
+        val task = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId)
+
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
+        // Doesn't modify transaction
+        assertThat(wct.hierarchyOps).isEmpty()
+    }
+
+    @Test
+    fun onDesktopWindowClose_singleActiveTask_isMinimized() {
+        val task = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
+
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
+        // Doesn't modify transaction
+        assertThat(wct.hierarchyOps).isEmpty()
+    }
+
+    @Test
+    fun onDesktopWindowClose_multipleActiveTasks() {
+        val task1 = setUpFreeformTask()
+        setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1)
+        // Doesn't modify transaction
+        assertThat(wct.hierarchyOps).isEmpty()
+    }
+
+    @Test
+    fun onDesktopWindowClose_multipleActiveTasks_isOnlyNonClosingTask() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId)
+
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1)
+        // Adds remove wallpaper operation
+        wct.assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    fun onDesktopWindowClose_multipleActiveTasks_hasMinimized() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
+
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1)
+        // Adds remove wallpaper operation
+        wct.assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() {
+        val task = setUpFreeformTask(active = false)
+        val transition = Binder()
+        whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+            .thenReturn(transition)
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+
+        controller.minimizeTask(task)
+
+        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+        captor.value.hierarchyOps.none { hop ->
+            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+        }
+    }
+
+    @Test
+    fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() {
+        val task = setUpPipTask(autoEnterEnabled = true)
+        val handler = mock(TransitionHandler::class.java)
+        whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder())
+        whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+            .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+        controller.minimizeTask(task)
+
+        verify(freeformTaskTransitionStarter).startPipTransition(any())
+        verify(freeformTaskTransitionStarter, never()).startMinimizedModeTransition(any())
+    }
+
+    @Test
+    fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() {
+        val task = setUpPipTask(autoEnterEnabled = false)
+        whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+            .thenReturn(Binder())
+
+        controller.minimizeTask(task)
+
+        verify(freeformTaskTransitionStarter).startMinimizedModeTransition(any())
+        verify(freeformTaskTransitionStarter, never()).startPipTransition(any())
+    }
+
+    @Test
+    fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
+        val task = setUpFreeformTask(active = true)
+        val transition = Binder()
+        whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+            .thenReturn(transition)
+
+        controller.minimizeTask(task)
+
+        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+        captor.value.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK }
+    }
+
+    @Test
+    fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() {
+        val task = setUpFreeformTask()
+        val transition = Binder()
+        whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+            .thenReturn(transition)
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+
+        // The only active task is being minimized.
+        controller.minimizeTask(task)
+
+        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+        // Adds remove wallpaper operation
+        captor.value.assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() {
+        val task = setUpFreeformTask()
+        val transition = Binder()
+        whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+            .thenReturn(transition)
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
+
+        // The only active task is already minimized.
+        controller.minimizeTask(task)
+
+        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+        captor.value.hierarchyOps.none { hop ->
+            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+        }
+    }
+
+    @Test
+    fun onDesktopWindowMinimize_multipleActiveTasks_doesntRemoveWallpaper() {
+        val task1 = setUpFreeformTask(active = true)
+        setUpFreeformTask(active = true)
+        val transition = Binder()
+        whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+            .thenReturn(transition)
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+
+        controller.minimizeTask(task1)
+
+        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+        captor.value.hierarchyOps.none { hop ->
+            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+        }
+    }
+
+    @Test
+    fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() {
+        val task1 = setUpFreeformTask(active = true)
+        val task2 = setUpFreeformTask(active = true)
+        val transition = Binder()
+        whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+            .thenReturn(transition)
+        val wallpaperToken = MockToken().token()
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
+
+        // task1 is the only visible task as task2 is minimized.
+        controller.minimizeTask(task1)
+        // Adds remove wallpaper operation
+        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+        // Adds remove wallpaper operation
+        captor.value.assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    fun onDesktopWindowMinimize_triesToExitImmersive() {
+        val task = setUpFreeformTask()
+        val transition = Binder()
+        whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+            .thenReturn(transition)
+
+        controller.minimizeTask(task)
+
+        verify(mMockDesktopImmersiveController).exitImmersiveIfApplicable(any(), eq(task), any())
+    }
+
+    @Test
+    fun onDesktopWindowMinimize_invokesImmersiveTransitionStartCallback() {
+        val task = setUpFreeformTask()
+        val transition = Binder()
+        val runOnTransit = RunOnStartTransitionCallback()
+        whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+            .thenReturn(transition)
+        whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any()))
+            .thenReturn(
+                ExitResult.Exit(exitingTask = task.taskId, runOnTransitionStart = runOnTransit)
+            )
+
+        controller.minimizeTask(task)
+
+        assertThat(runOnTransit.invocations).isEqualTo(1)
+        assertThat(runOnTransit.lastInvoked).isEqualTo(transition)
+    }
+
+    @Test
+    fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
+        val homeTask = setUpHomeTask()
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+        val fullscreenTask = createFullscreenTask()
+
+        val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+        assertNotNull(wct, "should handle request")
+        assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+
+        assertThat(wct.hierarchyOps).hasSize(1)
+    }
+
+    @Test
+    fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() {
+        val homeTask = setUpHomeTask()
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+        val fullscreenTask = createFullscreenTask()
+        fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+
+        val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+        assertNotNull(wct, "should handle request")
+        assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+
+        // There are 5 hops that are happening in this case:
+        // 1. Moving the fullscreen task to top as we add moveToDesktop() changes
+        // 2. Bringing home task to front
+        // 3. Pending intent for the wallpaper
+        // 4. Bringing the existing freeform task to top
+        // 5. Bringing the fullscreen task back at the top
+        assertThat(wct.hierarchyOps).hasSize(5)
+        wct.assertReorderAt(1, homeTask, toTop = true)
+        wct.assertReorderAt(4, fullscreenTask, toTop = true)
+    }
+
+    @Test
+    fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() {
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+        val fullscreenTask = createFullscreenTask()
+
+        val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+        // Make sure we only reorder the new task to top (we don't reorder the old task to bottom)
+        assertThat(wct?.hierarchyOps?.size).isEqualTo(1)
+        wct!!.assertReorderAt(0, fullscreenTask, toTop = true)
+    }
+
+    @Test
+    fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() {
+        val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+        freeformTasks.forEach { markTaskVisible(it) }
+        val fullscreenTask = createFullscreenTask()
+
+        val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+        // Make sure we reorder the new task to top, and the back task to the bottom
+        assertThat(wct!!.hierarchyOps.size).isEqualTo(2)
+        wct.assertReorderAt(0, fullscreenTask, toTop = true)
+        wct.assertReorderAt(1, freeformTasks[0], toTop = false)
+    }
+
+    @Test
+    fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() {
+        val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+        freeformTasks.forEach { markTaskVisible(it) }
+        val fullscreenTask = createFullscreenTask()
+        fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+
+        val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+        // Make sure we reorder the new task to top, and the back task to the bottom
+        assertThat(wct!!.hierarchyOps.size).isEqualTo(9)
+        wct.assertReorderAt(0, fullscreenTask, toTop = true)
+        wct.assertReorderAt(8, freeformTasks[0], toTop = false)
+    }
+
+    @Test
+    fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() {
+        val minimizedTask = setUpFreeformTask()
+        taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId)
+        val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+        freeformTasks.forEach { markTaskVisible(it) }
+        val homeTask = setUpHomeTask()
+        val fullscreenTask = createFullscreenTask()
+        fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+
+        val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+        assertThat(wct!!.hierarchyOps.size).isEqualTo(10)
+        wct.assertReorderAt(0, fullscreenTask, toTop = true)
+        // Make sure we reorder the home task to the top, desktop tasks to top of them and minimized
+        // task is under the home task.
+        wct.assertReorderAt(1, homeTask, toTop = true)
+        wct.assertReorderAt(9, freeformTasks[0], toTop = false)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() {
+        whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+
+        val fullscreenTask = createFullscreenTask()
+        val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+        assertNotNull(wct, "should handle request")
+        assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        // There are 3 hops that are happening in this case:
+        // 1. Moving the fullscreen task to top as we add moveToDesktop() changes
+        // 2. Pending intent for the wallpaper
+        // 3. Bringing the fullscreen task back at the top
+        wct.assertPendingIntentAt(1, desktopWallpaperIntent)
+        wct.assertReorderAt(2, fullscreenTask, toTop = true)
+    }
+
+    @Test
+    fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() {
+        whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+
+        val fullscreenTask = createFullscreenTask()
+        val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+        assertThat(wct).isNull()
+    }
+
+    @Test
+    fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
+        val freeformTask = setUpFreeformTask()
+        markTaskHidden(freeformTask)
+        val fullscreenTask = createFullscreenTask()
+        assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+    }
+
+    @Test
+    fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
+        val fullscreenTask = createFullscreenTask()
+        assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+    }
+
+    @Test
+    fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() {
+        val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
+        createFreeformTask(displayId = SECOND_DISPLAY)
+
+        val result =
+            controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay))
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() {
+        val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+        freeformTasks.forEach { markTaskVisible(it) }
+        val newFreeformTask = createFreeformTask()
+
+        val wct =
+            controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN))
+
+        assertThat(wct?.hierarchyOps?.size).isEqualTo(1)
+        wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom
+    }
+
+    @Test
+    fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() {
+        val freeformTask = setUpFreeformTask()
+        markTaskHidden(freeformTask)
+
+        val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+        // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA.
+        assertNotNull(wct, "should handle request")
+        assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() {
+        whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+
+        val freeformTask = setUpFreeformTask()
+        markTaskHidden(freeformTask)
+        val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+        assertNotNull(wct, "should handle request")
+        assertFalse(wct.anyWindowingModeChange(freeformTask.token))
+    }
+
+    @Test
+    fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() {
+        whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+
+        val freeformTask = setUpFreeformTask()
+        markTaskHidden(freeformTask)
+        val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+        assertNotNull(wct, "should handle request")
+        assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
+        val freeformTask1 = setUpFreeformTask()
+        val freeformTask2 = createFreeformTask()
+
+        markTaskHidden(freeformTask1)
+        val result =
+            controller.handleRequest(
+                Binder(),
+                createTransition(freeformTask2, type = TRANSIT_TO_FRONT),
+            )
+
+        assertNotNull(result, "Should handle request")
+        assertThat(result.hierarchyOps?.size).isEqualTo(2)
+        result.assertReorderAt(1, freeformTask2, toTop = true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
+        val freeformTask1 = setUpFreeformTask()
+        val freeformTask2 = createFreeformTask()
+
+        markTaskHidden(freeformTask1)
+        val result =
+            controller.handleRequest(
+                Binder(),
+                createTransition(freeformTask2, type = TRANSIT_TO_FRONT),
+            )
+
+        assertNotNull(result, "Should handle request")
+        assertThat(result.hierarchyOps?.size).isEqualTo(3)
+        // Add desktop wallpaper activity
+        result.assertPendingIntentAt(0, desktopWallpaperIntent)
+        // Bring active desktop tasks to front
+        result.assertReorderAt(1, freeformTask1, toTop = true)
+        // Bring new task to front
+        result.assertReorderAt(2, freeformTask2, toTop = true)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() {
+        val task = createFreeformTask()
+        val result = controller.handleRequest(Binder(), createTransition(task))
+
+        assertNotNull(result, "Should handle request")
+        assertThat(result.hierarchyOps?.size).isEqualTo(1)
+        result.assertReorderAt(0, task, toTop = true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
+        val task = createFreeformTask()
+        val result = controller.handleRequest(Binder(), createTransition(task))
+
+        assertNotNull(result, "Should handle request")
+        assertThat(result.hierarchyOps?.size).isEqualTo(2)
+        // Add desktop wallpaper activity
+        result.assertPendingIntentAt(0, desktopWallpaperIntent)
+        // Bring new task to front
+        result.assertReorderAt(1, task, toTop = true)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() {
+        val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
+        // Second display task
+        createFreeformTask(displayId = SECOND_DISPLAY)
+
+        val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
+
+        assertNotNull(result, "Should handle request")
+        assertThat(result.hierarchyOps?.size).isEqualTo(1)
+        result.assertReorderAt(0, taskDefaultDisplay, toTop = true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
+        val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
+        // Second display task
+        createFreeformTask(displayId = SECOND_DISPLAY)
+
+        val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
+
+        assertNotNull(result, "Should handle request")
+        assertThat(result.hierarchyOps?.size).isEqualTo(2)
+        // Add desktop wallpaper activity
+        result.assertPendingIntentAt(0, desktopWallpaperIntent)
+        // Bring new task to front
+        result.assertReorderAt(1, taskDefaultDisplay, toTop = true)
+    }
+
+    @Test
+    fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() {
+        whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false)
+
+        val freeformTask1 = setUpFreeformTask()
+        markTaskVisible(freeformTask1)
+
+        val freeformTask2 = createFreeformTask()
+        val result =
+            controller.handleRequest(
+                freeformTask2.token.asBinder(),
+                createTransition(freeformTask2),
+            )
+        assertFalse(result.anyDensityConfigChange(freeformTask2.token))
+    }
+
+    @Test
+    fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() {
+        whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true)
+
+        val freeformTask1 = setUpFreeformTask()
+        markTaskVisible(freeformTask1)
+
+        val freeformTask2 = createFreeformTask()
+        val result =
+            controller.handleRequest(
+                freeformTask2.token.asBinder(),
+                createTransition(freeformTask2),
+            )
+        assertTrue(result.anyDensityConfigChange(freeformTask2.token))
+    }
+
+    @Test
+    fun handleRequest_freeformTask_keyguardLocked_returnNull() {
+        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() {
+        val task =
+            TestRunningTaskInfoBuilder()
+                .setActivityType(ACTIVITY_TYPE_STANDARD)
+                .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+                .build()
+        val transition = createTransition(task = task, type = TRANSIT_CLOSE)
+        val result = controller.handleRequest(Binder(), transition)
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun handleRequest_noTriggerTask_returnNull() {
+        assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull()
+    }
+
+    @Test
+    fun handleRequest_triggerTaskNotStandard_returnNull() {
+        val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
+        assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
+    }
+
+    @Test
+    fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() {
+        val task =
+            TestRunningTaskInfoBuilder()
+                .setActivityType(ACTIVITY_TYPE_STANDARD)
+                .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+                .build()
+        assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
+    }
+
+    @Test
+    fun handleRequest_recentsAnimationRunning_returnNull() {
+        // Set up a visible freeform task so a fullscreen task should be converted to freeform
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+
+        // Mark recents animation running
+        recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING)
+
+        // Open a fullscreen task, check that it does not result in a WCT with changes to it
+        val fullscreenTask = createFullscreenTask()
+        assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+    }
+
+    @Test
+    fun handleRequest_recentsAnimationRunning_relaunchActiveTask_taskBecomesUndefined() {
+        // Set up a visible freeform task
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+
+        // Mark recents animation running
+        recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING)
+
+        // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA.
+        val result = controller.handleRequest(Binder(), createTransition(freeformTask))
+        assertThat(result?.changes?.get(freeformTask.token.asBinder())?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    fun handleRequest_topActivityTransparentWithoutDisplay_returnSwitchToFreeformWCT() {
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+
+        val task =
+            setUpFullscreenTask().apply {
+                isActivityStackTransparent = true
+                isTopActivityNoDisplay = true
+                numActivities = 1
+            }
+
+        val result = controller.handleRequest(Binder(), createTransition(task))
+        assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    fun handleRequest_topActivityTransparentWithDisplay_returnSwitchToFullscreenWCT() {
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+
+        val task =
+            setUpFreeformTask().apply {
+                isActivityStackTransparent = true
+                isTopActivityNoDisplay = false
+                numActivities = 1
+            }
+
+        val result = controller.handleRequest(Binder(), createTransition(task))
+        assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT() {
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+
+        // Set task as systemUI package
+        val systemUIPackageName =
+            context.resources.getString(com.android.internal.R.string.config_systemUi)
+        val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+        val task =
+            setUpFreeformTask().apply {
+                baseActivity = baseComponent
+                isTopActivityNoDisplay = false
+            }
+
+        val result = controller.handleRequest(Binder(), createTransition(task))
+        assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    fun handleRequest_systemUIActivityWithoutDisplay_returnSwitchToFreeformWCT() {
+        val freeformTask = setUpFreeformTask()
+        markTaskVisible(freeformTask)
+
+        // Set task as systemUI package
+        val systemUIPackageName =
+            context.resources.getString(com.android.internal.R.string.config_systemUi)
+        val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+        val task =
+            setUpFullscreenTask().apply {
+                baseActivity = baseComponent
+                isTopActivityNoDisplay = true
+            }
+
+        val result = controller.handleRequest(Binder(), createTransition(task))
+        assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
+        val task = setUpFreeformTask()
+
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() {
+        val task = setUpFreeformTask()
+
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_notInDesktop_doesNotHandle() {
+        val task = setUpFreeformTask()
+        markTaskHidden(task)
+
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_singleTaskNoToken_doesNotHandle() {
+        val task = setUpFreeformTask()
+
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
+        val task = setUpFreeformTask()
+
+        taskRepository.wallpaperActivityToken = MockToken().token()
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() {
+        val task = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+        // Should create remove wallpaper transaction
+        assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_multipleTasks_noWallpaper_doesNotHandle() {
+        val task1 = setUpFreeformTask()
+        setUpFreeformTask()
+
+        taskRepository.wallpaperActivityToken = MockToken().token()
+        val result =
+            controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_multipleTasks_doesNotHandle() {
+        val task1 = setUpFreeformTask()
+        setUpFreeformTask()
+
+        taskRepository.wallpaperActivityToken = MockToken().token()
+        val result =
+            controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
     )
-    whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
-      (i.arguments.first() as Rect).set(stableBounds)
-    }
-  }
+    fun handleRequest_backTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() {
+        val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val wallpaperToken = MockToken().token()
 
-  private fun setUpPortraitDisplay() {
-    whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
-    whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
-    val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_SHORT,
-      DISPLAY_DIMENSION_LONG - Companion.TASKBAR_FRAME_HEIGHT
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+        val result =
+            controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+        // Should create remove wallpaper transaction
+        assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() {
+        val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+        val result =
+            controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+        // Should create remove wallpaper transaction
+        assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_removesWallpaper() {
+        val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+        // Task is being minimized so mark it as not visible.
+        taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false)
+        val result =
+            controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
+        val task = setUpFreeformTask()
+
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_closeTransition_singleTaskNoToken_doesNotHandle() {
+        val task = setUpFreeformTask()
+
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
+        val task = setUpFreeformTask()
+
+        taskRepository.wallpaperActivityToken = MockToken().token()
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() {
+        val task = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        val result =
+            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+        // Should create remove wallpaper transaction
+        assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() {
+        val task1 = setUpFreeformTask()
+        setUpFreeformTask()
+
+        taskRepository.wallpaperActivityToken = MockToken().token()
+        val result =
+            controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_closeTransition_multipleTasksFlagEnabled_doesNotHandle() {
+        val task1 = setUpFreeformTask()
+        setUpFreeformTask()
+
+        taskRepository.wallpaperActivityToken = MockToken().token()
+        val result =
+            controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() {
+        val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+        val result =
+            controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+        // Should create remove wallpaper transaction
+        assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() {
+        val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+        val result =
+            controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+        // Should create remove wallpaper transaction
+        assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    fun handleRequest_closeTransition_minimizadTask_withWallpaper_removesWallpaper() {
+        val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+        val wallpaperToken = MockToken().token()
+
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+        // Task is being minimized so mark it as not visible.
+        taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false)
+        val result =
+            controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK))
+
+        assertNull(result, "Should not handle request")
+    }
+
+    @Test
+    fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() {
+        val task1 = setUpFullscreenTask()
+        val task2 = setUpFullscreenTask()
+        val task3 = setUpFullscreenTask()
+
+        task1.isFocused = true
+        task2.isFocused = false
+        task3.isFocused = false
+
+        controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+        val wct = getLatestEnterDesktopWct()
+        assertThat(wct.changes[task1.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+    }
+
+    @Test
+    fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() {
+        val task1 = setUpSplitScreenTask()
+        val task2 = setUpFullscreenTask()
+        val task3 = setUpFullscreenTask()
+        val task4 = setUpSplitScreenTask()
+
+        task1.isFocused = true
+        task2.isFocused = false
+        task3.isFocused = false
+        task4.isFocused = true
+
+        task4.parentTaskId = task1.taskId
+
+        controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+        val wct = getLatestEnterDesktopWct()
+        assertThat(wct.changes[task4.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+        verify(splitScreenController)
+            .prepareExitSplitScreen(
+                any(),
+                anyInt(),
+                eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE),
+            )
+    }
+
+    @Test
+    fun moveFocusedTaskToFullscreen() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+
+        task1.isFocused = false
+        task2.isFocused = true
+        task3.isFocused = false
+
+        controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        assertThat(wct.changes[task2.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+    }
+
+    @Test
+    fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+
+        task1.isFocused = false
+        task2.isFocused = true
+        task3.isFocused = false
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
+        taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false)
+
+        controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+        assertThat(taskChange.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+        wct.assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+
+        task1.isFocused = false
+        task2.isFocused = true
+        task3.isFocused = false
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+        assertThat(taskChange.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+        // Does not remove wallpaper activity, as desktop still has visible desktop tasks
+        assertThat(wct.hierarchyOps).isEmpty()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun removeDesktop_multipleTasks_removesAll() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
+
+        controller.removeDesktop(displayId = DEFAULT_DISPLAY)
+
+        val wct = getLatestWct(TRANSIT_CLOSE)
+        assertThat(wct.hierarchyOps).hasSize(3)
+        wct.assertRemoveAt(index = 0, task1.token)
+        wct.assertRemoveAt(index = 1, task2.token)
+        wct.assertRemoveAt(index = 2, task3.token)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun removeDesktop_multipleTasksWithBackgroundTask_removesAll() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
+        whenever(shellTaskOrganizer.getRunningTaskInfo(task3.taskId)).thenReturn(null)
+
+        controller.removeDesktop(displayId = DEFAULT_DISPLAY)
+
+        val wct = getLatestWct(TRANSIT_CLOSE)
+        assertThat(wct.hierarchyOps).hasSize(2)
+        wct.assertRemoveAt(index = 0, task1.token)
+        wct.assertRemoveAt(index = 1, task2.token)
+        verify(recentTasksController).removeBackgroundTask(task3.taskId)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task = setUpFullscreenTask()
+        setUpLandscapeDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+        setUpLandscapeDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task =
+            setUpFullscreenTask(
+                screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+                shouldLetterbox = true,
+            )
+        setUpLandscapeDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task =
+            setUpFullscreenTask(
+                isResizable = false,
+                screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
+            )
+        setUpLandscapeDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task =
+            setUpFullscreenTask(
+                isResizable = false,
+                screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+                shouldLetterbox = true,
+            )
+        setUpLandscapeDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
+        setUpPortraitDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task =
+            setUpFullscreenTask(
+                deviceOrientation = ORIENTATION_PORTRAIT,
+                screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+            )
+        setUpPortraitDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task =
+            setUpFullscreenTask(
+                deviceOrientation = ORIENTATION_PORTRAIT,
+                screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
+                shouldLetterbox = true,
+            )
+        setUpPortraitDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task =
+            setUpFullscreenTask(
+                isResizable = false,
+                deviceOrientation = ORIENTATION_PORTRAIT,
+                screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+            )
+        setUpPortraitDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task =
+            setUpFullscreenTask(
+                isResizable = false,
+                deviceOrientation = ORIENTATION_PORTRAIT,
+                screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
+                shouldLetterbox = true,
+            )
+        setUpPortraitDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() {
+        val task = setUpFreeformTask()
+        val spyController = spy(controller)
+        val mockSurface = mock(SurfaceControl::class.java)
+        val mockDisplayLayout = mock(DisplayLayout::class.java)
+        whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+        whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+        spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, -100, 500, 1000))
+
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
+        spyController.onDragPositioningEnd(
+            task,
+            mockSurface,
+            Point(100, -100), /* position */
+            PointF(200f, -200f), /* inputCoordinate */
+            Rect(100, -100, 500, 1000), /* currentDragBounds */
+            Rect(0, 50, 2000, 2000), /* validDragArea */
+            Rect() /* dragStartBounds */,
+            motionEvent,
+            desktopWindowDecoration,
+        )
+        val rectAfterEnd = Rect(100, 50, 500, 1150)
+        verify(transitions)
+            .startTransition(
+                eq(TRANSIT_CHANGE),
+                Mockito.argThat { wct ->
+                    return@argThat wct.changes.any { (token, change) ->
+                        change.configuration.windowConfiguration.bounds == rectAfterEnd
+                    }
+                },
+                eq(null),
+            )
+    }
+
+    @Test
+    fun onDesktopDragEnd_noIndicator_updatesTaskBounds() {
+        val task = setUpFreeformTask()
+        val spyController = spy(controller)
+        val mockSurface = mock(SurfaceControl::class.java)
+        val mockDisplayLayout = mock(DisplayLayout::class.java)
+        whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+        whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+        spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
+
+        val currentDragBounds = Rect(100, 200, 500, 1000)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
+
+        spyController.onDragPositioningEnd(
+            task,
+            mockSurface,
+            Point(100, 200), /* position */
+            PointF(200f, 300f), /* inputCoordinate */
+            currentDragBounds, /* currentDragBounds */
+            Rect(0, 50, 2000, 2000) /* validDragArea */,
+            Rect() /* dragStartBounds */,
+            motionEvent,
+            desktopWindowDecoration,
+        )
+
+        verify(transitions)
+            .startTransition(
+                eq(TRANSIT_CHANGE),
+                Mockito.argThat { wct ->
+                    return@argThat wct.changes.any { (token, change) ->
+                        change.configuration.windowConfiguration.bounds == currentDragBounds
+                    }
+                },
+                eq(null),
+            )
+    }
+
+    @Test
+    fun onDesktopDragEnd_fullscreenIndicator_dragToExitDesktop() {
+        val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100))
+        val spyController = spy(controller)
+        val mockSurface = mock(SurfaceControl::class.java)
+        val mockDisplayLayout = mock(DisplayLayout::class.java)
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+        whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+        whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+        whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+        }
+        whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(false)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
+
+        // Drag move the task to the top edge
+        spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
+        spyController.onDragPositioningEnd(
+            task,
+            mockSurface,
+            Point(100, 50), /* position */
+            PointF(200f, 300f), /* inputCoordinate */
+            Rect(100, 50, 500, 1000), /* currentDragBounds */
+            Rect(0, 50, 2000, 2000) /* validDragArea */,
+            Rect() /* dragStartBounds */,
+            motionEvent,
+            desktopWindowDecoration,
+        )
+
+        // Assert the task exits desktop mode
+        val wct = getLatestExitDesktopWct()
+        assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize() {
+        val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100))
+        val spyController = spy(controller)
+        val mockSurface = mock(SurfaceControl::class.java)
+        val mockDisplayLayout = mock(DisplayLayout::class.java)
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+        whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+        whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+        whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+        }
+        whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
+
+        // Drag move the task to the top edge
+        val currentDragBounds = Rect(100, 50, 500, 1000)
+        spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
+        spyController.onDragPositioningEnd(
+            task,
+            mockSurface,
+            Point(100, 50), /* position */
+            PointF(200f, 300f), /* inputCoordinate */
+            currentDragBounds,
+            Rect(0, 50, 2000, 2000) /* validDragArea */,
+            Rect() /* dragStartBounds */,
+            motionEvent,
+            desktopWindowDecoration,
+        )
+
+        // Assert bounds set to stable bounds
+        val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
+        assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
+        // Assert event is properly logged
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingStarted(
+                ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER,
+                InputMethod.UNKNOWN_INPUT_METHOD,
+                task,
+                task.configuration.windowConfiguration.bounds.width(),
+                task.configuration.windowConfiguration.bounds.height(),
+                displayController,
+            )
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER,
+                InputMethod.UNKNOWN_INPUT_METHOD,
+                task,
+                STABLE_BOUNDS.width(),
+                STABLE_BOUNDS.height(),
+                displayController,
+            )
+    }
+
+    @Test
+    fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize_noBoundsChange() {
+        val task = setUpFreeformTask(bounds = STABLE_BOUNDS)
+        val spyController = spy(controller)
+        val mockSurface = mock(SurfaceControl::class.java)
+        val mockDisplayLayout = mock(DisplayLayout::class.java)
+        val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+        tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+        whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+        whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+        whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+        }
+        whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+            .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
+
+        // Drag move the task to the top edge
+        val currentDragBounds = Rect(100, 50, 500, 1000)
+        spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
+        spyController.onDragPositioningEnd(
+            task,
+            mockSurface,
+            Point(100, 50), /* position */
+            PointF(200f, 300f), /* inputCoordinate */
+            currentDragBounds, /* currentDragBounds */
+            Rect(0, 50, 2000, 2000) /* validDragArea */,
+            Rect() /* dragStartBounds */,
+            motionEvent,
+            desktopWindowDecoration,
+        )
+
+        // Assert that task is NOT updated via WCT
+        verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
+        // Assert that task leash is updated via Surface Animations
+        verify(mReturnToDragStartAnimator)
+            .start(
+                eq(task.taskId),
+                eq(mockSurface),
+                eq(currentDragBounds),
+                eq(STABLE_BOUNDS),
+                anyOrNull(),
+            )
+        // Assert no event is logged
+        verify(desktopModeEventLogger, never())
+            .logTaskResizingStarted(any(), any(), any(), any(), any(), any(), any())
+        verify(desktopModeEventLogger, never())
+            .logTaskResizingEnded(any(), any(), any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun enterSplit_freeformTaskIsMovedToSplit() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+
+        task1.isFocused = false
+        task2.isFocused = true
+        task3.isFocused = false
+
+        controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
+
+        verify(splitScreenController)
+            .requestEnterSplitSelect(
+                eq(task2),
+                any(),
+                eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
+                eq(task2.configuration.windowConfiguration.bounds),
+            )
+    }
+
+    @Test
+    fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+
+        task1.isFocused = false
+        task2.isFocused = true
+        task3.isFocused = false
+        taskRepository.wallpaperActivityToken = wallpaperToken
+        taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
+        taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false)
+
+        controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
+
+        val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(splitScreenController)
+            .requestEnterSplitSelect(
+                eq(task2),
+                wctArgument.capture(),
+                eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
+                eq(task2.configuration.windowConfiguration.bounds),
+            )
+        // Removes wallpaper activity when leaving desktop
+        wctArgument.value.assertRemoveAt(index = 0, wallpaperToken)
+    }
+
+    @Test
+    fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() {
+        val task1 = setUpFreeformTask()
+        val task2 = setUpFreeformTask()
+        val task3 = setUpFreeformTask()
+        val wallpaperToken = MockToken().token()
+
+        task1.isFocused = false
+        task2.isFocused = true
+        task3.isFocused = false
+        taskRepository.wallpaperActivityToken = wallpaperToken
+
+        controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
+
+        val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(splitScreenController)
+            .requestEnterSplitSelect(
+                eq(task2),
+                wctArgument.capture(),
+                eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
+                eq(task2.configuration.windowConfiguration.bounds),
+            )
+        // Does not remove wallpaper activity, as desktop still has visible desktop tasks
+        assertThat(wctArgument.value.hierarchyOps).isEmpty()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+    fun newWindow_fromFullscreenOpensInSplit() {
+        setUpLandscapeDisplay()
+        val task = setUpFullscreenTask()
+        val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+        runOpenNewWindow(task)
+        verify(splitScreenController)
+            .startIntent(
+                any(),
+                anyInt(),
+                any(),
+                any(),
+                optionsCaptor.capture(),
+                anyOrNull(),
+                eq(true),
+                eq(SPLIT_INDEX_UNDEFINED),
+            )
+        assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+            .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+    fun newWindow_fromSplitOpensInSplit() {
+        setUpLandscapeDisplay()
+        val task = setUpSplitScreenTask()
+        val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+        runOpenNewWindow(task)
+        verify(splitScreenController)
+            .startIntent(
+                any(),
+                anyInt(),
+                any(),
+                any(),
+                optionsCaptor.capture(),
+                anyOrNull(),
+                eq(true),
+                eq(SPLIT_INDEX_UNDEFINED),
+            )
+        assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+            .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+    fun newWindow_fromFreeformAddsNewWindow() {
+        setUpLandscapeDisplay()
+        val task = setUpFreeformTask()
+        val wctCaptor = argumentCaptor<WindowContainerTransaction>()
+        val transition = Binder()
+        whenever(
+                mMockDesktopImmersiveController.exitImmersiveIfApplicable(
+                    any(),
+                    anyInt(),
+                    anyOrNull(),
+                    any(),
+                )
+            )
+            .thenReturn(ExitResult.NoExit)
+        whenever(
+                desktopMixedTransitionHandler.startLaunchTransition(
+                    anyInt(),
+                    any(),
+                    anyOrNull(),
+                    anyOrNull(),
+                    anyOrNull(),
+                )
+            )
+            .thenReturn(transition)
+
+        runOpenNewWindow(task)
+
+        verify(desktopMixedTransitionHandler)
+            .startLaunchTransition(
+                anyInt(),
+                wctCaptor.capture(),
+                anyOrNull(),
+                anyOrNull(),
+                anyOrNull(),
+            )
+        assertThat(
+                ActivityOptions.fromBundle(wctCaptor.firstValue.hierarchyOps[0].launchOptions)
+                    .launchWindowingMode
+            )
+            .isEqualTo(WINDOWING_MODE_FREEFORM)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+    fun newWindow_fromFreeform_exitsImmersiveIfNeeded() {
+        setUpLandscapeDisplay()
+        val immersiveTask = setUpFreeformTask()
+        val task = setUpFreeformTask()
+        val runOnStart = RunOnStartTransitionCallback()
+        val transition = Binder()
+        whenever(
+                mMockDesktopImmersiveController.exitImmersiveIfApplicable(
+                    any(),
+                    anyInt(),
+                    anyOrNull(),
+                    any(),
+                )
+            )
+            .thenReturn(ExitResult.Exit(immersiveTask.taskId, runOnStart))
+        whenever(
+                desktopMixedTransitionHandler.startLaunchTransition(
+                    anyInt(),
+                    any(),
+                    anyOrNull(),
+                    anyOrNull(),
+                    anyOrNull(),
+                )
+            )
+            .thenReturn(transition)
+
+        runOpenNewWindow(task)
+
+        runOnStart.assertOnlyInvocation(transition)
+    }
+
+    private fun runOpenNewWindow(task: RunningTaskInfo) {
+        markTaskVisible(task)
+        task.baseActivity = mock(ComponentName::class.java)
+        task.isFocused = true
+        runningTasks.add(task)
+        controller.openNewWindow(task)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+    fun openInstance_fromFullscreenOpensInSplit() {
+        setUpLandscapeDisplay()
+        val task = setUpFullscreenTask()
+        val taskToRequest = setUpFreeformTask()
+        val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+        runOpenInstance(task, taskToRequest.taskId)
+        verify(splitScreenController)
+            .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
+        assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+            .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+    fun openInstance_fromSplitOpensInSplit() {
+        setUpLandscapeDisplay()
+        val task = setUpSplitScreenTask()
+        val taskToRequest = setUpFreeformTask()
+        val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+        runOpenInstance(task, taskToRequest.taskId)
+        verify(splitScreenController)
+            .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
+        assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+            .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+    fun openInstance_fromFreeformAddsNewWindow() {
+        setUpLandscapeDisplay()
+        val task = setUpFreeformTask()
+        val taskToRequest = setUpFreeformTask()
+        runOpenInstance(task, taskToRequest.taskId)
+        verify(desktopMixedTransitionHandler)
+            .startLaunchTransition(anyInt(), any(), anyInt(), anyOrNull(), anyOrNull())
+        val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
+        assertThat(wct.hierarchyOps).hasSize(1)
+        wct.assertReorderAt(index = 0, taskToRequest)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+    fun openInstance_fromFreeform_minimizesIfNeeded() {
+        setUpLandscapeDisplay()
+        val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() }
+        val oldestTask = freeformTasks.first()
+        val newestTask = freeformTasks.last()
+
+        val transition = Binder()
+        val wctCaptor = argumentCaptor<WindowContainerTransaction>()
+        whenever(
+                desktopMixedTransitionHandler.startLaunchTransition(
+                    anyInt(),
+                    wctCaptor.capture(),
+                    anyInt(),
+                    anyOrNull(),
+                    anyOrNull(),
+                )
+            )
+            .thenReturn(transition)
+
+        runOpenInstance(newestTask, freeformTasks[1].taskId)
+
+        val wct = wctCaptor.firstValue
+        assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize
+        wct.assertReorderAt(0, freeformTasks[1], toTop = true)
+        wct.assertReorderAt(1, oldestTask, toTop = false)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+    fun openInstance_fromFreeform_exitsImmersiveIfNeeded() {
+        setUpLandscapeDisplay()
+        val freeformTask = setUpFreeformTask()
+        val immersiveTask = setUpFreeformTask()
+        taskRepository.setTaskInFullImmersiveState(
+            displayId = immersiveTask.displayId,
+            taskId = immersiveTask.taskId,
+            immersive = true,
+        )
+        val runOnStartTransit = RunOnStartTransitionCallback()
+        val transition = Binder()
+        whenever(
+                desktopMixedTransitionHandler.startLaunchTransition(
+                    anyInt(),
+                    any(),
+                    anyInt(),
+                    anyOrNull(),
+                    anyOrNull(),
+                )
+            )
+            .thenReturn(transition)
+        whenever(
+                mMockDesktopImmersiveController.exitImmersiveIfApplicable(
+                    any(),
+                    eq(DEFAULT_DISPLAY),
+                    eq(freeformTask.taskId),
+                    any(),
+                )
+            )
+            .thenReturn(
+                ExitResult.Exit(
+                    exitingTask = immersiveTask.taskId,
+                    runOnTransitionStart = runOnStartTransit,
+                )
+            )
+
+        runOpenInstance(immersiveTask, freeformTask.taskId)
+
+        verify(mMockDesktopImmersiveController)
+            .exitImmersiveIfApplicable(
+                any(),
+                eq(immersiveTask.displayId),
+                eq(freeformTask.taskId),
+                any(),
+            )
+        runOnStartTransit.assertOnlyInvocation(transition)
+    }
+
+    private fun runOpenInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) {
+        markTaskVisible(callingTask)
+        callingTask.baseActivity = mock(ComponentName::class.java)
+        callingTask.isFocused = true
+        runningTasks.add(callingTask)
+        controller.openInstance(callingTask, requestedTaskId)
+    }
+
+    @Test
+    fun toggleBounds_togglesToStableBounds() {
+        val bounds = Rect(0, 0, 100, 100)
+        val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
+
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.MAXIMIZE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
+                InputMethod.TOUCH,
+            ),
+        )
+
+        // Assert bounds set to stable bounds
+        val wct = getLatestToggleResizeDesktopTaskWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.MAXIMIZE_BUTTON,
+                InputMethod.TOUCH,
+                task,
+                STABLE_BOUNDS.width(),
+                STABLE_BOUNDS.height(),
+                displayController,
+            )
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
+    fun snapToHalfScreen_getSnapBounds_calculatesBoundsForResizable() {
+        val bounds = Rect(100, 100, 300, 300)
+        val task =
+            setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
+                topActivityInfo =
+                    ActivityInfo().apply {
+                        screenOrientation = SCREEN_ORIENTATION_LANDSCAPE
+                        configuration.windowConfiguration.appBounds = bounds
+                    }
+                isResizeable = true
+            }
+
+        val currentDragBounds = Rect(0, 100, 200, 300)
+        val expectedBounds =
+            Rect(
+                STABLE_BOUNDS.left,
+                STABLE_BOUNDS.top,
+                STABLE_BOUNDS.right / 2,
+                STABLE_BOUNDS.bottom,
+            )
+
+        controller.snapToHalfScreen(
+            task,
+            mockSurface,
+            currentDragBounds,
+            SnapPosition.LEFT,
+            ResizeTrigger.SNAP_LEFT_MENU,
+            InputMethod.TOUCH,
+            desktopWindowDecoration,
+        )
+        // Assert bounds set to stable bounds
+        val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
+        assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.SNAP_LEFT_MENU,
+                InputMethod.TOUCH,
+                task,
+                expectedBounds.width(),
+                expectedBounds.height(),
+                displayController,
+            )
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
+    fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() {
+        // Set up task to already be in snapped-left bounds
+        val bounds =
+            Rect(
+                STABLE_BOUNDS.left,
+                STABLE_BOUNDS.top,
+                STABLE_BOUNDS.right / 2,
+                STABLE_BOUNDS.bottom,
+            )
+        val task =
+            setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
+                topActivityInfo =
+                    ActivityInfo().apply {
+                        screenOrientation = SCREEN_ORIENTATION_LANDSCAPE
+                        configuration.windowConfiguration.appBounds = bounds
+                    }
+                isResizeable = true
+            }
+
+        // Attempt to snap left again
+        val currentDragBounds = Rect(bounds).apply { offset(-100, 0) }
+        controller.snapToHalfScreen(
+            task,
+            mockSurface,
+            currentDragBounds,
+            SnapPosition.LEFT,
+            ResizeTrigger.SNAP_LEFT_MENU,
+            InputMethod.TOUCH,
+            desktopWindowDecoration,
+        )
+        // Assert that task is NOT updated via WCT
+        verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
+
+        // Assert that task leash is updated via Surface Animations
+        verify(mReturnToDragStartAnimator)
+            .start(eq(task.taskId), eq(mockSurface), eq(currentDragBounds), eq(bounds), anyOrNull())
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.SNAP_LEFT_MENU,
+                InputMethod.TOUCH,
+                task,
+                bounds.width(),
+                bounds.height(),
+                displayController,
+            )
+    }
+
+    @Test
+    @DisableFlags(
+        Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING,
+        Flags.FLAG_ENABLE_TILE_RESIZING,
     )
-    whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
-      (i.arguments.first() as Rect).set(stableBounds)
+    fun handleSnapResizingTaskOnDrag_nonResizable_snapsToHalfScreen() {
+        val task =
+            setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { isResizeable = false }
+        val preDragBounds = Rect(100, 100, 400, 500)
+        val currentDragBounds = Rect(0, 100, 300, 500)
+        val expectedBounds =
+            Rect(
+                STABLE_BOUNDS.left,
+                STABLE_BOUNDS.top,
+                STABLE_BOUNDS.right / 2,
+                STABLE_BOUNDS.bottom,
+            )
+
+        controller.handleSnapResizingTaskOnDrag(
+            task,
+            SnapPosition.LEFT,
+            mockSurface,
+            currentDragBounds,
+            preDragBounds,
+            motionEvent,
+            desktopWindowDecoration,
+        )
+        val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
+        assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingStarted(
+                ResizeTrigger.DRAG_LEFT,
+                InputMethod.UNKNOWN_INPUT_METHOD,
+                task,
+                preDragBounds.width(),
+                preDragBounds.height(),
+                displayController,
+            )
     }
-  }
 
-  private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
-    val task = createSplitScreenTask(displayId)
-    whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true)
-    whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
-    runningTasks.add(task)
-    return task
-  }
+    @Test
+    @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING)
+    fun handleSnapResizingTaskOnDrag_nonResizable_startsRepositionAnimation() {
+        val task =
+            setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { isResizeable = false }
+        val preDragBounds = Rect(100, 100, 400, 500)
+        val currentDragBounds = Rect(0, 100, 300, 500)
 
-  private fun markTaskVisible(task: RunningTaskInfo) {
-    taskRepository.updateTask(task.displayId, task.taskId, isVisible = true)
-  }
-
-  private fun markTaskHidden(task: RunningTaskInfo) {
-    taskRepository.updateTask(task.displayId, task.taskId, isVisible = false)
-  }
-
-  private fun getLatestWct(
-      @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
-      handlerClass: Class<out TransitionHandler>? = null
-  ): WindowContainerTransaction {
-    val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (handlerClass == null) {
-      verify(transitions).startTransition(eq(type), arg.capture(), isNull())
-    } else {
-      verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
+        controller.handleSnapResizingTaskOnDrag(
+            task,
+            SnapPosition.LEFT,
+            mockSurface,
+            currentDragBounds,
+            preDragBounds,
+            motionEvent,
+            desktopWindowDecoration,
+        )
+        verify(mReturnToDragStartAnimator)
+            .start(
+                eq(task.taskId),
+                eq(mockSurface),
+                eq(currentDragBounds),
+                eq(preDragBounds),
+                any(),
+            )
+        verify(desktopModeEventLogger, never())
+            .logTaskResizingStarted(any(), any(), any(), any(), any(), any(), any())
     }
-    return arg.value
-  }
 
-  private fun getLatestToggleResizeDesktopTaskWct(
-    currentBounds: Rect? = null
-  ): WindowContainerTransaction {
-    val arg: ArgumentCaptor<WindowContainerTransaction> =
-        ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
-        .startTransition(capture(arg), eq(currentBounds))
-    return arg.value
-  }
+    @Test
+    @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING)
+    fun handleInstantSnapResizingTask_nonResizable_animatorNotStartedAndShowsToast() {
+        val taskBounds = Rect(0, 0, 200, 100)
+        val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply { isResizeable = false }
 
-  private fun getLatestDesktopMixedTaskWct(
-    @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
-  ): WindowContainerTransaction {
-    val arg: ArgumentCaptor<WindowContainerTransaction> =
-      ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(desktopMixedTransitionHandler)
-      .startLaunchTransition(eq(type), capture(arg), anyInt(), anyOrNull(), anyOrNull())
-    return arg.value
-  }
+        controller.handleInstantSnapResizingTask(
+            task,
+            SnapPosition.LEFT,
+            ResizeTrigger.SNAP_LEFT_MENU,
+            InputMethod.MOUSE,
+            desktopWindowDecoration,
+        )
 
-  private fun getLatestEnterDesktopWct(): WindowContainerTransaction {
-    val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
-    return arg.value
-  }
+        // Assert that task is NOT updated via WCT
+        verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
+        verify(mockToast).show()
+    }
 
-  private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
-    val arg: ArgumentCaptor<WindowContainerTransaction> =
-        ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
-    return arg.value
-  }
+    @Test
+    @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING)
+    @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
+    fun handleInstantSnapResizingTask_resizable_snapsToHalfScreenAndNotShowToast() {
+        val taskBounds = Rect(0, 0, 200, 100)
+        val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply { isResizeable = true }
+        val expectedBounds =
+            Rect(
+                STABLE_BOUNDS.left,
+                STABLE_BOUNDS.top,
+                STABLE_BOUNDS.right / 2,
+                STABLE_BOUNDS.bottom,
+            )
 
-  private fun getLatestExitDesktopWct(): WindowContainerTransaction {
-    val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any())
-    return arg.value
-  }
+        controller.handleInstantSnapResizingTask(
+            task,
+            SnapPosition.LEFT,
+            ResizeTrigger.SNAP_LEFT_MENU,
+            InputMethod.MOUSE,
+            desktopWindowDecoration,
+        )
 
-  private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
-      wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
+        // Assert bounds set to half of the stable bounds
+        val wct = getLatestToggleResizeDesktopTaskWct(taskBounds)
+        assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
+        verify(mockToast, never()).show()
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingStarted(
+                ResizeTrigger.SNAP_LEFT_MENU,
+                InputMethod.MOUSE,
+                task,
+                taskBounds.width(),
+                taskBounds.height(),
+                displayController,
+            )
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.SNAP_LEFT_MENU,
+                InputMethod.MOUSE,
+                task,
+                expectedBounds.width(),
+                expectedBounds.height(),
+                displayController,
+            )
+    }
 
-  private fun verifyWCTNotExecuted() {
-    verify(transitions, never()).startTransition(anyInt(), any(), isNull())
-  }
+    @Test
+    fun toggleBounds_togglesToCalculatedBoundsForNonResizable() {
+        val bounds = Rect(0, 0, 200, 100)
+        val task =
+            setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
+                topActivityInfo =
+                    ActivityInfo().apply {
+                        screenOrientation = SCREEN_ORIENTATION_LANDSCAPE
+                        configuration.windowConfiguration.appBounds = bounds
+                    }
+                appCompatTaskInfo.topActivityLetterboxAppWidth = bounds.width()
+                appCompatTaskInfo.topActivityLetterboxAppHeight = bounds.height()
+                isResizeable = false
+            }
 
-  private fun verifyExitDesktopWCTNotExecuted() {
-    verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any())
-  }
+        // Bounds should be 1000 x 500, vertically centered in the 1000 x 1000 stable bounds
+        val expectedBounds = Rect(STABLE_BOUNDS.left, 250, STABLE_BOUNDS.right, 750)
 
-  private fun verifyEnterDesktopWCTNotExecuted() {
-    verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any())
-  }
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.MAXIMIZE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
+                InputMethod.TOUCH,
+            ),
+        )
 
-  private fun createTransition(
-      task: RunningTaskInfo?,
-      @WindowManager.TransitionType type: Int = TRANSIT_OPEN
-  ): TransitionRequestInfo {
-    return TransitionRequestInfo(type, task, null /* remoteTransition */)
-  }
+        // Assert bounds set to stable bounds
+        val wct = getLatestToggleResizeDesktopTaskWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.MAXIMIZE_BUTTON,
+                InputMethod.TOUCH,
+                task,
+                expectedBounds.width(),
+                expectedBounds.height(),
+                displayController,
+            )
+    }
 
-  private companion object {
-    const val SECOND_DISPLAY = 2
-    val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
-    const val MAX_TASK_LIMIT = 6
-    private const val TASKBAR_FRAME_HEIGHT = 200
-  }
+    @Test
+    fun toggleBounds_lastBoundsBeforeMaximizeSaved() {
+        val bounds = Rect(0, 0, 100, 100)
+        val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
+
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.MAXIMIZE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
+                InputMethod.TOUCH,
+            ),
+        )
+
+        assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds)
+        verify(desktopModeEventLogger, never())
+            .logTaskResizingEnded(any(), any(), any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() {
+        val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+        val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
+
+        // Maximize
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.MAXIMIZE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
+                InputMethod.TOUCH,
+            ),
+        )
+        task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
+
+        // Restore
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.RESTORE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
+                InputMethod.TOUCH,
+            ),
+        )
+
+        // Assert bounds set to last bounds before maximize
+        val wct = getLatestToggleResizeDesktopTaskWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.MAXIMIZE_BUTTON,
+                InputMethod.TOUCH,
+                task,
+                boundsBeforeMaximize.width(),
+                boundsBeforeMaximize.height(),
+                displayController,
+            )
+    }
+
+    @Test
+    fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualWidth() {
+        val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+        val task =
+            setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply { isResizeable = false }
+
+        // Maximize
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.MAXIMIZE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
+                InputMethod.TOUCH,
+            ),
+        )
+        task.configuration.windowConfiguration.bounds.set(
+            STABLE_BOUNDS.left,
+            boundsBeforeMaximize.top,
+            STABLE_BOUNDS.right,
+            boundsBeforeMaximize.bottom,
+        )
+
+        // Restore
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.RESTORE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
+                InputMethod.TOUCH,
+            ),
+        )
+
+        // Assert bounds set to last bounds before maximize
+        val wct = getLatestToggleResizeDesktopTaskWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.MAXIMIZE_BUTTON,
+                InputMethod.TOUCH,
+                task,
+                boundsBeforeMaximize.width(),
+                boundsBeforeMaximize.height(),
+                displayController,
+            )
+    }
+
+    @Test
+    fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualHeight() {
+        val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+        val task =
+            setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply { isResizeable = false }
+
+        // Maximize
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.MAXIMIZE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
+                InputMethod.TOUCH,
+            ),
+        )
+        task.configuration.windowConfiguration.bounds.set(
+            boundsBeforeMaximize.left,
+            STABLE_BOUNDS.top,
+            boundsBeforeMaximize.right,
+            STABLE_BOUNDS.bottom,
+        )
+
+        // Restore
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.RESTORE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
+                InputMethod.TOUCH,
+            ),
+        )
+
+        // Assert bounds set to last bounds before maximize
+        val wct = getLatestToggleResizeDesktopTaskWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.MAXIMIZE_BUTTON,
+                InputMethod.TOUCH,
+                task,
+                boundsBeforeMaximize.width(),
+                boundsBeforeMaximize.height(),
+                displayController,
+            )
+    }
+
+    @Test
+    fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() {
+        val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+        val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
+
+        // Maximize
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.MAXIMIZE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE,
+                InputMethod.TOUCH,
+            ),
+        )
+        task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
+
+        // Restore
+        controller.toggleDesktopTaskSize(
+            task,
+            ToggleTaskSizeInteraction(
+                ToggleTaskSizeInteraction.Direction.RESTORE,
+                ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE,
+                InputMethod.TOUCH,
+            ),
+        )
+
+        // Assert last bounds before maximize removed after use
+        assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
+        verify(desktopModeEventLogger, times(1))
+            .logTaskResizingEnded(
+                ResizeTrigger.MAXIMIZE_BUTTON,
+                InputMethod.TOUCH,
+                task,
+                boundsBeforeMaximize.width(),
+                boundsBeforeMaximize.height(),
+                displayController,
+            )
+    }
+
+    @Test
+    fun onUnhandledDrag_newFreeformIntent() {
+        testOnUnhandledDrag(
+            DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
+            PointF(1200f, 700f),
+            Rect(240, 700, 2160, 1900),
+        )
+    }
+
+    @Test
+    fun onUnhandledDrag_newFreeformIntentSplitLeft() {
+        testOnUnhandledDrag(
+            DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
+            PointF(50f, 700f),
+            Rect(0, 0, 500, 1000),
+        )
+    }
+
+    @Test
+    fun onUnhandledDrag_newFreeformIntentSplitRight() {
+        testOnUnhandledDrag(
+            DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
+            PointF(2500f, 700f),
+            Rect(500, 0, 1000, 1000),
+        )
+    }
+
+    @Test
+    fun onUnhandledDrag_newFullscreenIntent() {
+        testOnUnhandledDrag(
+            DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
+            PointF(1200f, 50f),
+            Rect(),
+        )
+    }
+
+    @Test
+    fun shellController_registersUserChangeListener() {
+        verify(shellController, times(2)).addUserChangeListener(any())
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun onTaskInfoChanged_inImmersiveUnrequestsImmersive_exits() {
+        val task = setUpFreeformTask(DEFAULT_DISPLAY)
+        taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = true)
+
+        task.requestedVisibleTypes = WindowInsets.Type.statusBars()
+        controller.onTaskInfoChanged(task)
+
+        verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(eq(task), any())
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun onTaskInfoChanged_notInImmersiveUnrequestsImmersive_noReExit() {
+        val task = setUpFreeformTask(DEFAULT_DISPLAY)
+        taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = false)
+
+        task.requestedVisibleTypes = WindowInsets.Type.statusBars()
+        controller.onTaskInfoChanged(task)
+
+        verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any())
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun onTaskInfoChanged_inImmersiveUnrequestsImmersive_inRecentsTransition_noExit() {
+        val task = setUpFreeformTask(DEFAULT_DISPLAY)
+        taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = true)
+        recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_REQUESTED)
+
+        task.requestedVisibleTypes = WindowInsets.Type.statusBars()
+        controller.onTaskInfoChanged(task)
+
+        verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any())
+    }
+
+    @Test
+    fun moveTaskToDesktop_background_attemptsImmersiveExit() {
+        val task = setUpFreeformTask(background = true)
+        val wct = WindowContainerTransaction()
+        val runOnStartTransit = RunOnStartTransitionCallback()
+        val transition = Binder()
+        whenever(
+                mMockDesktopImmersiveController.exitImmersiveIfApplicable(
+                    eq(wct),
+                    eq(task.displayId),
+                    eq(task.taskId),
+                    any(),
+                )
+            )
+            .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit))
+        whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
+
+        controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+
+        verify(mMockDesktopImmersiveController)
+            .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())
+        runOnStartTransit.assertOnlyInvocation(transition)
+    }
+
+    @Test
+    fun moveTaskToDesktop_foreground_attemptsImmersiveExit() {
+        val task = setUpFreeformTask(background = false)
+        val wct = WindowContainerTransaction()
+        val runOnStartTransit = RunOnStartTransitionCallback()
+        val transition = Binder()
+        whenever(
+                mMockDesktopImmersiveController.exitImmersiveIfApplicable(
+                    eq(wct),
+                    eq(task.displayId),
+                    eq(task.taskId),
+                    any(),
+                )
+            )
+            .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit))
+        whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
+
+        controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+
+        verify(mMockDesktopImmersiveController)
+            .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())
+        runOnStartTransit.assertOnlyInvocation(transition)
+    }
+
+    @Test
+    fun moveTaskToFront_background_attemptsImmersiveExit() {
+        val task = setUpFreeformTask(background = true)
+        val runOnStartTransit = RunOnStartTransitionCallback()
+        val transition = Binder()
+        whenever(
+                mMockDesktopImmersiveController.exitImmersiveIfApplicable(
+                    any(),
+                    eq(task.displayId),
+                    eq(task.taskId),
+                    any(),
+                )
+            )
+            .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit))
+        whenever(
+                desktopMixedTransitionHandler.startLaunchTransition(
+                    any(),
+                    any(),
+                    anyInt(),
+                    anyOrNull(),
+                    anyOrNull(),
+                )
+            )
+            .thenReturn(transition)
+
+        controller.moveTaskToFront(task.taskId, remoteTransition = null)
+
+        verify(mMockDesktopImmersiveController)
+            .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any())
+        runOnStartTransit.assertOnlyInvocation(transition)
+    }
+
+    @Test
+    fun moveTaskToFront_foreground_attemptsImmersiveExit() {
+        val task = setUpFreeformTask(background = false)
+        val runOnStartTransit = RunOnStartTransitionCallback()
+        val transition = Binder()
+        whenever(
+                mMockDesktopImmersiveController.exitImmersiveIfApplicable(
+                    any(),
+                    eq(task.displayId),
+                    eq(task.taskId),
+                    any(),
+                )
+            )
+            .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit))
+        whenever(
+                desktopMixedTransitionHandler.startLaunchTransition(
+                    any(),
+                    any(),
+                    eq(task.taskId),
+                    anyOrNull(),
+                    anyOrNull(),
+                )
+            )
+            .thenReturn(transition)
+
+        controller.moveTaskToFront(task.taskId, remoteTransition = null)
+
+        verify(mMockDesktopImmersiveController)
+            .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any())
+        runOnStartTransit.assertOnlyInvocation(transition)
+    }
+
+    @Test
+    fun handleRequest_freeformLaunchToDesktop_attemptsImmersiveExit() {
+        markTaskVisible(setUpFreeformTask())
+        val task = setUpFreeformTask()
+        markTaskVisible(task)
+        val binder = Binder()
+
+        controller.handleRequest(binder, createTransition(task))
+
+        verify(mMockDesktopImmersiveController)
+            .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any())
+    }
+
+    @Test
+    fun handleRequest_fullscreenLaunchToDesktop_attemptsImmersiveExit() {
+        setUpFreeformTask()
+        val task = setUpFullscreenTask()
+        val binder = Binder()
+
+        controller.handleRequest(binder, createTransition(task))
+
+        verify(mMockDesktopImmersiveController)
+            .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() {
+        val triggerTask = setUpFullscreenTask(displayId = 5)
+        taskRepository.setTaskInFullImmersiveState(
+            displayId = triggerTask.displayId,
+            taskId = triggerTask.taskId,
+            immersive = true,
+        )
+
+        assertThat(
+                controller.shouldPlayDesktopAnimation(
+                    TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+                )
+            )
+            .isFalse()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() {
+        val triggerTask = setUpFreeformTask(displayId = 5)
+        taskRepository.setTaskInFullImmersiveState(
+            displayId = triggerTask.displayId,
+            taskId = triggerTask.taskId,
+            immersive = true,
+        )
+
+        assertThat(
+                controller.shouldPlayDesktopAnimation(
+                    TransitionRequestInfo(TRANSIT_CHANGE, triggerTask, /* remoteTransition= */ null)
+                )
+            )
+            .isFalse()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() {
+        val triggerTask = setUpFreeformTask(displayId = 5)
+        taskRepository.setTaskInFullImmersiveState(
+            displayId = triggerTask.displayId,
+            taskId = triggerTask.taskId,
+            immersive = false,
+        )
+
+        assertThat(
+                controller.shouldPlayDesktopAnimation(
+                    TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+                )
+            )
+            .isFalse()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() {
+        // At least one freeform task to be in a desktop.
+        val existingTask = setUpFreeformTask(displayId = 5)
+        val triggerTask = setUpFullscreenTask(displayId = 5)
+        assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
+        taskRepository.setTaskInFullImmersiveState(
+            displayId = existingTask.displayId,
+            taskId = existingTask.taskId,
+            immersive = true,
+        )
+
+        assertThat(
+                controller.shouldPlayDesktopAnimation(
+                    TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+                )
+            )
+            .isTrue()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() {
+        val triggerTask = setUpFullscreenTask(displayId = 5)
+        assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
+
+        assertThat(
+                controller.shouldPlayDesktopAnimation(
+                    TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+                )
+            )
+            .isFalse()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() {
+        // At least one freeform task to be in a desktop.
+        val existingTask = setUpFreeformTask(displayId = 5)
+        val triggerTask = setUpFreeformTask(displayId = 5, active = false)
+        assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
+        taskRepository.setTaskInFullImmersiveState(
+            displayId = existingTask.displayId,
+            taskId = existingTask.taskId,
+            immersive = true,
+        )
+
+        assertThat(
+                controller.shouldPlayDesktopAnimation(
+                    TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+                )
+            )
+            .isTrue()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() {
+        val triggerTask = setUpFreeformTask(displayId = 5, active = false)
+        assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
+
+        assertThat(
+                controller.shouldPlayDesktopAnimation(
+                    TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+                )
+            )
+            .isFalse()
+    }
+
+    private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
+        var invocations = 0
+            private set
+
+        var lastInvoked: IBinder? = null
+            private set
+
+        override fun invoke(transition: IBinder) {
+            invocations++
+            lastInvoked = transition
+        }
+    }
+
+    private fun RunOnStartTransitionCallback.assertOnlyInvocation(transition: IBinder) {
+        assertThat(invocations).isEqualTo(1)
+        assertThat(lastInvoked).isEqualTo(transition)
+    }
+
+    /**
+     * Assert that an unhandled drag event launches a PendingIntent with the windowing mode and
+     * bounds we are expecting.
+     */
+    private fun testOnUnhandledDrag(
+        indicatorType: DesktopModeVisualIndicator.IndicatorType,
+        inputCoordinate: PointF,
+        expectedBounds: Rect,
+    ) {
+        setUpLandscapeDisplay()
+        val task = setUpFreeformTask()
+        markTaskVisible(task)
+        task.isFocused = true
+        val runningTasks = ArrayList<RunningTaskInfo>()
+        runningTasks.add(task)
+        val spyController = spy(controller)
+        val mockPendingIntent = mock(PendingIntent::class.java)
+        val mockDragEvent = mock(DragEvent::class.java)
+        val mockCallback = mock(Consumer::class.java)
+        val b = SurfaceControl.Builder()
+        b.setName("test surface")
+        val dragSurface = b.build()
+        whenever(shellTaskOrganizer.runningTasks).thenReturn(runningTasks)
+        whenever(mockDragEvent.dragSurface).thenReturn(dragSurface)
+        whenever(mockDragEvent.x).thenReturn(inputCoordinate.x)
+        whenever(mockDragEvent.y).thenReturn(inputCoordinate.y)
+        whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        doReturn(indicatorType)
+            .whenever(spyController)
+            .updateVisualIndicator(
+                eq(task),
+                anyOrNull(),
+                anyOrNull(),
+                anyOrNull(),
+                eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT),
+            )
+
+        spyController.onUnhandledDrag(
+            mockPendingIntent,
+            mockDragEvent,
+            mockCallback as Consumer<Boolean>,
+        )
+        val arg: ArgumentCaptor<WindowContainerTransaction> =
+            ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        var expectedWindowingMode: Int
+        if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) {
+            expectedWindowingMode = WINDOWING_MODE_FULLSCREEN
+            // Fullscreen launches currently use default transitions
+            verify(transitions).startTransition(any(), capture(arg), anyOrNull())
+        } else {
+            expectedWindowingMode = WINDOWING_MODE_FREEFORM
+            // All other launches use a special handler.
+            verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg))
+        }
+        assertThat(
+                ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
+                    .launchWindowingMode
+            )
+            .isEqualTo(expectedWindowingMode)
+        assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions).launchBounds)
+            .isEqualTo(expectedBounds)
+    }
+
+    private val desktopWallpaperIntent: Intent
+        get() = Intent(context, DesktopWallpaperActivity::class.java)
+
+    private fun addFreeformTaskAtPosition(
+        pos: DesktopTaskPosition,
+        stableBounds: Rect,
+        bounds: Rect = DEFAULT_LANDSCAPE_BOUNDS,
+        offsetPos: Point = Point(0, 0),
+    ): RunningTaskInfo {
+        val offset = pos.getTopLeftCoordinates(stableBounds, bounds)
+        val prevTaskBounds = Rect(bounds)
+        prevTaskBounds.offsetTo(offset.x + offsetPos.x, offset.y + offsetPos.y)
+        return setUpFreeformTask(bounds = prevTaskBounds)
+    }
+
+    private fun setUpFreeformTask(
+        displayId: Int = DEFAULT_DISPLAY,
+        bounds: Rect? = null,
+        active: Boolean = true,
+        background: Boolean = false,
+    ): RunningTaskInfo {
+        val task = createFreeformTask(displayId, bounds)
+        val activityInfo = ActivityInfo()
+        task.topActivityInfo = activityInfo
+        if (background) {
+            whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+            whenever(recentTasksController.findTaskInBackground(task.taskId))
+                .thenReturn(createTaskInfo(task.taskId))
+        } else {
+            whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        }
+        taskRepository.addTask(displayId, task.taskId, isVisible = active)
+        if (!background) {
+            runningTasks.add(task)
+        }
+        return task
+    }
+
+    private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo {
+        return setUpFreeformTask().apply {
+            pictureInPictureParams =
+                PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build()
+        }
+    }
+
+    private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+        val task = createHomeTask(displayId)
+        whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        runningTasks.add(task)
+        return task
+    }
+
+    private fun setUpFullscreenTask(
+        displayId: Int = DEFAULT_DISPLAY,
+        isResizable: Boolean = true,
+        windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
+        deviceOrientation: Int = ORIENTATION_LANDSCAPE,
+        screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED,
+        shouldLetterbox: Boolean = false,
+        gravity: Int = Gravity.NO_GRAVITY,
+        enableUserFullscreenOverride: Boolean = false,
+        enableSystemFullscreenOverride: Boolean = false,
+        aspectRatioOverrideApplied: Boolean = false,
+    ): RunningTaskInfo {
+        val task = createFullscreenTask(displayId)
+        val activityInfo = ActivityInfo()
+        activityInfo.screenOrientation = screenOrientation
+        activityInfo.windowLayout = ActivityInfo.WindowLayout(0, 0F, 0, 0F, gravity, 0, 0)
+        with(task) {
+            topActivityInfo = activityInfo
+            isResizeable = isResizable
+            configuration.orientation = deviceOrientation
+            configuration.windowConfiguration.windowingMode = windowingMode
+            appCompatTaskInfo.isUserFullscreenOverrideEnabled = enableUserFullscreenOverride
+            appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride
+
+            if (deviceOrientation == ORIENTATION_LANDSCAPE) {
+                configuration.windowConfiguration.appBounds =
+                    Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
+                appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_LONG
+                appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_SHORT
+            } else {
+                configuration.windowConfiguration.appBounds =
+                    Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG)
+                appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_SHORT
+                appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_LONG
+            }
+
+            if (shouldLetterbox) {
+                appCompatTaskInfo.setHasMinAspectRatioOverride(aspectRatioOverrideApplied)
+                if (
+                    deviceOrientation == ORIENTATION_LANDSCAPE &&
+                        screenOrientation == SCREEN_ORIENTATION_PORTRAIT
+                ) {
+                    // Letterbox to portrait size
+                    appCompatTaskInfo.setTopActivityLetterboxed(true)
+                    appCompatTaskInfo.topActivityLetterboxAppWidth = 1200
+                    appCompatTaskInfo.topActivityLetterboxAppHeight = 1600
+                } else if (
+                    deviceOrientation == ORIENTATION_PORTRAIT &&
+                        screenOrientation == SCREEN_ORIENTATION_LANDSCAPE
+                ) {
+                    // Letterbox to landscape size
+                    appCompatTaskInfo.setTopActivityLetterboxed(true)
+                    appCompatTaskInfo.topActivityLetterboxAppWidth = 1600
+                    appCompatTaskInfo.topActivityLetterboxAppHeight = 1200
+                }
+            }
+        }
+        whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        runningTasks.add(task)
+        return task
+    }
+
+    private fun setUpLandscapeDisplay() {
+        whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG)
+        whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT)
+        val stableBounds =
+            Rect(
+                0,
+                0,
+                DISPLAY_DIMENSION_LONG,
+                DISPLAY_DIMENSION_SHORT - Companion.TASKBAR_FRAME_HEIGHT,
+            )
+        whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+    }
+
+    private fun setUpPortraitDisplay() {
+        whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
+        whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
+        val stableBounds =
+            Rect(
+                0,
+                0,
+                DISPLAY_DIMENSION_SHORT,
+                DISPLAY_DIMENSION_LONG - Companion.TASKBAR_FRAME_HEIGHT,
+            )
+        whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+    }
+
+    private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+        val task = createSplitScreenTask(displayId)
+        whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true)
+        whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        runningTasks.add(task)
+        return task
+    }
+
+    private fun markTaskVisible(task: RunningTaskInfo) {
+        taskRepository.updateTask(task.displayId, task.taskId, isVisible = true)
+    }
+
+    private fun markTaskHidden(task: RunningTaskInfo) {
+        taskRepository.updateTask(task.displayId, task.taskId, isVisible = false)
+    }
+
+    private fun getLatestWct(
+        @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
+        handlerClass: Class<out TransitionHandler>? = null,
+    ): WindowContainerTransaction {
+        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        if (handlerClass == null) {
+            verify(transitions).startTransition(eq(type), arg.capture(), isNull())
+        } else {
+            verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
+        }
+        return arg.value
+    }
+
+    private fun getLatestToggleResizeDesktopTaskWct(
+        currentBounds: Rect? = null
+    ): WindowContainerTransaction {
+        val arg: ArgumentCaptor<WindowContainerTransaction> =
+            ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
+            .startTransition(capture(arg), eq(currentBounds))
+        return arg.value
+    }
+
+    private fun getLatestDesktopMixedTaskWct(
+        @WindowManager.TransitionType type: Int = TRANSIT_OPEN
+    ): WindowContainerTransaction {
+        val arg: ArgumentCaptor<WindowContainerTransaction> =
+            ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(desktopMixedTransitionHandler)
+            .startLaunchTransition(eq(type), capture(arg), anyInt(), anyOrNull(), anyOrNull())
+        return arg.value
+    }
+
+    private fun getLatestEnterDesktopWct(): WindowContainerTransaction {
+        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
+        return arg.value
+    }
+
+    private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
+        val arg: ArgumentCaptor<WindowContainerTransaction> =
+            ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
+        return arg.value
+    }
+
+    private fun getLatestExitDesktopWct(): WindowContainerTransaction {
+        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any())
+        return arg.value
+    }
+
+    private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
+        wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
+
+    private fun verifyWCTNotExecuted() {
+        verify(transitions, never()).startTransition(anyInt(), any(), isNull())
+    }
+
+    private fun verifyExitDesktopWCTNotExecuted() {
+        verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any())
+    }
+
+    private fun verifyEnterDesktopWCTNotExecuted() {
+        verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any())
+    }
+
+    private fun createTransition(
+        task: RunningTaskInfo?,
+        @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
+    ): TransitionRequestInfo {
+        return TransitionRequestInfo(type, task, null /* remoteTransition */)
+    }
+
+    private companion object {
+        const val SECOND_DISPLAY = 2
+        val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
+        const val MAX_TASK_LIMIT = 6
+        private const val TASKBAR_FRAME_HEIGHT = 200
+    }
 }
 
 private fun WindowContainerTransaction.assertIndexInBounds(index: Int) {
-  assertWithMessage("WCT does not have a hierarchy operation at index $index")
-      .that(hierarchyOps.size)
-      .isGreaterThan(index)
+    assertWithMessage("WCT does not have a hierarchy operation at index $index")
+        .that(hierarchyOps.size)
+        .isGreaterThan(index)
 }
 
 private fun WindowContainerTransaction.assertReorderAt(
     index: Int,
     task: RunningTaskInfo,
-    toTop: Boolean? = null
+    toTop: Boolean? = null,
 ) {
-  assertIndexInBounds(index)
-  val op = hierarchyOps[index]
-  assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER)
-  assertThat(op.container).isEqualTo(task.token.asBinder())
-  toTop?.let { assertThat(op.toTop).isEqualTo(it) }
+    assertIndexInBounds(index)
+    val op = hierarchyOps[index]
+    assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER)
+    assertThat(op.container).isEqualTo(task.token.asBinder())
+    toTop?.let { assertThat(op.toTop).isEqualTo(it) }
 }
 
 private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) {
-  for (i in tasks.indices) {
-    assertReorderAt(i, tasks[i])
-  }
+    for (i in tasks.indices) {
+        assertReorderAt(i, tasks[i])
+    }
 }
 
 /** Checks if the reorder hierarchy operations in [range] correspond to [tasks] list */
 private fun WindowContainerTransaction.assertReorderSequenceInRange(
-  range: IntRange,
-  vararg tasks: RunningTaskInfo
+    range: IntRange,
+    vararg tasks: RunningTaskInfo,
 ) {
-  assertThat(hierarchyOps.slice(range).map { it.type to it.container })
-    .containsExactlyElementsIn(tasks.map { HIERARCHY_OP_TYPE_REORDER to it.token.asBinder() })
-    .inOrder()
+    assertThat(hierarchyOps.slice(range).map { it.type to it.container })
+        .containsExactlyElementsIn(tasks.map { HIERARCHY_OP_TYPE_REORDER to it.token.asBinder() })
+        .inOrder()
 }
 
 private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) {
-  assertIndexInBounds(index)
-  val op = hierarchyOps[index]
-  assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
-  assertThat(op.container).isEqualTo(token.asBinder())
+    assertIndexInBounds(index)
+    val op = hierarchyOps[index]
+    assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+    assertThat(op.container).isEqualTo(token.asBinder())
 }
 
 private fun WindowContainerTransaction.assertNoRemoveAt(index: Int, token: WindowContainerToken) {
-  assertIndexInBounds(index)
-  val op = hierarchyOps[index]
-  assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
-  assertThat(op.container).isEqualTo(token.asBinder())
+    assertIndexInBounds(index)
+    val op = hierarchyOps[index]
+    assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+    assertThat(op.container).isEqualTo(token.asBinder())
 }
 
 private fun WindowContainerTransaction.hasRemoveAt(index: Int, token: WindowContainerToken) {
-  assertIndexInBounds(index)
-  val op = hierarchyOps[index]
-  assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
-  assertThat(op.container).isEqualTo(token.asBinder())
+    assertIndexInBounds(index)
+    val op = hierarchyOps[index]
+    assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+    assertThat(op.container).isEqualTo(token.asBinder())
 }
 
 private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) {
-  assertIndexInBounds(index)
-  val op = hierarchyOps[index]
-  assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT)
-  assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component)
+    assertIndexInBounds(index)
+    val op = hierarchyOps[index]
+    assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT)
+    assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component)
 }
 
 private fun WindowContainerTransaction.assertLaunchTaskAt(
     index: Int,
     taskId: Int,
-    windowingMode: Int
+    windowingMode: Int,
 ) {
-  val keyLaunchWindowingMode = "android.activity.windowingMode"
+    val keyLaunchWindowingMode = "android.activity.windowingMode"
 
-  assertIndexInBounds(index)
-  val op = hierarchyOps[index]
-  assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_LAUNCH_TASK)
-  assertThat(op.launchOptions?.getInt(LAUNCH_KEY_TASK_ID)).isEqualTo(taskId)
-  assertThat(op.launchOptions?.getInt(keyLaunchWindowingMode, WINDOWING_MODE_UNDEFINED))
-      .isEqualTo(windowingMode)
+    assertIndexInBounds(index)
+    val op = hierarchyOps[index]
+    assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_LAUNCH_TASK)
+    assertThat(op.launchOptions?.getInt(LAUNCH_KEY_TASK_ID)).isEqualTo(taskId)
+    assertThat(op.launchOptions?.getInt(keyLaunchWindowingMode, WINDOWING_MODE_UNDEFINED))
+        .isEqualTo(windowingMode)
 }
 
 private fun WindowContainerTransaction?.anyDensityConfigChange(
     token: WindowContainerToken
 ): Boolean {
-  return this?.changes?.any { change ->
-    change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0)
-  } ?: false
+    return this?.changes?.any { change ->
+        change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0)
+    } ?: false
 }
 
 private fun WindowContainerTransaction?.anyWindowingModeChange(
-  token: WindowContainerToken
+    token: WindowContainerToken
 ): Boolean {
-return this?.changes?.any { change ->
-  change.key == token.asBinder() && change.value.windowingMode >= 0
-} ?: false
+    return this?.changes?.any { change ->
+        change.key == token.asBinder() && change.value.windowingMode >= 0
+    } ?: false
 }
 
 private fun createTaskInfo(id: Int) =
     RecentTaskInfo().apply {
-      taskId = id
-      token = WindowContainerToken(mock(IWindowContainerToken::class.java))
+        taskId = id
+        token = WindowContainerToken(mock(IWindowContainerToken::class.java))
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 1e4d108..e6f1fcf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -42,12 +42,12 @@
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
 import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
 import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.TransitionInfoBuilder
 import com.android.wm.shell.transition.Transitions
@@ -85,9 +85,7 @@
 @ExperimentalCoroutinesApi
 class DesktopTasksLimiterTest : ShellTestCase() {
 
-    @JvmField
-    @Rule
-    val setFlagsRule = SetFlagsRule()
+    @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
     @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
     @Mock lateinit var transitions: Transitions
@@ -108,9 +106,12 @@
 
     @Before
     fun setUp() {
-        mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT)
-                .spyStatic(DesktopModeStatus::class.java).startMocking()
-        doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) }
+        mockitoSession =
+            ExtendedMockito.mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DesktopModeStatus::class.java)
+                .startMocking()
+        doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
         shellInit = spy(ShellInit(testExecutor))
         Dispatchers.setMain(StandardTestDispatcher())
         testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
@@ -123,12 +124,19 @@
                 persistentRepository,
                 repositoryInitializer,
                 testScope,
-                userManager
+                userManager,
             )
         desktopTaskRepo = userRepositories.current
         desktopTasksLimiter =
-            DesktopTasksLimiter(transitions, userRepositories, shellTaskOrganizer, MAX_TASK_LIMIT,
-                interactionJankMonitor, mContext, handler)
+            DesktopTasksLimiter(
+                transitions,
+                userRepositories,
+                shellTaskOrganizer,
+                MAX_TASK_LIMIT,
+                interactionJankMonitor,
+                mContext,
+                handler,
+            )
     }
 
     @After
@@ -140,16 +148,30 @@
     @Test
     fun createDesktopTasksLimiter_withZeroLimit_shouldThrow() {
         assertFailsWith<IllegalArgumentException> {
-            DesktopTasksLimiter(transitions, userRepositories, shellTaskOrganizer, 0,
-                interactionJankMonitor, mContext, handler)
+            DesktopTasksLimiter(
+                transitions,
+                userRepositories,
+                shellTaskOrganizer,
+                0,
+                interactionJankMonitor,
+                mContext,
+                handler,
+            )
         }
     }
 
     @Test
     fun createDesktopTasksLimiter_withNegativeLimit_shouldThrow() {
         assertFailsWith<IllegalArgumentException> {
-            DesktopTasksLimiter(transitions, userRepositories, shellTaskOrganizer, -5,
-                interactionJankMonitor, mContext, handler)
+            DesktopTasksLimiter(
+                transitions,
+                userRepositories,
+                shellTaskOrganizer,
+                -5,
+                interactionJankMonitor,
+                mContext,
+                handler,
+            )
         }
     }
 
@@ -168,11 +190,14 @@
         val task = setUpFreeformTask()
         markTaskHidden(task)
 
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
                 Binder() /* transition */,
                 TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
                 StubTransaction() /* startTransaction */,
-                StubTransaction() /* finishTransaction */)
+                StubTransaction(), /* finishTransaction */
+            )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
     }
@@ -184,13 +209,19 @@
         val task = setUpFreeformTask()
         markTaskHidden(task)
         desktopTasksLimiter.addPendingMinimizeChange(
-            pendingTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+            pendingTransition,
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+        )
 
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
-            taskTransition /* transition */,
-            TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
-            StubTransaction() /* startTransaction */,
-            StubTransaction() /* finishTransaction */)
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
+                taskTransition /* transition */,
+                TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
+                StubTransaction() /* startTransaction */,
+                StubTransaction(), /* finishTransaction */
+            )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
     }
@@ -201,13 +232,19 @@
         val task = setUpFreeformTask()
         markTaskVisible(task)
         desktopTasksLimiter.addPendingMinimizeChange(
-                transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+            transition,
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+        )
 
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
                 transition,
                 TransitionInfoBuilder(TRANSIT_OPEN).build(),
                 StubTransaction() /* startTransaction */,
-                StubTransaction() /* finishTransaction */)
+                StubTransaction(), /* finishTransaction */
+            )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
     }
@@ -218,13 +255,19 @@
         val task = setUpFreeformTask()
         markTaskHidden(task)
         desktopTasksLimiter.addPendingMinimizeChange(
-                transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+            transition,
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+        )
 
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
                 transition,
                 TransitionInfoBuilder(TRANSIT_OPEN).build(),
                 StubTransaction() /* startTransaction */,
-                StubTransaction() /* finishTransaction */)
+                StubTransaction(), /* finishTransaction */
+            )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
     }
@@ -234,13 +277,19 @@
         val transition = Binder()
         val task = setUpFreeformTask()
         desktopTasksLimiter.addPendingMinimizeChange(
-                transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+            transition,
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+        )
 
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
                 transition,
                 TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
                 StubTransaction() /* startTransaction */,
-                StubTransaction() /* finishTransaction */)
+                StubTransaction(), /* finishTransaction */
+            )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
     }
@@ -251,22 +300,29 @@
         val transition = Binder()
         val task = setUpFreeformTask()
         desktopTasksLimiter.addPendingMinimizeChange(
-            transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
-
-        val change = TransitionInfo.Change(task.token, mock(SurfaceControl::class.java)).apply {
-            mode = TRANSIT_TO_BACK
-            taskInfo = task
-            setStartAbsBounds(bounds)
-        }
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
             transition,
-            TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) },
-            StubTransaction() /* startTransaction */,
-            StubTransaction() /* finishTransaction */)
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+        )
+
+        val change =
+            TransitionInfo.Change(task.token, mock(SurfaceControl::class.java)).apply {
+                mode = TRANSIT_TO_BACK
+                taskInfo = task
+                setStartAbsBounds(bounds)
+            }
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
+                transition,
+                TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) },
+                StubTransaction() /* startTransaction */,
+                StubTransaction(), /* finishTransaction */
+            )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
-        assertThat(desktopTaskRepo.removeBoundsBeforeMinimize(taskId = task.taskId)).isEqualTo(
-            bounds)
+        assertThat(desktopTaskRepo.removeBoundsBeforeMinimize(taskId = task.taskId))
+            .isEqualTo(bounds)
     }
 
     @Test
@@ -275,15 +331,22 @@
         val newTransition = Binder()
         val task = setUpFreeformTask()
         desktopTasksLimiter.addPendingMinimizeChange(
-            mergedTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
-        desktopTasksLimiter.getTransitionObserver().onTransitionMerged(
-            mergedTransition, newTransition)
+            mergedTransition,
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+        )
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionMerged(mergedTransition, newTransition)
 
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
-            newTransition,
-            TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
-            StubTransaction() /* startTransaction */,
-            StubTransaction() /* finishTransaction */)
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
+                newTransition,
+                TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
+                StubTransaction() /* startTransaction */,
+                StubTransaction(), /* finishTransaction */
+            )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
     }
@@ -297,7 +360,9 @@
 
         val wct = WindowContainerTransaction()
         desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks(
-            DEFAULT_DISPLAY, wct)
+            DEFAULT_DISPLAY,
+            wct,
+        )
 
         assertThat(wct.isEmpty).isTrue()
     }
@@ -307,7 +372,9 @@
     fun removeLeftoverMinimizedTasks_noMinimizedTasks_doesNothing() {
         val wct = WindowContainerTransaction()
         desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks(
-            DEFAULT_DISPLAY, wct)
+            DEFAULT_DISPLAY,
+            wct,
+        )
 
         assertThat(wct.isEmpty).isTrue()
     }
@@ -322,7 +389,9 @@
 
         val wct = WindowContainerTransaction()
         desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks(
-            DEFAULT_DISPLAY, wct)
+            DEFAULT_DISPLAY,
+            wct,
+        )
 
         assertThat(wct.hierarchyOps).hasSize(2)
         assertThat(wct.hierarchyOps[0].type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
@@ -351,10 +420,11 @@
 
         val wct = WindowContainerTransaction()
         val minimizedTaskId =
-                desktopTasksLimiter.addAndGetMinimizeTaskChanges(
-                        displayId = DEFAULT_DISPLAY,
-                        wct = wct,
-                        newFrontTaskId = setUpFreeformTask().taskId)
+            desktopTasksLimiter.addAndGetMinimizeTaskChanges(
+                displayId = DEFAULT_DISPLAY,
+                wct = wct,
+                newFrontTaskId = setUpFreeformTask().taskId,
+            )
 
         assertThat(minimizedTaskId).isNull()
         assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
@@ -367,10 +437,11 @@
 
         val wct = WindowContainerTransaction()
         val minimizedTaskId =
-                desktopTasksLimiter.addAndGetMinimizeTaskChanges(
-                        displayId = DEFAULT_DISPLAY,
-                        wct = wct,
-                        newFrontTaskId = setUpFreeformTask().taskId)
+            desktopTasksLimiter.addAndGetMinimizeTaskChanges(
+                displayId = DEFAULT_DISPLAY,
+                wct = wct,
+                newFrontTaskId = setUpFreeformTask().taskId,
+            )
 
         assertThat(minimizedTaskId).isEqualTo(tasks.first().taskId)
         assertThat(wct.hierarchyOps.size).isEqualTo(1)
@@ -385,10 +456,11 @@
 
         val wct = WindowContainerTransaction()
         val minimizedTaskId =
-                desktopTasksLimiter.addAndGetMinimizeTaskChanges(
-                        displayId = 0,
-                        wct = wct,
-                        newFrontTaskId = setUpFreeformTask().taskId)
+            desktopTasksLimiter.addAndGetMinimizeTaskChanges(
+                displayId = 0,
+                wct = wct,
+                newFrontTaskId = setUpFreeformTask().taskId,
+            )
 
         assertThat(minimizedTaskId).isNull()
         assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
@@ -398,8 +470,8 @@
     fun getTaskToMinimize_tasksWithinLimit_returnsNull() {
         val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
 
-        val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize(
-                visibleOrderedTasks = tasks.map { it.taskId })
+        val minimizedTask =
+            desktopTasksLimiter.getTaskIdToMinimize(visibleOrderedTasks = tasks.map { it.taskId })
 
         assertThat(minimizedTask).isNull()
     }
@@ -408,8 +480,8 @@
     fun getTaskToMinimize_tasksAboveLimit_returnsBackTask() {
         val tasks = (1..MAX_TASK_LIMIT + 1).map { setUpFreeformTask() }
 
-        val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize(
-                visibleOrderedTasks = tasks.map { it.taskId })
+        val minimizedTask =
+            desktopTasksLimiter.getTaskIdToMinimize(visibleOrderedTasks = tasks.map { it.taskId })
 
         // first == front, last == back
         assertThat(minimizedTask).isEqualTo(tasks.last().taskId)
@@ -418,12 +490,19 @@
     @Test
     fun getTaskToMinimize_tasksAboveLimit_otherLimit_returnsBackTask() {
         desktopTasksLimiter =
-            DesktopTasksLimiter(transitions, userRepositories, shellTaskOrganizer, MAX_TASK_LIMIT2,
-                interactionJankMonitor, mContext, handler)
+            DesktopTasksLimiter(
+                transitions,
+                userRepositories,
+                shellTaskOrganizer,
+                MAX_TASK_LIMIT2,
+                interactionJankMonitor,
+                mContext,
+                handler,
+            )
         val tasks = (1..MAX_TASK_LIMIT2 + 1).map { setUpFreeformTask() }
 
-        val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize(
-            visibleOrderedTasks = tasks.map { it.taskId })
+        val minimizedTask =
+            desktopTasksLimiter.getTaskIdToMinimize(visibleOrderedTasks = tasks.map { it.taskId })
 
         // first == front, last == back
         assertThat(minimizedTask).isEqualTo(tasks.last().taskId)
@@ -433,9 +512,11 @@
     fun getTaskToMinimize_withNewTask_tasksAboveLimit_returnsBackTask() {
         val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
 
-        val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize(
+        val minimizedTask =
+            desktopTasksLimiter.getTaskIdToMinimize(
                 visibleOrderedTasks = tasks.map { it.taskId },
-                newTaskIdInFront = setUpFreeformTask().taskId)
+                newTaskIdInFront = setUpFreeformTask().taskId,
+            )
 
         // first == front, last == back
         assertThat(minimizedTask).isEqualTo(tasks.last().taskId)
@@ -444,10 +525,12 @@
     @Test
     fun getTaskToMinimize_tasksAtLimit_newIntentReturnsBackTask() {
         val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
-        val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize(
-            visibleOrderedTasks = tasks.map { it.taskId },
-            newTaskIdInFront = null,
-            launchingNewIntent = true)
+        val minimizedTask =
+            desktopTasksLimiter.getTaskIdToMinimize(
+                visibleOrderedTasks = tasks.map { it.taskId },
+                newTaskIdInFront = null,
+                launchingNewIntent = true,
+            )
 
         // first == front, last == back
         assertThat(minimizedTask).isEqualTo(tasks.last().taskId)
@@ -459,25 +542,28 @@
         val transition = Binder()
         val task = setUpFreeformTask()
         desktopTasksLimiter.addPendingMinimizeChange(
-            transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
-
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
             transition,
-            TransitionInfoBuilder(TRANSIT_OPEN).build(),
-            StubTransaction() /* startTransaction */,
-            StubTransaction() /* finishTransaction */)
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+        )
+
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
+                transition,
+                TransitionInfoBuilder(TRANSIT_OPEN).build(),
+                StubTransaction() /* startTransaction */,
+                StubTransaction(), /* finishTransaction */
+            )
 
         desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition)
 
-        verify(interactionJankMonitor).begin(
-            any(),
-            eq(mContext),
-            eq(handler),
-            eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
+        verify(interactionJankMonitor)
+            .begin(any(), eq(mContext), eq(handler), eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
 
-        desktopTasksLimiter.getTransitionObserver().onTransitionFinished(
-            transition,
-            /* aborted = */ false)
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionFinished(transition, /* aborted= */ false)
 
         verify(interactionJankMonitor).end(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
     }
@@ -488,26 +574,28 @@
         val transition = Binder()
         val task = setUpFreeformTask()
         desktopTasksLimiter.addPendingMinimizeChange(
-            transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
-
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
             transition,
-            TransitionInfoBuilder(TRANSIT_OPEN).build(),
-            StubTransaction() /* startTransaction */,
-            StubTransaction() /* finishTransaction */)
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+        )
+
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
+                transition,
+                TransitionInfoBuilder(TRANSIT_OPEN).build(),
+                StubTransaction() /* startTransaction */,
+                StubTransaction(), /* finishTransaction */
+            )
 
         desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition)
 
-        verify(interactionJankMonitor).begin(
-            any(),
-            eq(mContext),
-            eq(handler),
-            eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW),
-        )
+        verify(interactionJankMonitor)
+            .begin(any(), eq(mContext), eq(handler), eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
 
-        desktopTasksLimiter.getTransitionObserver().onTransitionFinished(
-            transition,
-            /* aborted = */ true)
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionFinished(transition, /* aborted= */ true)
 
         verify(interactionJankMonitor).cancel(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
     }
@@ -519,25 +607,28 @@
         val newTransition = Binder()
         val task = setUpFreeformTask()
         desktopTasksLimiter.addPendingMinimizeChange(
-            mergedTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
-
-        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
             mergedTransition,
-            TransitionInfoBuilder(TRANSIT_OPEN).build(),
-            StubTransaction() /* startTransaction */,
-            StubTransaction() /* finishTransaction */)
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+        )
+
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionReady(
+                mergedTransition,
+                TransitionInfoBuilder(TRANSIT_OPEN).build(),
+                StubTransaction() /* startTransaction */,
+                StubTransaction(), /* finishTransaction */
+            )
 
         desktopTasksLimiter.getTransitionObserver().onTransitionStarting(mergedTransition)
 
-        verify(interactionJankMonitor).begin(
-            any(),
-            eq(mContext),
-            eq(handler),
-            eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
+        verify(interactionJankMonitor)
+            .begin(any(), eq(mContext), eq(handler), eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
 
-        desktopTasksLimiter.getTransitionObserver().onTransitionMerged(
-            mergedTransition,
-            newTransition)
+        desktopTasksLimiter
+            .getTransitionObserver()
+            .onTransitionMerged(mergedTransition, newTransition)
 
         verify(interactionJankMonitor).end(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW))
     }
@@ -550,19 +641,11 @@
     }
 
     private fun markTaskVisible(task: RunningTaskInfo) {
-        desktopTaskRepo.updateTask(
-                task.displayId,
-                task.taskId,
-                isVisible = true
-        )
+        desktopTaskRepo.updateTask(task.displayId, task.taskId, isVisible = true)
     }
 
     private fun markTaskHidden(task: RunningTaskInfo) {
-        desktopTaskRepo.updateTask(
-                task.displayId,
-                task.taskId,
-                isVisible = false
-        )
+        desktopTaskRepo.updateTask(task.displayId, task.taskId, isVisible = false)
     }
 
     private companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index b31a3f5..c9623bc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -99,7 +99,7 @@
                 shellTaskOrganizer,
                 mixedHandler,
                 backAnimationController,
-                shellInit
+                shellInit,
             )
     }
 
@@ -139,7 +139,10 @@
         verify(taskRepository).minimizeTask(task.displayId, task.taskId)
         val pendingTransition =
             DesktopMixedTransitionHandler.PendingMixedTransition.Minimize(
-                transition, task.taskId, isLastTask = false)
+                transition,
+                task.taskId,
+                isLastTask = false,
+            )
         verify(mixedHandler).addPendingMixedTransition(pendingTransition)
     }
 
@@ -162,7 +165,10 @@
         verify(taskRepository).minimizeTask(task.displayId, task.taskId)
         val pendingTransition =
             DesktopMixedTransitionHandler.PendingMixedTransition.Minimize(
-                transition, task.taskId, isLastTask = true)
+                transition,
+                task.taskId,
+                isLastTask = true,
+            )
         verify(mixedHandler).addPendingMixedTransition(pendingTransition)
     }
 
@@ -251,7 +257,8 @@
                     parent = null
                     taskInfo = task
                     flags = flags
-                })
+                }
+            )
             if (withWallpaper) {
                 addChange(
                     Change(mock(), mock()).apply {
@@ -259,14 +266,15 @@
                         parent = null
                         taskInfo = createWallpaperTaskInfo()
                         flags = flags
-                    })
+                    }
+                )
             }
         }
     }
 
     private fun createOpenChangeTransition(
         task: RunningTaskInfo?,
-        type: Int = TRANSIT_OPEN
+        type: Int = TRANSIT_OPEN,
     ): TransitionInfo {
         return TransitionInfo(TRANSIT_OPEN, 0 /* flags */).apply {
             addChange(
@@ -275,7 +283,8 @@
                     parent = null
                     taskInfo = task
                     flags = flags
-                })
+                }
+            )
         }
     }
 
@@ -287,13 +296,14 @@
                     parent = null
                     taskInfo = task
                     flags = flags
-                })
+                }
+            )
         }
     }
 
     private fun getLatestWct(
         @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
-        handlerClass: Class<out Transitions.TransitionHandler>? = null
+        handlerClass: Class<out Transitions.TransitionHandler>? = null,
     ): WindowContainerTransaction {
         val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
         if (handlerClass == null) {
@@ -330,8 +340,6 @@
         RunningTaskInfo().apply {
             token = mock<WindowContainerToken>()
             baseIntent =
-                Intent().apply {
-                    component = DesktopWallpaperActivity.wallpaperActivityComponent
-                }
+                Intent().apply { component = DesktopWallpaperActivity.wallpaperActivityComponent }
         }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt
index a2e939d..b9e307fa5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt
@@ -82,20 +82,20 @@
         datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
         shellInit = spy(ShellInit(testExecutor))
 
-        val profiles: MutableList<UserInfo> = mutableListOf(
-            UserInfo(USER_ID_1, "User 1", 0),
-            UserInfo(PROFILE_ID_2, "Profile 2", 0))
+        val profiles: MutableList<UserInfo> =
+            mutableListOf(UserInfo(USER_ID_1, "User 1", 0), UserInfo(PROFILE_ID_2, "Profile 2", 0))
         whenever(userManager.getProfiles(USER_ID_1)).thenReturn(profiles)
 
-        userRepositories = DesktopUserRepositories(
-            context,
-            shellInit,
-            shellController,
-            persistentRepository,
-            repositoryInitializer,
-            datastoreScope,
-            userManager
-        )
+        userRepositories =
+            DesktopUserRepositories(
+                context,
+                shellInit,
+                shellController,
+                persistentRepository,
+                repositoryInitializer,
+                datastoreScope,
+                userManager,
+            )
     }
 
     @After
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 13528b9..e4eff9f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -61,9 +61,7 @@
 @RunWithLooper
 @RunWith(AndroidTestingRunner::class)
 class DragToDesktopTransitionHandlerTest : ShellTestCase() {
-    @JvmField
-    @Rule
-    val mAnimatorTestRule = AnimatorTestRule(this)
+    @JvmField @Rule val mAnimatorTestRule = AnimatorTestRule(this)
 
     @Mock private lateinit var transitions: Transitions
     @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@@ -123,11 +121,11 @@
             info =
                 createTransitionInfo(
                     type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
-                    draggedTask = task
+                    draggedTask = task,
                 ),
             startTransaction = mock(),
             finishTransaction = mock(),
-            finishCallback = {}
+            finishCallback = {},
         )
 
         verify(dragAnimator).startAnimation()
@@ -137,13 +135,13 @@
     fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() {
         performEarlyCancel(
             defaultHandler,
-            DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+            DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL,
         )
         verify(transitions)
             .startTransition(
                 eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP),
                 any(),
-                eq(defaultHandler)
+                eq(defaultHandler),
             )
     }
 
@@ -151,7 +149,7 @@
     fun startDragToDesktop_cancelledBeforeReady_verifySplitLeftCancel() {
         performEarlyCancel(
             defaultHandler,
-            DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT
+            DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT,
         )
         verify(splitScreenController)
             .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any())
@@ -161,7 +159,7 @@
     fun startDragToDesktop_cancelledBeforeReady_verifySplitRightCancel() {
         performEarlyCancel(
             defaultHandler,
-            DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT
+            DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT,
         )
         verify(splitScreenController)
             .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any())
@@ -214,7 +212,7 @@
             .startTransition(
                 eq(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP),
                 any(),
-                eq(defaultHandler)
+                eq(defaultHandler),
             )
     }
 
@@ -277,14 +275,18 @@
         val startToken = startDrag(defaultHandler)
 
         // Then user cancelled after it had already started.
-        val cancelToken = cancelDragToDesktopTransition(
-            defaultHandler, DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL)
+        val cancelToken =
+            cancelDragToDesktopTransition(
+                defaultHandler,
+                DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL,
+            )
         defaultHandler.mergeAnimation(
             cancelToken,
             TransitionInfo(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, 0),
             mock<SurfaceControl.Transaction>(),
             startToken,
-            mock<Transitions.TransitionFinishCallback>())
+            mock<Transitions.TransitionFinishCallback>(),
+        )
 
         // Cancel animation should run since it had already started.
         verify(dragAnimator).cancelAnimator()
@@ -296,8 +298,11 @@
         val startToken = startDrag(defaultHandler)
 
         // Then user cancelled after it had already started.
-        val cancelToken = cancelDragToDesktopTransition(
-            defaultHandler, DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL)
+        val cancelToken =
+            cancelDragToDesktopTransition(
+                defaultHandler,
+                DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL,
+            )
         defaultHandler.onTransitionConsumed(cancelToken, aborted = true, null)
 
         // Cancel animation should run since it had already started.
@@ -360,7 +365,7 @@
             .startTransition(
                 eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP),
                 any(),
-                eq(defaultHandler)
+                eq(defaultHandler),
             )
     }
 
@@ -374,7 +379,7 @@
             .startTransition(
                 eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP),
                 any(),
-                eq(defaultHandler)
+                eq(defaultHandler),
             )
     }
 
@@ -390,7 +395,7 @@
             info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = task),
             t = transaction,
             mergeTarget = mock(),
-            finishCallback = finishCallback
+            finishCallback = finishCallback,
         )
 
         // Should NOT have any transaction changes
@@ -414,11 +419,11 @@
             info =
                 createTransitionInfo(
                     type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
-                    draggedTask = task
+                    draggedTask = task,
                 ),
             t = mergedStartTransaction,
             mergeTarget = startTransition,
-            finishCallback = finishCallback
+            finishCallback = finishCallback,
         )
 
         // Should show dragged task layer in start and finish transaction
@@ -446,11 +451,11 @@
             info =
                 createTransitionInfo(
                     type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
-                    draggedTask = task
+                    draggedTask = task,
                 ),
             t = mergedStartTransaction,
             mergeTarget = startTransition,
-            finishCallback = finishCallback
+            finishCallback = finishCallback,
         )
 
         // Should show dragged task layer in start and finish transaction
@@ -475,7 +480,7 @@
         assertEquals(
             "Expects to return system properties stored value",
             /* expected= */ value,
-            /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name)
+            /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name),
         )
     }
 
@@ -491,7 +496,7 @@
         assertEquals(
             "Expects to return scaled system properties stored value",
             /* expected= */ value / scale,
-            /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name, scale = scale)
+            /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name, scale = scale),
         )
     }
 
@@ -508,8 +513,8 @@
             /* expected= */ defaultValue,
             /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(
                 name,
-                default = defaultValue
-            )
+                default = defaultValue,
+            ),
         )
     }
 
@@ -530,8 +535,8 @@
             /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(
                 name,
                 default = defaultValue,
-                scale = scale
-            )
+                scale = scale,
+            ),
         )
     }
 
@@ -542,8 +547,8 @@
         defaultHandler.onTransitionConsumed(transition, aborted = true, mock())
 
         verify(mockInteractionJankMonitor).cancel(eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD))
-        verify(mockInteractionJankMonitor, times(0)).cancel(
-            eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE))
+        verify(mockInteractionJankMonitor, times(0))
+            .cancel(eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE))
     }
 
     @Test
@@ -554,13 +559,14 @@
         defaultHandler.onTaskResizeAnimationListener = mock()
         defaultHandler.mergeAnimation(
             transition = endTransition,
-            info = createTransitionInfo(
-                type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
-                draggedTask = task
-            ),
+            info =
+                createTransitionInfo(
+                    type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+                    draggedTask = task,
+                ),
             t = mock<SurfaceControl.Transaction>(),
             mergeTarget = startTransition,
-            finishCallback = mock<Transitions.TransitionFinishCallback>()
+            finishCallback = mock<Transitions.TransitionFinishCallback>(),
         )
 
         defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock())
@@ -574,7 +580,7 @@
     private fun startDrag(
         handler: DragToDesktopTransitionHandler,
         task: RunningTaskInfo = createTask(),
-        finishTransaction: SurfaceControl.Transaction = mock()
+        finishTransaction: SurfaceControl.Transaction = mock(),
     ): IBinder {
         whenever(dragAnimator.position).thenReturn(PointF())
         // Simulate transition is started and is ready to animate.
@@ -584,11 +590,11 @@
             info =
                 createTransitionInfo(
                     type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
-                    draggedTask = task
+                    draggedTask = task,
                 ),
             startTransaction = mock(),
             finishTransaction = finishTransaction,
-            finishCallback = {}
+            finishCallback = {},
         )
         return transition
     }
@@ -596,14 +602,14 @@
     private fun startDragToDesktopTransition(
         handler: DragToDesktopTransitionHandler,
         task: RunningTaskInfo,
-        dragAnimator: MoveToDesktopAnimator
+        dragAnimator: MoveToDesktopAnimator,
     ): IBinder {
         val token = mock<IBinder>()
         whenever(
                 transitions.startTransition(
                     eq(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP),
                     any(),
-                    eq(handler)
+                    eq(handler),
                 )
             )
             .thenReturn(token)
@@ -613,13 +619,14 @@
 
     private fun cancelDragToDesktopTransition(
         handler: DragToDesktopTransitionHandler,
-        cancelState: DragToDesktopTransitionHandler.CancelState): IBinder {
+        cancelState: DragToDesktopTransitionHandler.CancelState,
+    ): IBinder {
         val token = mock<IBinder>()
         whenever(
                 transitions.startTransition(
                     eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP),
                     any(),
-                    eq(handler)
+                    eq(handler),
                 )
             )
             .thenReturn(token)
@@ -630,7 +637,7 @@
 
     private fun performEarlyCancel(
         handler: DragToDesktopTransitionHandler,
-        cancelState: DragToDesktopTransitionHandler.CancelState
+        cancelState: DragToDesktopTransitionHandler.CancelState,
     ) {
         val task = createTask()
         // Simulate transition is started and is ready to animate.
@@ -643,11 +650,11 @@
             info =
                 createTransitionInfo(
                     type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
-                    draggedTask = task
+                    draggedTask = task,
                 ),
             startTransaction = mock(),
             finishTransaction = mock(),
-            finishCallback = {}
+            finishCallback = {},
         )
 
         // Don't even animate the "drag" since it was already cancelled.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
index 38c6ed9..e102539 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
@@ -31,67 +31,71 @@
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class WindowDecorCaptionHandleRepositoryTest {
-  private lateinit var captionHandleRepository: WindowDecorCaptionHandleRepository
+    private lateinit var captionHandleRepository: WindowDecorCaptionHandleRepository
 
-  @Before
-  fun setUp() {
-    captionHandleRepository = WindowDecorCaptionHandleRepository()
-  }
+    @Before
+    fun setUp() {
+        captionHandleRepository = WindowDecorCaptionHandleRepository()
+    }
 
-  @Test
-  fun initialState_noAction_returnsNoCaption() {
-    // Check the initial value of `captionStateFlow`.
-    assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
-  }
+    @Test
+    fun initialState_noAction_returnsNoCaption() {
+        // Check the initial value of `captionStateFlow`.
+        assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
+    }
 
-  @Test
-  fun notifyCaptionChange_toAppHandleVisible_updatesStateWithCorrectData() {
-    val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME)
-    val appHandleCaptionState =
-        CaptionState.AppHandle(
-          runningTaskInfo = taskInfo,
-          isHandleMenuExpanded = false,
-          globalAppHandleBounds = Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3),
-          isCapturedLinkAvailable = false)
+    @Test
+    fun notifyCaptionChange_toAppHandleVisible_updatesStateWithCorrectData() {
+        val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME)
+        val appHandleCaptionState =
+            CaptionState.AppHandle(
+                runningTaskInfo = taskInfo,
+                isHandleMenuExpanded = false,
+                globalAppHandleBounds =
+                    Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3),
+                isCapturedLinkAvailable = false,
+            )
 
-    captionHandleRepository.notifyCaptionChanged(appHandleCaptionState)
+        captionHandleRepository.notifyCaptionChanged(appHandleCaptionState)
 
-    assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHandleCaptionState)
-  }
+        assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHandleCaptionState)
+    }
 
-  @Test
-  fun notifyCaptionChange_toAppChipVisible_updatesStateWithCorrectData() {
-    val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME)
-    val appHeaderCaptionState =
-        CaptionState.AppHeader(
-          runningTaskInfo = taskInfo,
-          isHeaderMenuExpanded = true,
-          globalAppChipBounds = Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3),
-          isCapturedLinkAvailable = false)
+    @Test
+    fun notifyCaptionChange_toAppChipVisible_updatesStateWithCorrectData() {
+        val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME)
+        val appHeaderCaptionState =
+            CaptionState.AppHeader(
+                runningTaskInfo = taskInfo,
+                isHeaderMenuExpanded = true,
+                globalAppChipBounds =
+                    Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3),
+                isCapturedLinkAvailable = false,
+            )
 
-    captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState)
+        captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState)
 
-    assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHeaderCaptionState)
-  }
+        assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHeaderCaptionState)
+    }
 
-  @Test
-  fun notifyCaptionChange_toNoCaption_updatesState() {
-    captionHandleRepository.notifyCaptionChanged(CaptionState.NoCaption)
+    @Test
+    fun notifyCaptionChange_toNoCaption_updatesState() {
+        captionHandleRepository.notifyCaptionChanged(CaptionState.NoCaption)
 
-    assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
-  }
+        assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
+    }
 
-  private fun createTaskInfo(
-      deviceWindowingMode: Int = WINDOWING_MODE_UNDEFINED,
-      runningTaskPackageName: String = LAUNCHER_PACKAGE_NAME
-  ): RunningTaskInfo =
-      RunningTaskInfo().apply {
-        configuration.windowConfiguration.apply { windowingMode = deviceWindowingMode }
-        topActivityInfo?.apply { packageName = runningTaskPackageName }
-      }
+    private fun createTaskInfo(
+        deviceWindowingMode: Int = WINDOWING_MODE_UNDEFINED,
+        runningTaskPackageName: String = LAUNCHER_PACKAGE_NAME,
+    ): RunningTaskInfo =
+        RunningTaskInfo().apply {
+            configuration.windowConfiguration.apply { windowingMode = deviceWindowingMode }
+            topActivityInfo?.apply { packageName = runningTaskPackageName }
+        }
 
-  private companion object {
-    const val GMAIL_PACKAGE_NAME = "com.google.android.gm"
-    const val LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher"
-  }
+    private companion object {
+        const val GMAIL_PACKAGE_NAME = "com.google.android.gm"
+        const val LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher"
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt
index c33005e..1569f9d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt
@@ -25,10 +25,10 @@
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.desktopmode.DesktopRepository
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTaskBuilder
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createSystemModalTask
-import com.android.wm.shell.desktopmode.DesktopRepository
 import com.android.wm.shell.desktopmode.DesktopUserRepositories
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.TransitionInfoBuilder
@@ -44,8 +44,8 @@
 import org.mockito.kotlin.whenever
 
 /**
- * Tests for {@link SystemModalsTransitionHandler}
- * Usage: atest WMShellUnitTests:SystemModalsTransitionHandlerTest
+ * Tests for {@link SystemModalsTransitionHandler} Usage: atest
+ * WMShellUnitTests:SystemModalsTransitionHandlerTest
  */
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
index 9c00c0c..5475032 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
@@ -69,431 +69,448 @@
 @RunWith(AndroidTestingRunner::class)
 @OptIn(ExperimentalCoroutinesApi::class)
 class AppHandleEducationControllerTest : ShellTestCase() {
-  @JvmField
-  @Rule
-  val extendedMockitoRule =
-      ExtendedMockitoRule.Builder(this)
-          .mockStatic(DesktopModeStatus::class.java)
-          .mockStatic(SystemProperties::class.java)
-          .build()!!
-  @JvmField @Rule val setFlagsRule = SetFlagsRule()
+    @JvmField
+    @Rule
+    val extendedMockitoRule =
+        ExtendedMockitoRule.Builder(this)
+            .mockStatic(DesktopModeStatus::class.java)
+            .mockStatic(SystemProperties::class.java)
+            .build()!!
+    @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
-  private lateinit var educationController: AppHandleEducationController
-  private lateinit var testableContext: TestableContext
-  private val testScope = TestScope()
-  private val testDataStoreFlow = MutableStateFlow(createWindowingEducationProto())
-  private val testCaptionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption)
-  private val educationConfigCaptor =
-      argumentCaptor<DesktopWindowingEducationTooltipController.TooltipEducationViewConfig>()
-  @Mock private lateinit var mockEducationFilter: AppHandleEducationFilter
-  @Mock private lateinit var mockDataStoreRepository: AppHandleEducationDatastoreRepository
-  @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
-  @Mock private lateinit var mockTooltipController: DesktopWindowingEducationTooltipController
+    private lateinit var educationController: AppHandleEducationController
+    private lateinit var testableContext: TestableContext
+    private val testScope = TestScope()
+    private val testDataStoreFlow = MutableStateFlow(createWindowingEducationProto())
+    private val testCaptionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption)
+    private val educationConfigCaptor =
+        argumentCaptor<DesktopWindowingEducationTooltipController.TooltipEducationViewConfig>()
+    @Mock private lateinit var mockEducationFilter: AppHandleEducationFilter
+    @Mock private lateinit var mockDataStoreRepository: AppHandleEducationDatastoreRepository
+    @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
+    @Mock private lateinit var mockTooltipController: DesktopWindowingEducationTooltipController
 
-  @Before
-  fun setUp() {
-    MockitoAnnotations.initMocks(this)
-    Dispatchers.setMain(StandardTestDispatcher(testScope.testScheduler))
-    testableContext = TestableContext(mContext)
-    whenever(mockDataStoreRepository.dataStoreFlow).thenReturn(testDataStoreFlow)
-    whenever(mockCaptionHandleRepository.captionStateFlow).thenReturn(testCaptionStateFlow)
-    whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        Dispatchers.setMain(StandardTestDispatcher(testScope.testScheduler))
+        testableContext = TestableContext(mContext)
+        whenever(mockDataStoreRepository.dataStoreFlow).thenReturn(testDataStoreFlow)
+        whenever(mockCaptionHandleRepository.captionStateFlow).thenReturn(testCaptionStateFlow)
+        whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
 
-    educationController =
-        AppHandleEducationController(
-            testableContext,
-            mockEducationFilter,
-            mockDataStoreRepository,
-            mockCaptionHandleRepository,
-            mockTooltipController,
-            testScope.backgroundScope,
-            Dispatchers.Main)
-  }
+        educationController =
+            AppHandleEducationController(
+                testableContext,
+                mockEducationFilter,
+                mockDataStoreRepository,
+                mockCaptionHandleRepository,
+                mockTooltipController,
+                testScope.backgroundScope,
+                Dispatchers.Main,
+            )
+    }
 
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  fun init_appHandleVisible_shouldCallShowEducationTooltip() =
-      testScope.runTest {
-        // App handle is visible. Should show education tooltip.
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun init_appHandleVisible_shouldCallShowEducationTooltip() =
+        testScope.runTest {
+            // App handle is visible. Should show education tooltip.
+            setShouldShowAppHandleEducation(true)
+
+            // Simulate app handle visible.
+            testCaptionStateFlow.value = createAppHandleState()
+            // Wait for first tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun init_flagDisabled_shouldNotCallShowEducationTooltip() =
+        testScope.runTest {
+            // App handle visible but education aconfig flag disabled, should not show education
+            // tooltip.
+            whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false)
+            setShouldShowAppHandleEducation(true)
+
+            // Simulate app handle visible.
+            testCaptionStateFlow.value = createAppHandleState()
+            // Wait for first tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, never()).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun init_shouldShowAppHandleEducationReturnsFalse_shouldNotCallShowEducationTooltip() =
+        testScope.runTest {
+            // App handle is visible but [shouldShowAppHandleEducation] api returns false, should
+            // not
+            // show education tooltip.
+            setShouldShowAppHandleEducation(false)
+
+            // Simulate app handle visible.
+            testCaptionStateFlow.value = createAppHandleState()
+            // Wait for first tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, never()).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun init_appHandleNotVisible_shouldNotCallShowEducationTooltip() =
+        testScope.runTest {
+            // App handle is not visible, should not show education tooltip.
+            setShouldShowAppHandleEducation(true)
+
+            // Simulate app handle is not visible.
+            testCaptionStateFlow.value = CaptionState.NoCaption
+            // Wait for first tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, never()).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun init_appHandleHintViewedAlready_shouldNotCallShowEducationTooltip() =
+        testScope.runTest {
+            // App handle is visible but app handle hint has been viewed before,
+            // should not show education tooltip.
+            // Mark app handle hint viewed.
+            testDataStoreFlow.value =
+                createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
+            setShouldShowAppHandleEducation(true)
+
+            // Simulate app handle visible.
+            testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
+            // Wait for first tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, never()).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() =
+        testScope.runTest {
+            // App handle is visible but app handle hint has been viewed before.
+            // But as we are overriding prerequisite conditions, we should show app
+            // handle tooltip.
+            // Mark app handle hint viewed.
+            testDataStoreFlow.value =
+                createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
+            val systemPropertiesKey =
+                "persist.desktop_windowing_app_handle_education_override_conditions"
+            whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean()))
+                .thenReturn(true)
+            setShouldShowAppHandleEducation(true)
+
+            // Simulate app handle visible.
+            testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
+            // Wait for first tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun init_appHandleExpanded_shouldMarkAppHandleHintUsed() =
+        testScope.runTest {
+            setShouldShowAppHandleEducation(false)
+
+            // Simulate app handle visible and expanded.
+            testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+            // Wait for some time before verifying
+            waitForBufferDelay()
+
+            verify(mockDataStoreRepository, times(1))
+                .updateAppHandleHintUsedTimestampMillis(eq(true))
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun init_showFirstTooltip_shouldMarkAppHandleHintViewed() =
+        testScope.runTest {
+            // App handle is visible. Should show education tooltip.
+            setShouldShowAppHandleEducation(true)
+
+            // Simulate app handle visible.
+            testCaptionStateFlow.value = createAppHandleState()
+            // Wait for first tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockDataStoreRepository, times(1))
+                .updateAppHandleHintViewedTimestampMillis(eq(true))
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("b/371527084: revisit testcase after refactoring original logic")
+    fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() =
+        testScope.runTest {
+            // After first tooltip is dismissed, app handle is expanded. Should show second
+            // education
+            // tooltip.
+            showAndDismissFirstTooltip()
+
+            // Simulate app handle expanded.
+            testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+
+            // [showEducationTooltip] should be called twice, once for each tooltip.
+            verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("b/371527084: revisit testcase after refactoring original logic")
+    fun showWindowingImageButtonTooltip_appHandleExpandedAfterTimeout_shouldCallShowEducationTooltipOnce() =
+        testScope.runTest {
+            // After first tooltip is dismissed, app handle is expanded after timeout. Should not
+            // show
+            // second education tooltip.
+            showAndDismissFirstTooltip()
+
+            // Wait for timeout to occur, after this timeout we should not listen for further
+            // triggers
+            // anymore.
+            advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS)
+            runCurrent()
+            // Simulate app handle expanded.
+            testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+
+            // [showEducationTooltip] should be called once, just for the first tooltip.
+            verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("b/371527084: revisit testcase after refactoring original logic")
+    fun showWindowingImageButtonTooltip_appHandleExpandedTwice_shouldCallShowEducationTooltipTwice() =
+        testScope.runTest {
+            // After first tooltip is dismissed, app handle is expanded twice. Should show second
+            // education tooltip only once.
+            showAndDismissFirstTooltip()
+
+            // Simulate app handle expanded.
+            testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+            // Simulate app handle being expanded twice.
+            testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+            waitForBufferDelay()
+
+            // [showEducationTooltip] should not be called thrice, even if app handle was expanded
+            // twice. Should be called twice, once for each tooltip.
+            verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("b/371527084: revisit testcase after refactoring original logic")
+    fun showWindowingImageButtonTooltip_appHandleNotExpanded_shouldCallShowEducationTooltipOnce() =
+        testScope.runTest {
+            // After first tooltip is dismissed, app handle is not expanded. Should not show second
+            // education tooltip.
+            showAndDismissFirstTooltip()
+
+            // Simulate app handle visible but not expanded.
+            testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+
+            // [showEducationTooltip] should be called once, just for the first tooltip.
+            verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("b/371527084: revisit testcase after refactoring original logic")
+    fun showExitWindowingButtonTooltip_appHeaderVisible_shouldCallShowEducationTooltipThrice() =
+        testScope.runTest {
+            // After first two tooltips are dismissed, app header is visible. Should show third
+            // education tooltip.
+            showAndDismissFirstTooltip()
+            showAndDismissSecondTooltip()
+
+            // Simulate app header visible.
+            testCaptionStateFlow.value = createAppHeaderState()
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, times(3)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("b/371527084: revisit testcase after refactoring original logic")
+    fun showExitWindowingButtonTooltip_appHeaderVisibleAfterTimeout_shouldCallShowEducationTooltipTwice() =
+        testScope.runTest {
+            // After first two tooltips are dismissed, app header is visible after timeout. Should
+            // not
+            // show third education tooltip.
+            showAndDismissFirstTooltip()
+            showAndDismissSecondTooltip()
+
+            // Wait for timeout to occur, after this timeout we should not listen for further
+            // triggers
+            // anymore.
+            advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS)
+            runCurrent()
+            // Simulate app header visible.
+            testCaptionStateFlow.value = createAppHeaderState()
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("b/371527084: revisit testcase after refactoring original logic")
+    fun showExitWindowingButtonTooltip_appHeaderVisibleTwice_shouldCallShowEducationTooltipThrice() =
+        testScope.runTest {
+            // After first two tooltips are dismissed, app header is visible twice. Should show
+            // third
+            // education tooltip only once.
+            showAndDismissFirstTooltip()
+            showAndDismissSecondTooltip()
+
+            // Simulate app header visible.
+            testCaptionStateFlow.value = createAppHeaderState()
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+            testCaptionStateFlow.value = createAppHeaderState()
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, times(3)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("b/371527084: revisit testcase after refactoring original logic")
+    fun showExitWindowingButtonTooltip_appHeaderExpanded_shouldCallShowEducationTooltipTwice() =
+        testScope.runTest {
+            // After first two tooltips are dismissed, app header is visible but expanded. Should
+            // not
+            // show third education tooltip.
+            showAndDismissFirstTooltip()
+            showAndDismissSecondTooltip()
+
+            // Simulate app header visible.
+            testCaptionStateFlow.value = createAppHeaderState(isHeaderMenuExpanded = true)
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() =
+        testScope.runTest {
+            // App handle is visible. Should show education tooltip.
+            setShouldShowAppHandleEducation(true)
+            val mockOpenHandleMenuCallback: (Int) -> Unit = mock()
+            val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock()
+            educationController.setAppHandleEducationTooltipCallbacks(
+                mockOpenHandleMenuCallback,
+                mockToDesktopModeCallback,
+            )
+            // Simulate app handle visible.
+            testCaptionStateFlow.value = createAppHandleState()
+            // Wait for first tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, atLeastOnce())
+                .showEducationTooltip(educationConfigCaptor.capture(), any())
+            educationConfigCaptor.lastValue.onEducationClickAction.invoke()
+
+            verify(mockOpenHandleMenuCallback, times(1)).invoke(any())
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+    @Ignore("b/371527084: revisit testcase after refactoring original logic")
+    fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() =
+        testScope.runTest {
+            // After first tooltip is dismissed, app handle is expanded. Should show second
+            // education
+            // tooltip.
+            showAndDismissFirstTooltip()
+            val mockOpenHandleMenuCallback: (Int) -> Unit = mock()
+            val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock()
+            educationController.setAppHandleEducationTooltipCallbacks(
+                mockOpenHandleMenuCallback,
+                mockToDesktopModeCallback,
+            )
+            // Simulate app handle expanded.
+            testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+            // Wait for next tooltip to showup.
+            waitForBufferDelay()
+
+            verify(mockTooltipController, atLeastOnce())
+                .showEducationTooltip(educationConfigCaptor.capture(), any())
+            educationConfigCaptor.lastValue.onEducationClickAction.invoke()
+
+            verify(mockToDesktopModeCallback, times(1)).invoke(any(), any())
+        }
+
+    private suspend fun TestScope.showAndDismissFirstTooltip() {
         setShouldShowAppHandleEducation(true)
-
         // Simulate app handle visible.
-        testCaptionStateFlow.value = createAppHandleState()
+        testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
         // Wait for first tooltip to showup.
         waitForBufferDelay()
-
-        verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  fun init_flagDisabled_shouldNotCallShowEducationTooltip() =
-      testScope.runTest {
-        // App handle visible but education aconfig flag disabled, should not show education
-        // tooltip.
-        whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false)
-        setShouldShowAppHandleEducation(true)
-
-        // Simulate app handle visible.
-        testCaptionStateFlow.value = createAppHandleState()
-        // Wait for first tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockTooltipController, never()).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  fun init_shouldShowAppHandleEducationReturnsFalse_shouldNotCallShowEducationTooltip() =
-      testScope.runTest {
-        // App handle is visible but [shouldShowAppHandleEducation] api returns false, should not
-        // show education tooltip.
+        // [shouldShowAppHandleEducation] should return false as education has been viewed
+        // before.
         setShouldShowAppHandleEducation(false)
+        // Dismiss previous tooltip, after this we should listen for next tooltip's trigger.
+        captureAndInvokeOnDismissAction()
+    }
 
-        // Simulate app handle visible.
-        testCaptionStateFlow.value = createAppHandleState()
-        // Wait for first tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockTooltipController, never()).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  fun init_appHandleNotVisible_shouldNotCallShowEducationTooltip() =
-      testScope.runTest {
-        // App handle is not visible, should not show education tooltip.
-        setShouldShowAppHandleEducation(true)
-
-        // Simulate app handle is not visible.
-        testCaptionStateFlow.value = CaptionState.NoCaption
-        // Wait for first tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockTooltipController, never()).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  fun init_appHandleHintViewedAlready_shouldNotCallShowEducationTooltip() =
-      testScope.runTest {
-        // App handle is visible but app handle hint has been viewed before,
-        // should not show education tooltip.
-        // Mark app handle hint viewed.
-        testDataStoreFlow.value =
-            createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
-        setShouldShowAppHandleEducation(true)
-
-        // Simulate app handle visible.
-        testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
-        // Wait for first tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockTooltipController, never()).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() =
-      testScope.runTest {
-        // App handle is visible but app handle hint has been viewed before.
-        // But as we are overriding prerequisite conditions, we should show app
-        // handle tooltip.
-        // Mark app handle hint viewed.
-        testDataStoreFlow.value =
-            createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
-        val systemPropertiesKey =
-            "persist.desktop_windowing_app_handle_education_override_conditions"
-        whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean()))
-            .thenReturn(true)
-        setShouldShowAppHandleEducation(true)
-
-        // Simulate app handle visible.
-        testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
-        // Wait for first tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  fun init_appHandleExpanded_shouldMarkAppHandleHintUsed() =
-      testScope.runTest {
-        setShouldShowAppHandleEducation(false)
-
-        // Simulate app handle visible and expanded.
-        testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
-        // Wait for some time before verifying
-        waitForBufferDelay()
-
-        verify(mockDataStoreRepository, times(1)).updateAppHandleHintUsedTimestampMillis(eq(true))
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  fun init_showFirstTooltip_shouldMarkAppHandleHintViewed() =
-      testScope.runTest {
-        // App handle is visible. Should show education tooltip.
-        setShouldShowAppHandleEducation(true)
-
-        // Simulate app handle visible.
-        testCaptionStateFlow.value = createAppHandleState()
-        // Wait for first tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockDataStoreRepository, times(1)).updateAppHandleHintViewedTimestampMillis(eq(true))
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  @Ignore("b/371527084: revisit testcase after refactoring original logic")
-  fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() =
-      testScope.runTest {
-        // After first tooltip is dismissed, app handle is expanded. Should show second education
-        // tooltip.
-        showAndDismissFirstTooltip()
-
+    private fun TestScope.showAndDismissSecondTooltip() {
         // Simulate app handle expanded.
         testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
         // Wait for next tooltip to showup.
         waitForBufferDelay()
+        // Dismiss previous tooltip, after this we should listen for next tooltip's trigger.
+        captureAndInvokeOnDismissAction()
+    }
 
-        // [showEducationTooltip] should be called twice, once for each tooltip.
-        verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  @Ignore("b/371527084: revisit testcase after refactoring original logic")
-  fun showWindowingImageButtonTooltip_appHandleExpandedAfterTimeout_shouldCallShowEducationTooltipOnce() =
-      testScope.runTest {
-        // After first tooltip is dismissed, app handle is expanded after timeout. Should not show
-        // second education tooltip.
-        showAndDismissFirstTooltip()
-
-        // Wait for timeout to occur, after this timeout we should not listen for further triggers
-        // anymore.
-        advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS)
-        runCurrent()
-        // Simulate app handle expanded.
-        testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
-        // Wait for next tooltip to showup.
-        waitForBufferDelay()
-
-        // [showEducationTooltip] should be called once, just for the first tooltip.
-        verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  @Ignore("b/371527084: revisit testcase after refactoring original logic")
-  fun showWindowingImageButtonTooltip_appHandleExpandedTwice_shouldCallShowEducationTooltipTwice() =
-      testScope.runTest {
-        // After first tooltip is dismissed, app handle is expanded twice. Should show second
-        // education tooltip only once.
-        showAndDismissFirstTooltip()
-
-        // Simulate app handle expanded.
-        testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
-        // Wait for next tooltip to showup.
-        waitForBufferDelay()
-        // Simulate app handle being expanded twice.
-        testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
-        waitForBufferDelay()
-
-        // [showEducationTooltip] should not be called thrice, even if app handle was expanded
-        // twice. Should be called twice, once for each tooltip.
-        verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  @Ignore("b/371527084: revisit testcase after refactoring original logic")
-  fun showWindowingImageButtonTooltip_appHandleNotExpanded_shouldCallShowEducationTooltipOnce() =
-      testScope.runTest {
-        // After first tooltip is dismissed, app handle is not expanded. Should not show second
-        // education tooltip.
-        showAndDismissFirstTooltip()
-
-        // Simulate app handle visible but not expanded.
-        testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
-        // Wait for next tooltip to showup.
-        waitForBufferDelay()
-
-        // [showEducationTooltip] should be called once, just for the first tooltip.
-        verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  @Ignore("b/371527084: revisit testcase after refactoring original logic")
-  fun showExitWindowingButtonTooltip_appHeaderVisible_shouldCallShowEducationTooltipThrice() =
-      testScope.runTest {
-        // After first two tooltips are dismissed, app header is visible. Should show third
-        // education tooltip.
-        showAndDismissFirstTooltip()
-        showAndDismissSecondTooltip()
-
-        // Simulate app header visible.
-        testCaptionStateFlow.value = createAppHeaderState()
-        // Wait for next tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockTooltipController, times(3)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  @Ignore("b/371527084: revisit testcase after refactoring original logic")
-  fun showExitWindowingButtonTooltip_appHeaderVisibleAfterTimeout_shouldCallShowEducationTooltipTwice() =
-      testScope.runTest {
-        // After first two tooltips are dismissed, app header is visible after timeout. Should not
-        // show third education tooltip.
-        showAndDismissFirstTooltip()
-        showAndDismissSecondTooltip()
-
-        // Wait for timeout to occur, after this timeout we should not listen for further triggers
-        // anymore.
-        advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS)
-        runCurrent()
-        // Simulate app header visible.
-        testCaptionStateFlow.value = createAppHeaderState()
-        // Wait for next tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  @Ignore("b/371527084: revisit testcase after refactoring original logic")
-  fun showExitWindowingButtonTooltip_appHeaderVisibleTwice_shouldCallShowEducationTooltipThrice() =
-      testScope.runTest {
-        // After first two tooltips are dismissed, app header is visible twice. Should show third
-        // education tooltip only once.
-        showAndDismissFirstTooltip()
-        showAndDismissSecondTooltip()
-
-        // Simulate app header visible.
-        testCaptionStateFlow.value = createAppHeaderState()
-        // Wait for next tooltip to showup.
-        waitForBufferDelay()
-        testCaptionStateFlow.value = createAppHeaderState()
-        // Wait for next tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockTooltipController, times(3)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  @Ignore("b/371527084: revisit testcase after refactoring original logic")
-  fun showExitWindowingButtonTooltip_appHeaderExpanded_shouldCallShowEducationTooltipTwice() =
-      testScope.runTest {
-        // After first two tooltips are dismissed, app header is visible but expanded. Should not
-        // show third education tooltip.
-        showAndDismissFirstTooltip()
-        showAndDismissSecondTooltip()
-
-        // Simulate app header visible.
-        testCaptionStateFlow.value = createAppHeaderState(isHeaderMenuExpanded = true)
-        // Wait for next tooltip to showup.
-        waitForBufferDelay()
-
-        verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
-      }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() =
-      testScope.runTest {
-        // App handle is visible. Should show education tooltip.
-        setShouldShowAppHandleEducation(true)
-        val mockOpenHandleMenuCallback: (Int) -> Unit = mock()
-        val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock()
-        educationController.setAppHandleEducationTooltipCallbacks(
-            mockOpenHandleMenuCallback, mockToDesktopModeCallback)
-        // Simulate app handle visible.
-        testCaptionStateFlow.value = createAppHandleState()
-        // Wait for first tooltip to showup.
-        waitForBufferDelay()
-
+    private fun captureAndInvokeOnDismissAction() {
         verify(mockTooltipController, atLeastOnce())
             .showEducationTooltip(educationConfigCaptor.capture(), any())
-        educationConfigCaptor.lastValue.onEducationClickAction.invoke()
+        educationConfigCaptor.lastValue.onDismissAction.invoke()
+    }
 
-        verify(mockOpenHandleMenuCallback, times(1)).invoke(any())
-      }
+    private suspend fun setShouldShowAppHandleEducation(shouldShowAppHandleEducation: Boolean) =
+        whenever(mockEducationFilter.shouldShowAppHandleEducation(any()))
+            .thenReturn(shouldShowAppHandleEducation)
 
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
-  @Ignore("b/371527084: revisit testcase after refactoring original logic")
-  fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() =
-      testScope.runTest {
-        // After first tooltip is dismissed, app handle is expanded. Should show second education
-        // tooltip.
-        showAndDismissFirstTooltip()
-        val mockOpenHandleMenuCallback: (Int) -> Unit = mock()
-        val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock()
-        educationController.setAppHandleEducationTooltipCallbacks(
-            mockOpenHandleMenuCallback, mockToDesktopModeCallback)
-        // Simulate app handle expanded.
-        testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
-        // Wait for next tooltip to showup.
-        waitForBufferDelay()
+    /**
+     * Class under test waits for some time before showing education, simulate advance time before
+     * verifying or moving forward
+     */
+    private fun TestScope.waitForBufferDelay() {
+        advanceTimeBy(APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS)
+        runCurrent()
+    }
 
-        verify(mockTooltipController, atLeastOnce())
-            .showEducationTooltip(educationConfigCaptor.capture(), any())
-        educationConfigCaptor.lastValue.onEducationClickAction.invoke()
-
-        verify(mockToDesktopModeCallback, times(1)).invoke(any(), any())
-      }
-
-  private suspend fun TestScope.showAndDismissFirstTooltip() {
-    setShouldShowAppHandleEducation(true)
-    // Simulate app handle visible.
-    testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
-    // Wait for first tooltip to showup.
-    waitForBufferDelay()
-    // [shouldShowAppHandleEducation] should return false as education has been viewed
-    // before.
-    setShouldShowAppHandleEducation(false)
-    // Dismiss previous tooltip, after this we should listen for next tooltip's trigger.
-    captureAndInvokeOnDismissAction()
-  }
-
-  private fun TestScope.showAndDismissSecondTooltip() {
-    // Simulate app handle expanded.
-    testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
-    // Wait for next tooltip to showup.
-    waitForBufferDelay()
-    // Dismiss previous tooltip, after this we should listen for next tooltip's trigger.
-    captureAndInvokeOnDismissAction()
-  }
-
-  private fun captureAndInvokeOnDismissAction() {
-    verify(mockTooltipController, atLeastOnce())
-        .showEducationTooltip(educationConfigCaptor.capture(), any())
-    educationConfigCaptor.lastValue.onDismissAction.invoke()
-  }
-
-  private suspend fun setShouldShowAppHandleEducation(shouldShowAppHandleEducation: Boolean) =
-      whenever(mockEducationFilter.shouldShowAppHandleEducation(any()))
-          .thenReturn(shouldShowAppHandleEducation)
-
-  /**
-   * Class under test waits for some time before showing education, simulate advance time before
-   * verifying or moving forward
-   */
-  private fun TestScope.waitForBufferDelay() {
-    advanceTimeBy(APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS)
-    runCurrent()
-  }
-
-  private companion object {
-    val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long = APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L
-    val APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS: Long =
-        APP_HANDLE_EDUCATION_TIMEOUT_MILLIS + 1000L
-  }
+    private companion object {
+        val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long =
+            APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L
+        val APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS: Long =
+            APP_HANDLE_EDUCATION_TIMEOUT_MILLIS + 1000L
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
index 963890d..4db883d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
@@ -49,85 +49,89 @@
 @RunWith(AndroidTestingRunner::class)
 @ExperimentalCoroutinesApi
 class AppHandleEducationDatastoreRepositoryTest {
-  private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
-  private lateinit var testDatastore: DataStore<WindowingEducationProto>
-  private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository
-  private lateinit var datastoreScope: CoroutineScope
+    private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
+    private lateinit var testDatastore: DataStore<WindowingEducationProto>
+    private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository
+    private lateinit var datastoreScope: CoroutineScope
 
-  @Before
-  fun setUp() {
-    Dispatchers.setMain(StandardTestDispatcher())
-    datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
-    testDatastore =
-        DataStoreFactory.create(
-            serializer =
-                AppHandleEducationDatastoreRepository.Companion.WindowingEducationProtoSerializer,
-            scope = datastoreScope) {
-              testContext.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE)
+    @Before
+    fun setUp() {
+        Dispatchers.setMain(StandardTestDispatcher())
+        datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
+        testDatastore =
+            DataStoreFactory.create(
+                serializer =
+                    AppHandleEducationDatastoreRepository.Companion
+                        .WindowingEducationProtoSerializer,
+                scope = datastoreScope,
+            ) {
+                testContext.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE)
             }
-    datastoreRepository = AppHandleEducationDatastoreRepository(testDatastore)
-  }
+        datastoreRepository = AppHandleEducationDatastoreRepository(testDatastore)
+    }
 
-  @After
-  fun tearDown() {
-    File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore")
-        .deleteRecursively()
+    @After
+    fun tearDown() {
+        File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore")
+            .deleteRecursively()
 
-    datastoreScope.cancel()
-  }
+        datastoreScope.cancel()
+    }
 
-  @Test
-  fun getWindowingEducationProto_returnsCorrectProto() =
-      runTest(StandardTestDispatcher()) {
-        val windowingEducationProto =
-            createWindowingEducationProto(
-                appHandleHintViewedTimestampMillis = 123L,
-                appHandleHintUsedTimestampMillis = 124L,
-                appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2),
-                appUsageStatsLastUpdateTimestampMillis = 125L)
-        testDatastore.updateData { windowingEducationProto }
+    @Test
+    fun getWindowingEducationProto_returnsCorrectProto() =
+        runTest(StandardTestDispatcher()) {
+            val windowingEducationProto =
+                createWindowingEducationProto(
+                    appHandleHintViewedTimestampMillis = 123L,
+                    appHandleHintUsedTimestampMillis = 124L,
+                    appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2),
+                    appUsageStatsLastUpdateTimestampMillis = 125L,
+                )
+            testDatastore.updateData { windowingEducationProto }
 
-        val resultProto = datastoreRepository.windowingEducationProto()
+            val resultProto = datastoreRepository.windowingEducationProto()
 
-        assertThat(resultProto).isEqualTo(windowingEducationProto)
-      }
+            assertThat(resultProto).isEqualTo(windowingEducationProto)
+        }
 
-  @Test
-  fun updateAppUsageStats_updatesDatastoreProto() =
-      runTest(StandardTestDispatcher()) {
-        val appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 3)
-        val appUsageStatsLastUpdateTimestamp = Duration.ofMillis(123L)
-        val windowingEducationProto =
-            createWindowingEducationProto(
-                appUsageStats = appUsageStats,
-                appUsageStatsLastUpdateTimestampMillis =
-                    appUsageStatsLastUpdateTimestamp.toMillis())
+    @Test
+    fun updateAppUsageStats_updatesDatastoreProto() =
+        runTest(StandardTestDispatcher()) {
+            val appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 3)
+            val appUsageStatsLastUpdateTimestamp = Duration.ofMillis(123L)
+            val windowingEducationProto =
+                createWindowingEducationProto(
+                    appUsageStats = appUsageStats,
+                    appUsageStatsLastUpdateTimestampMillis =
+                        appUsageStatsLastUpdateTimestamp.toMillis(),
+                )
 
-        datastoreRepository.updateAppUsageStats(appUsageStats, appUsageStatsLastUpdateTimestamp)
+            datastoreRepository.updateAppUsageStats(appUsageStats, appUsageStatsLastUpdateTimestamp)
 
-        val result = testDatastore.data.first()
-        assertThat(result).isEqualTo(windowingEducationProto)
-      }
+            val result = testDatastore.data.first()
+            assertThat(result).isEqualTo(windowingEducationProto)
+        }
 
-  @Test
-  fun updateAppHandleHintViewedTimestampMillis_updatesDatastoreProto() =
-      runTest(StandardTestDispatcher()) {
-        datastoreRepository.updateAppHandleHintViewedTimestampMillis(true)
+    @Test
+    fun updateAppHandleHintViewedTimestampMillis_updatesDatastoreProto() =
+        runTest(StandardTestDispatcher()) {
+            datastoreRepository.updateAppHandleHintViewedTimestampMillis(true)
 
-        val result = testDatastore.data.first().hasAppHandleHintViewedTimestampMillis()
-        assertThat(result).isEqualTo(true)
-      }
+            val result = testDatastore.data.first().hasAppHandleHintViewedTimestampMillis()
+            assertThat(result).isEqualTo(true)
+        }
 
-  @Test
-  fun updateAppHandleHintUsedTimestampMillis_updatesDatastoreProto() =
-      runTest(StandardTestDispatcher()) {
-        datastoreRepository.updateAppHandleHintUsedTimestampMillis(true)
+    @Test
+    fun updateAppHandleHintUsedTimestampMillis_updatesDatastoreProto() =
+        runTest(StandardTestDispatcher()) {
+            datastoreRepository.updateAppHandleHintUsedTimestampMillis(true)
 
-        val result = testDatastore.data.first().hasAppHandleHintUsedTimestampMillis()
-        assertThat(result).isEqualTo(true)
-      }
+            val result = testDatastore.data.first().hasAppHandleHintUsedTimestampMillis()
+            assertThat(result).isEqualTo(true)
+        }
 
-  companion object {
-    private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb"
-  }
+    companion object {
+        private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb"
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
index e5edd69..2fc36ef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
@@ -53,189 +53,221 @@
 @RunWith(AndroidTestingRunner::class)
 @kotlinx.coroutines.ExperimentalCoroutinesApi
 class AppHandleEducationFilterTest : ShellTestCase() {
-  @JvmField
-  @Rule
-  val extendedMockitoRule =
-      ExtendedMockitoRule.Builder(this).mockStatic(SystemProperties::class.java).build()!!
-  @Mock private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository
-  @Mock private lateinit var mockUsageStatsManager: UsageStatsManager
-  private lateinit var educationFilter: AppHandleEducationFilter
-  private lateinit var testableResources: TestableResources
-  private lateinit var testableContext: TestableContext
+    @JvmField
+    @Rule
+    val extendedMockitoRule =
+        ExtendedMockitoRule.Builder(this).mockStatic(SystemProperties::class.java).build()!!
+    @Mock private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository
+    @Mock private lateinit var mockUsageStatsManager: UsageStatsManager
+    private lateinit var educationFilter: AppHandleEducationFilter
+    private lateinit var testableResources: TestableResources
+    private lateinit var testableContext: TestableContext
 
-  @Before
-  fun setup() {
-    MockitoAnnotations.initMocks(this)
-    testableContext = TestableContext(mContext)
-    testableResources =
-        testableContext.orCreateTestableResources.apply {
-          addOverride(
-              R.array.desktop_windowing_app_handle_education_allowlist_apps,
-              arrayOf(GMAIL_PACKAGE_NAME))
-          addOverride(R.integer.desktop_windowing_education_required_time_since_setup_seconds, 0)
-          addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
-          addOverride(
-              R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, MAX_VALUE)
-          addOverride(R.integer.desktop_windowing_education_app_launch_interval_seconds, 100)
-        }
-    testableContext.addMockSystemService(Context.USAGE_STATS_SERVICE, mockUsageStatsManager)
-    educationFilter = AppHandleEducationFilter(testableContext, datastoreRepository)
-  }
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        testableContext = TestableContext(mContext)
+        testableResources =
+            testableContext.orCreateTestableResources.apply {
+                addOverride(
+                    R.array.desktop_windowing_app_handle_education_allowlist_apps,
+                    arrayOf(GMAIL_PACKAGE_NAME),
+                )
+                addOverride(
+                    R.integer.desktop_windowing_education_required_time_since_setup_seconds,
+                    0,
+                )
+                addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
+                addOverride(
+                    R.integer.desktop_windowing_education_app_usage_cache_interval_seconds,
+                    MAX_VALUE,
+                )
+                addOverride(R.integer.desktop_windowing_education_app_launch_interval_seconds, 100)
+            }
+        testableContext.addMockSystemService(Context.USAGE_STATS_SERVICE, mockUsageStatsManager)
+        educationFilter = AppHandleEducationFilter(testableContext, datastoreRepository)
+    }
 
-  @Test
-  fun shouldShowAppHandleEducation_isTriggerValid_returnsTrue() = runTest {
-    // setup() makes sure that all of the conditions satisfy and #shouldShowAppHandleEducation
-    // should return true
-    val windowingEducationProto =
-        createWindowingEducationProto(
-            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
-            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
-    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+    @Test
+    fun shouldShowAppHandleEducation_isTriggerValid_returnsTrue() = runTest {
+        // setup() makes sure that all of the conditions satisfy and #shouldShowAppHandleEducation
+        // should return true
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+                appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
+            )
+        `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
 
-    val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+        val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
 
-    assertThat(result).isTrue()
-  }
+        assertThat(result).isTrue()
+    }
 
-  @Test
-  fun shouldShowAppHandleEducation_focusAppNotInAllowlist_returnsFalse() = runTest {
-    // Pass Youtube as current focus app, it is not in allowlist hence #shouldShowAppHandleEducation
-    // should return false
-    testableResources.addOverride(
-        R.array.desktop_windowing_app_handle_education_allowlist_apps, arrayOf(GMAIL_PACKAGE_NAME))
-    val windowingEducationProto =
-        createWindowingEducationProto(
-            appUsageStats = mapOf(YOUTUBE_PACKAGE_NAME to 4),
-            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
-    val captionState =
-        createAppHandleState(createTaskInfo(runningTaskPackageName = YOUTUBE_PACKAGE_NAME))
-    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+    @Test
+    fun shouldShowAppHandleEducation_focusAppNotInAllowlist_returnsFalse() = runTest {
+        // Pass Youtube as current focus app, it is not in allowlist hence
+        // #shouldShowAppHandleEducation
+        // should return false
+        testableResources.addOverride(
+            R.array.desktop_windowing_app_handle_education_allowlist_apps,
+            arrayOf(GMAIL_PACKAGE_NAME),
+        )
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = mapOf(YOUTUBE_PACKAGE_NAME to 4),
+                appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
+            )
+        val captionState =
+            createAppHandleState(createTaskInfo(runningTaskPackageName = YOUTUBE_PACKAGE_NAME))
+        `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
 
-    val result = educationFilter.shouldShowAppHandleEducation(captionState)
+        val result = educationFilter.shouldShowAppHandleEducation(captionState)
 
-    assertThat(result).isFalse()
-  }
+        assertThat(result).isFalse()
+    }
 
-  @Test
-  fun shouldShowAppHandleEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest {
-    // Time required to have passed setup is > 100 years, hence #shouldShowAppHandleEducation should
-    // return false
-    testableResources.addOverride(
-        R.integer.desktop_windowing_education_required_time_since_setup_seconds, MAX_VALUE)
-    val windowingEducationProto =
-        createWindowingEducationProto(
-            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
-            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
-    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+    @Test
+    fun shouldShowAppHandleEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest {
+        // Time required to have passed setup is > 100 years, hence #shouldShowAppHandleEducation
+        // should
+        // return false
+        testableResources.addOverride(
+            R.integer.desktop_windowing_education_required_time_since_setup_seconds,
+            MAX_VALUE,
+        )
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+                appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
+            )
+        `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
 
-    val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+        val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
 
-    assertThat(result).isFalse()
-  }
+        assertThat(result).isFalse()
+    }
 
-  @Test
-  fun shouldShowAppHandleEducation_appHandleHintViewedBefore_returnsFalse() = runTest {
-    // App handle hint has been viewed before, hence #shouldShowAppHandleEducation should return false
-    val windowingEducationProto =
-        createWindowingEducationProto(
-            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
-            appHandleHintViewedTimestampMillis = 123L,
-            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
-    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+    @Test
+    fun shouldShowAppHandleEducation_appHandleHintViewedBefore_returnsFalse() = runTest {
+        // App handle hint has been viewed before, hence #shouldShowAppHandleEducation should return
+        // false
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+                appHandleHintViewedTimestampMillis = 123L,
+                appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
+            )
+        `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
 
-    val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+        val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
 
-    assertThat(result).isFalse()
-  }
+        assertThat(result).isFalse()
+    }
 
-  @Test
-  fun shouldShowAppHandleEducation_appHandleHintUsedBefore_returnsFalse() = runTest {
-    // App handle hint has been used before, hence #shouldShowAppHandleEducation should return false
-    val windowingEducationProto =
-        createWindowingEducationProto(
-            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
-            appHandleHintUsedTimestampMillis = 123L,
-            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
-    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+    @Test
+    fun shouldShowAppHandleEducation_appHandleHintUsedBefore_returnsFalse() = runTest {
+        // App handle hint has been used before, hence #shouldShowAppHandleEducation should return
+        // false
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+                appHandleHintUsedTimestampMillis = 123L,
+                appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
+            )
+        `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
 
-    val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+        val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
 
-    assertThat(result).isFalse()
-  }
+        assertThat(result).isFalse()
+    }
 
-  @Test
-  fun shouldShowAppHandleEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest {
-    // Simulate that gmail app has been launched twice before, minimum app launch count is 3, hence
-    // #shouldShowAppHandleEducation should return false
-    testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
-    val windowingEducationProto =
-        createWindowingEducationProto(
-            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2),
-            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
-    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+    @Test
+    fun shouldShowAppHandleEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest {
+        // Simulate that gmail app has been launched twice before, minimum app launch count is 3,
+        // hence
+        // #shouldShowAppHandleEducation should return false
+        testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2),
+                appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
+            )
+        `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
 
-    val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+        val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
 
-    assertThat(result).isFalse()
-  }
+        assertThat(result).isFalse()
+    }
 
-  @Test
-  fun shouldShowAppHandleEducation_appUsageStatsStale_queryAppUsageStats() = runTest {
-    // UsageStats caching interval is set to 0ms, that means caching should happen very frequently
-    testableResources.addOverride(
-        R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, 0)
-    // The DataStore currently holds a proto object where Gmail's app launch count is recorded as 4.
-    // This value exceeds the minimum required count of 3.
-    testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
-    val windowingEducationProto =
-        createWindowingEducationProto(
-            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
-            appUsageStatsLastUpdateTimestampMillis = 0)
-    // The mocked UsageStatsManager is configured to return a launch count of 2 for Gmail.
-    // This value is below the minimum required count of 3.
-    `when`(mockUsageStatsManager.queryAndAggregateUsageStats(anyLong(), anyLong()))
-        .thenReturn(mapOf(GMAIL_PACKAGE_NAME to UsageStats().apply { mAppLaunchCount = 2 }))
-    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+    @Test
+    fun shouldShowAppHandleEducation_appUsageStatsStale_queryAppUsageStats() = runTest {
+        // UsageStats caching interval is set to 0ms, that means caching should happen very
+        // frequently
+        testableResources.addOverride(
+            R.integer.desktop_windowing_education_app_usage_cache_interval_seconds,
+            0,
+        )
+        // The DataStore currently holds a proto object where Gmail's app launch count is recorded
+        // as 4.
+        // This value exceeds the minimum required count of 3.
+        testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+                appUsageStatsLastUpdateTimestampMillis = 0,
+            )
+        // The mocked UsageStatsManager is configured to return a launch count of 2 for Gmail.
+        // This value is below the minimum required count of 3.
+        `when`(mockUsageStatsManager.queryAndAggregateUsageStats(anyLong(), anyLong()))
+            .thenReturn(mapOf(GMAIL_PACKAGE_NAME to UsageStats().apply { mAppLaunchCount = 2 }))
+        `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
 
-    val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+        val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
 
-    // Result should be false as queried usage stats should be considered to determine the result
-    // instead of cached stats
-    assertThat(result).isFalse()
-  }
+        // Result should be false as queried usage stats should be considered to determine the
+        // result
+        // instead of cached stats
+        assertThat(result).isFalse()
+    }
 
-  @Test
-  fun shouldShowAppHandleEducation_appHandleMenuExpanded_returnsFalse() = runTest {
-    val windowingEducationProto =
-        createWindowingEducationProto(
-            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
-            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
-    // Simulate app handle menu is expanded
-    val captionState = createAppHandleState(isHandleMenuExpanded = true)
-    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+    @Test
+    fun shouldShowAppHandleEducation_appHandleMenuExpanded_returnsFalse() = runTest {
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
+                appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
+            )
+        // Simulate app handle menu is expanded
+        val captionState = createAppHandleState(isHandleMenuExpanded = true)
+        `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
 
-    val result = educationFilter.shouldShowAppHandleEducation(captionState)
+        val result = educationFilter.shouldShowAppHandleEducation(captionState)
 
-    // We should not show app handle education if app menu is expanded
-    assertThat(result).isFalse()
-  }
+        // We should not show app handle education if app menu is expanded
+        assertThat(result).isFalse()
+    }
 
-  @Test
-  fun shouldShowAppHandleEducation_overridePrerequisite_returnsTrue() = runTest {
-    // Simulate that gmail app has been launched twice before, minimum app launch count is 3, hence
-    // #shouldShowAppHandleEducation should return false. But as we are overriding prerequisite
-    // conditions, #shouldShowAppHandleEducation should return true.
-    testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
-    val systemPropertiesKey = "persist.desktop_windowing_app_handle_education_override_conditions"
-    whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())).thenReturn(true)
-    val windowingEducationProto =
-        createWindowingEducationProto(
-            appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2),
-            appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE)
-    `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
+    @Test
+    fun shouldShowAppHandleEducation_overridePrerequisite_returnsTrue() = runTest {
+        // Simulate that gmail app has been launched twice before, minimum app launch count is 3,
+        // hence
+        // #shouldShowAppHandleEducation should return false. But as we are overriding prerequisite
+        // conditions, #shouldShowAppHandleEducation should return true.
+        testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
+        val systemPropertiesKey =
+            "persist.desktop_windowing_app_handle_education_override_conditions"
+        whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean()))
+            .thenReturn(true)
+        val windowingEducationProto =
+            createWindowingEducationProto(
+                appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2),
+                appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
+            )
+        `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
 
-    val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+        val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
 
-    assertThat(result).isTrue()
-  }
+        assertThat(result).isTrue()
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt
index 6a5d9f6..8d7fb5d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt
@@ -66,16 +66,27 @@
 
     private fun createRemoteHandler(taskIdToMinimize: Int) =
         DesktopWindowLimitRemoteHandler(
-            shellExecutor, rootTaskDisplayAreaOrganizer, remoteTransition, taskIdToMinimize)
+            shellExecutor,
+            rootTaskDisplayAreaOrganizer,
+            remoteTransition,
+            taskIdToMinimize,
+        )
 
     @Test
     fun startAnimation_dontSetTransition_returnsFalse() {
         val minimizeTask = createDesktopTask()
         val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId)
 
-        assertThat(remoteHandler.startAnimation(transition,
-            createMinimizeTransitionInfo(minimizeTask), startT, finishT, finishCallback)
-        ).isFalse()
+        assertThat(
+                remoteHandler.startAnimation(
+                    transition,
+                    createMinimizeTransitionInfo(minimizeTask),
+                    startT,
+                    finishT,
+                    finishCallback,
+                )
+            )
+            .isFalse()
     }
 
     @Test
@@ -84,9 +95,8 @@
         remoteHandler.setTransition(transition)
         val info = createToFrontTransitionInfo()
 
-        assertThat(
-            remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback)
-        ).isFalse()
+        assertThat(remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback))
+            .isFalse()
     }
 
     @Test
@@ -96,9 +106,8 @@
         remoteHandler.setTransition(transition)
         val info = createMinimizeTransitionInfo(minimizeTask)
 
-        assertThat(
-            remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback)
-        ).isTrue()
+        assertThat(remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback))
+            .isTrue()
     }
 
     @Test
@@ -109,8 +118,7 @@
 
         remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback)
 
-        verify(rootTaskDisplayAreaOrganizer, times(0))
-            .reparentToDisplayArea(anyInt(), any(), any())
+        verify(rootTaskDisplayAreaOrganizer, times(0)).reparentToDisplayArea(anyInt(), any(), any())
     }
 
     @Test
@@ -154,14 +162,18 @@
 
     private fun createToFrontTransitionInfo() =
         TransitionInfoBuilder(TRANSIT_TO_FRONT)
-            .addChange(TRANSIT_TO_FRONT,
-                TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build())
+            .addChange(
+                TRANSIT_TO_FRONT,
+                TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(),
+            )
             .build()
 
     private fun createMinimizeTransitionInfo(minimizeTask: ActivityManager.RunningTaskInfo) =
         TransitionInfoBuilder(TRANSIT_TO_FRONT)
-            .addChange(TRANSIT_TO_FRONT,
-                TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build())
+            .addChange(
+                TRANSIT_TO_FRONT,
+                TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(),
+            )
             .addChange(TRANSIT_TO_BACK, minimizeTask)
             .build()
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
index 4f7e80c..eae2066 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
@@ -63,9 +63,10 @@
             DataStoreFactory.create(
                 serializer =
                     DesktopPersistentRepository.Companion.DesktopPersistentRepositoriesSerializer,
-                scope = datastoreScope) {
-                    testContext.dataStoreFile(DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE)
-                }
+                scope = datastoreScope,
+            ) {
+                testContext.dataStoreFile(DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE)
+            }
         datastoreRepository = DesktopPersistentRepository(testDatastore)
     }
 
@@ -113,7 +114,8 @@
                 visibleTasks = visibleTasks,
                 minimizedTasks = minimizedTasks,
                 freeformTasksInZOrder = freeformTasksInZOrder,
-                userId = DEFAULT_USER_ID)
+                userId = DEFAULT_USER_ID,
+            )
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
             assertThat(actualDesktop?.tasksByTaskIdMap).hasSize(2)
@@ -137,7 +139,8 @@
                 visibleTasks = visibleTasks,
                 minimizedTasks = minimizedTasks,
                 freeformTasksInZOrder = freeformTasksInZOrder,
-                userId = DEFAULT_USER_ID)
+                userId = DEFAULT_USER_ID,
+            )
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
             assertThat(actualDesktop?.tasksByTaskIdMap?.get(task.taskId)?.desktopTaskState)
@@ -161,7 +164,8 @@
                 visibleTasks = visibleTasks,
                 minimizedTasks = minimizedTasks,
                 freeformTasksInZOrder = freeformTasksInZOrder,
-                userId = DEFAULT_USER_ID)
+                userId = DEFAULT_USER_ID,
+            )
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
             assertThat(actualDesktop?.tasksByTaskIdMap).isEmpty()
@@ -194,7 +198,7 @@
 
         fun createDesktopTask(
             taskId: Int,
-            state: DesktopTaskState = DesktopTaskState.VISIBLE
+            state: DesktopTaskState = DesktopTaskState.VISIBLE,
         ): DesktopTask =
             DesktopTask.newBuilder().setTaskId(taskId).setDesktopTaskState(state).build()
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
index cdf064b..a3c4416 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
@@ -47,15 +47,12 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
-
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @ExperimentalCoroutinesApi
 class DesktopRepositoryInitializerTest : ShellTestCase() {
 
-    @JvmField
-    @Rule
-    val setFlagsRule = SetFlagsRule()
+    @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
     private lateinit var repositoryInitializer: DesktopRepositoryInitializer
     private lateinit var shellInit: ShellInit
@@ -82,7 +79,7 @@
                 persistentRepository,
                 repositoryInitializer,
                 datastoreScope,
-                userManager
+                userManager,
             )
     }
 
@@ -90,101 +87,94 @@
     @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_DESKTOP_WINDOWING_HSUM)
     fun initWithPersistence_multipleUsers_addedCorrectly() =
         runTest(StandardTestDispatcher()) {
-            whenever(persistentRepository.getUserDesktopRepositoryMap()).thenReturn(
-                mapOf(
-                    USER_ID_1 to desktopRepositoryState1,
-                    USER_ID_2 to desktopRepositoryState2
+            whenever(persistentRepository.getUserDesktopRepositoryMap())
+                .thenReturn(
+                    mapOf(
+                        USER_ID_1 to desktopRepositoryState1,
+                        USER_ID_2 to desktopRepositoryState2,
+                    )
                 )
-            )
             whenever(persistentRepository.getDesktopRepositoryState(USER_ID_1))
                 .thenReturn(desktopRepositoryState1)
             whenever(persistentRepository.getDesktopRepositoryState(USER_ID_2))
                 .thenReturn(desktopRepositoryState2)
-            whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1))
-                .thenReturn(desktop1)
-            whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2))
-                .thenReturn(desktop2)
-            whenever(persistentRepository.readDesktop(USER_ID_2, DESKTOP_ID_3))
-                .thenReturn(desktop3)
+            whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)).thenReturn(desktop1)
+            whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)).thenReturn(desktop2)
+            whenever(persistentRepository.readDesktop(USER_ID_2, DESKTOP_ID_3)).thenReturn(desktop3)
 
             repositoryInitializer.initialize(desktopUserRepositories)
 
             // Desktop Repository currently returns all tasks across desktops for a specific user
-            // since the repository currently doesn't handle desktops. This test logic should be updated
+            // since the repository currently doesn't handle desktops. This test logic should be
+            // updated
             // once the repository handles multiple desktops.
             assertThat(
-                desktopUserRepositories.getProfile(USER_ID_1)
-                    .getActiveTasks(DEFAULT_DISPLAY)
-            )
+                    desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY)
+                )
                 .containsExactly(1, 3, 4, 5)
                 .inOrder()
             assertThat(
-                desktopUserRepositories.getProfile(USER_ID_1)
-                    .getExpandedTasksOrdered(DEFAULT_DISPLAY)
-            )
+                    desktopUserRepositories
+                        .getProfile(USER_ID_1)
+                        .getExpandedTasksOrdered(DEFAULT_DISPLAY)
+                )
                 .containsExactly(5, 1)
                 .inOrder()
             assertThat(
-                desktopUserRepositories.getProfile(USER_ID_1)
-                    .getMinimizedTasks(DEFAULT_DISPLAY)
-            )
+                    desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY)
+                )
                 .containsExactly(3, 4)
                 .inOrder()
 
             assertThat(
-                desktopUserRepositories.getProfile(USER_ID_2)
-                    .getActiveTasks(DEFAULT_DISPLAY)
-            )
+                    desktopUserRepositories.getProfile(USER_ID_2).getActiveTasks(DEFAULT_DISPLAY)
+                )
                 .containsExactly(7, 8)
                 .inOrder()
             assertThat(
-                desktopUserRepositories.getProfile(USER_ID_2)
-                    .getExpandedTasksOrdered(DEFAULT_DISPLAY)
-            )
+                    desktopUserRepositories
+                        .getProfile(USER_ID_2)
+                        .getExpandedTasksOrdered(DEFAULT_DISPLAY)
+                )
                 .contains(7)
             assertThat(
-                desktopUserRepositories.getProfile(USER_ID_2)
-                    .getMinimizedTasks(DEFAULT_DISPLAY)
-            ).containsExactly(8)
+                    desktopUserRepositories.getProfile(USER_ID_2).getMinimizedTasks(DEFAULT_DISPLAY)
+                )
+                .containsExactly(8)
         }
 
     @Test
     @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
     fun initWithPersistence_singleUser_addedCorrectly() =
         runTest(StandardTestDispatcher()) {
-            whenever(persistentRepository.getUserDesktopRepositoryMap()).thenReturn(
-                mapOf(
-                    USER_ID_1 to desktopRepositoryState1,
-                )
-            )
+            whenever(persistentRepository.getUserDesktopRepositoryMap())
+                .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1))
             whenever(persistentRepository.getDesktopRepositoryState(USER_ID_1))
                 .thenReturn(desktopRepositoryState1)
-            whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1))
-                .thenReturn(desktop1)
-            whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2))
-                .thenReturn(desktop2)
+            whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)).thenReturn(desktop1)
+            whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)).thenReturn(desktop2)
 
             repositoryInitializer.initialize(desktopUserRepositories)
 
             // Desktop Repository currently returns all tasks across desktops for a specific user
-            // since the repository currently doesn't handle desktops. This test logic should be updated
+            // since the repository currently doesn't handle desktops. This test logic should be
+            // updated
             // once the repository handles multiple desktops.
             assertThat(
-                desktopUserRepositories.getProfile(USER_ID_1)
-                    .getActiveTasks(DEFAULT_DISPLAY)
-            )
+                    desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY)
+                )
                 .containsExactly(1, 3, 4, 5)
                 .inOrder()
             assertThat(
-                desktopUserRepositories.getProfile(USER_ID_1)
-                    .getExpandedTasksOrdered(DEFAULT_DISPLAY)
-            )
+                    desktopUserRepositories
+                        .getProfile(USER_ID_1)
+                        .getExpandedTasksOrdered(DEFAULT_DISPLAY)
+                )
                 .containsExactly(5, 1)
                 .inOrder()
             assertThat(
-                desktopUserRepositories.getProfile(USER_ID_1)
-                    .getMinimizedTasks(DEFAULT_DISPLAY)
-            )
+                    desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY)
+                )
                 .containsExactly(3, 4)
                 .inOrder()
         }
@@ -202,70 +192,73 @@
         const val DESKTOP_ID_3 = 4
 
         val freeformTasksInZOrder1 = listOf(1, 3)
-        val desktop1: Desktop = Desktop.newBuilder()
-            .setDesktopId(DESKTOP_ID_1)
-            .addAllZOrderedTasks(freeformTasksInZOrder1)
-            .putTasksByTaskId(
-                1,
-                DesktopTask.newBuilder()
-                    .setTaskId(1)
-                    .setDesktopTaskState(DesktopTaskState.VISIBLE)
-                    .build()
-            )
-            .putTasksByTaskId(
-                3,
-                DesktopTask.newBuilder()
-                    .setTaskId(3)
-                    .setDesktopTaskState(DesktopTaskState.MINIMIZED)
-                    .build()
-            )
-            .build()
+        val desktop1: Desktop =
+            Desktop.newBuilder()
+                .setDesktopId(DESKTOP_ID_1)
+                .addAllZOrderedTasks(freeformTasksInZOrder1)
+                .putTasksByTaskId(
+                    1,
+                    DesktopTask.newBuilder()
+                        .setTaskId(1)
+                        .setDesktopTaskState(DesktopTaskState.VISIBLE)
+                        .build(),
+                )
+                .putTasksByTaskId(
+                    3,
+                    DesktopTask.newBuilder()
+                        .setTaskId(3)
+                        .setDesktopTaskState(DesktopTaskState.MINIMIZED)
+                        .build(),
+                )
+                .build()
 
         val freeformTasksInZOrder2 = listOf(4, 5)
-        val desktop2: Desktop = Desktop.newBuilder()
-            .setDesktopId(DESKTOP_ID_2)
-            .addAllZOrderedTasks(freeformTasksInZOrder2)
-            .putTasksByTaskId(
-                4,
-                DesktopTask.newBuilder()
-                    .setTaskId(4)
-                    .setDesktopTaskState(DesktopTaskState.MINIMIZED)
-                    .build()
-            )
-            .putTasksByTaskId(
-                5,
-                DesktopTask.newBuilder()
-                    .setTaskId(5)
-                    .setDesktopTaskState(DesktopTaskState.VISIBLE)
-                    .build()
-            )
-            .build()
+        val desktop2: Desktop =
+            Desktop.newBuilder()
+                .setDesktopId(DESKTOP_ID_2)
+                .addAllZOrderedTasks(freeformTasksInZOrder2)
+                .putTasksByTaskId(
+                    4,
+                    DesktopTask.newBuilder()
+                        .setTaskId(4)
+                        .setDesktopTaskState(DesktopTaskState.MINIMIZED)
+                        .build(),
+                )
+                .putTasksByTaskId(
+                    5,
+                    DesktopTask.newBuilder()
+                        .setTaskId(5)
+                        .setDesktopTaskState(DesktopTaskState.VISIBLE)
+                        .build(),
+                )
+                .build()
 
         val freeformTasksInZOrder3 = listOf(7, 8)
-        val desktop3: Desktop = Desktop.newBuilder()
-            .setDesktopId(DESKTOP_ID_3)
-            .addAllZOrderedTasks(freeformTasksInZOrder3)
-            .putTasksByTaskId(
-                7,
-                DesktopTask.newBuilder()
-                    .setTaskId(7)
-                    .setDesktopTaskState(DesktopTaskState.VISIBLE)
-                    .build()
-            )
-            .putTasksByTaskId(
-                8,
-                DesktopTask.newBuilder()
-                    .setTaskId(8)
-                    .setDesktopTaskState(DesktopTaskState.MINIMIZED)
-                    .build()
-            )
-            .build()
-        val desktopRepositoryState1: DesktopRepositoryState = DesktopRepositoryState.newBuilder()
-            .putDesktop(DESKTOP_ID_1, desktop1)
-            .putDesktop(DESKTOP_ID_2, desktop2)
-            .build()
-        val desktopRepositoryState2: DesktopRepositoryState = DesktopRepositoryState.newBuilder()
-            .putDesktop(DESKTOP_ID_3, desktop3)
-            .build()
+        val desktop3: Desktop =
+            Desktop.newBuilder()
+                .setDesktopId(DESKTOP_ID_3)
+                .addAllZOrderedTasks(freeformTasksInZOrder3)
+                .putTasksByTaskId(
+                    7,
+                    DesktopTask.newBuilder()
+                        .setTaskId(7)
+                        .setDesktopTaskState(DesktopTaskState.VISIBLE)
+                        .build(),
+                )
+                .putTasksByTaskId(
+                    8,
+                    DesktopTask.newBuilder()
+                        .setTaskId(8)
+                        .setDesktopTaskState(DesktopTaskState.MINIMIZED)
+                        .build(),
+                )
+                .build()
+        val desktopRepositoryState1: DesktopRepositoryState =
+            DesktopRepositoryState.newBuilder()
+                .putDesktop(DESKTOP_ID_1, desktop1)
+                .putDesktop(DESKTOP_ID_2, desktop2)
+                .build()
+        val desktopRepositoryState2: DesktopRepositoryState =
+            DesktopRepositoryState.newBuilder().putDesktop(DESKTOP_ID_3, desktop3).build()
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index 232ae07..ada7b4af 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -36,6 +36,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.common.split.SplitState;
+import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.shared.TransactionPool;
 import com.android.wm.shell.transition.Transitions;
@@ -81,11 +82,13 @@
                 ShellExecutor mainExecutor, Handler mainHandler, ShellExecutor bgExecutor,
                 Optional<RecentTasksController> recentTasks,
                 LaunchAdjacentController launchAdjacentController,
-                Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState) {
+                Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState,
+                Optional<DesktopTasksController> desktopTasksController) {
             super(context, displayId, syncQueue, taskOrganizer, mainStage,
                     sideStage, displayController, imeController, insetsController, splitLayout,
                     transitions, transactionPool, mainExecutor, mainHandler, bgExecutor,
-                    recentTasks, launchAdjacentController, windowDecorViewModel, splitState);
+                    recentTasks, launchAdjacentController, windowDecorViewModel, splitState,
+                    desktopTasksController);
 
             // Prepare root task for testing.
             mRootTask = new TestRunningTaskInfoBuilder().build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 0d612c1..ffa8b60 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -149,7 +149,7 @@
                 mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
                 mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
                 mTransactionPool, mMainExecutor, mMainHandler, mBgExecutor, Optional.empty(),
-                mLaunchAdjacentController, Optional.empty(), mSplitState);
+                mLaunchAdjacentController, Optional.empty(), mSplitState, Optional.empty());
         mStageCoordinator.setMixedHandler(mMixedHandler);
         mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
         doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index a6aeabd..9d1df86 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -143,8 +143,9 @@
         mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                 mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
-                mMainExecutor, mMainHandler, mBgExecutor, Optional.empty(), 
-                mLaunchAdjacentController, Optional.empty(), mSplitState));
+                mMainExecutor, mMainHandler, mBgExecutor, Optional.empty(),
+                mLaunchAdjacentController, Optional.empty(), mSplitState,
+                Optional.empty()));
         mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build();
 
         when(mSplitLayout.getTopLeftBounds()).thenReturn(mBounds1);
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 0214da4..aead0a7 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
@@ -1015,11 +1015,11 @@
             onCaptionButtonTouchListener = onTouchListenerCaptor
         )
 
-        whenever(mockTaskPositioner.onDragPositioningStart(any(), any(), any()))
+        whenever(mockTaskPositioner.onDragPositioningStart(any(), any(), any(), any()))
             .thenReturn(INITIAL_BOUNDS)
-        whenever(mockTaskPositioner.onDragPositioningMove(any(), any()))
+        whenever(mockTaskPositioner.onDragPositioningMove(any(), any(), any()))
             .thenReturn(INITIAL_BOUNDS)
-        whenever(mockTaskPositioner.onDragPositioningEnd(any(), any()))
+        whenever(mockTaskPositioner.onDragPositioningEnd(any(), any(), any()))
             .thenReturn(INITIAL_BOUNDS)
 
         val view = mock(View::class.java)
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 8a1a9b5..855b3dd 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
@@ -278,9 +278,9 @@
         when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel");
         final ActivityInfo activityInfo = createActivityInfo();
         when(mMockPackageManager.getActivityInfo(any(), anyInt())).thenReturn(activityInfo);
-        final ResolveInfo resolveInfo = new ResolveInfo();
-        resolveInfo.activityInfo = activityInfo;
-        when(mMockPackageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo);
+        final ResolveInfo resolveInfo = createResolveInfo(false /* handleAllWebDataUri */);
+        when(mMockPackageManager.resolveActivityAsUser(any(), anyInt(), anyInt()))
+                .thenReturn(resolveInfo);
         final Display defaultDisplay = mock(Display.class);
         doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
         doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
@@ -1664,11 +1664,9 @@
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
     public void browserApp_transferSessionUriUsedForBrowserAppWhenAvailable() {
         // Make {@link AppToWebUtils#isBrowserApp} return true
-        ResolveInfo resolveInfo = new ResolveInfo();
-        resolveInfo.handleAllWebDataURI = true;
-        resolveInfo.activityInfo = createActivityInfo();
+        ResolveInfo browserResolveInfo = createResolveInfo(true /* handleAllWebUriData */);
         when(mMockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt()))
-                .thenReturn(List.of(resolveInfo));
+                .thenReturn(List.of(browserResolveInfo));
 
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
         final DesktopModeWindowDecoration decor = createWindowDecoration(
@@ -1793,6 +1791,13 @@
         return windowDecor;
     }
 
+    private ResolveInfo createResolveInfo(boolean handleAllWebDataURI) {
+        final ResolveInfo info = new ResolveInfo();
+        info.handleAllWebDataURI = handleAllWebDataURI;
+        info.activityInfo = createActivityInfo();
+        return info;
+    }
+
     private ActivityManager.RunningTaskInfo createTaskInfo(boolean visible) {
         final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
                 new ActivityManager.TaskDescription.Builder();
@@ -1821,6 +1826,7 @@
         applicationInfo.packageName = "DesktopModeWindowDecorationTestPackage";
         final ActivityInfo activityInfo = new ActivityInfo();
         activityInfo.applicationInfo = applicationInfo;
+        activityInfo.packageName = "DesktopModeWindowDecorationTestPackage";
         activityInfo.name = "DesktopModeWindowDecorationTest";
         return activityInfo;
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt
index ce17c1d..3c3d6b6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt
@@ -68,9 +68,9 @@
             configuration.windowConfiguration.setBounds(PORTRAIT_BOUNDS)
         }
         doReturn(PORTRAIT_BOUNDS).`when`(mockTaskPositioner).onDragPositioningStart(
-            any(), any(), any())
-        doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningMove(any(), any())
-        doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningEnd(any(), any())
+            any(), any(), any(), any())
+        doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningMove(any(), any(), any())
+        doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningEnd(any(), any(), any())
         decoratedTaskPositioner = spy(
             FixedAspectRatioTaskPositionerDecorator(
             mockDesktopWindowDecoration, mockTaskPositioner)
@@ -87,7 +87,8 @@
             isResizeable = testCase.isResizeable
         }
 
-        decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalY)
+        decoratedTaskPositioner.onDragPositioningStart(
+            testCase.ctrlType, DISPLAY_ID, originalX, originalY)
 
         val capturedValues = getLatestOnStartArguments()
         assertThat(capturedValues.ctrlType).isEqualTo(testCase.ctrlType)
@@ -102,7 +103,8 @@
         val originalX = 0f
         val originalY = 0f
 
-        decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalY)
+        decoratedTaskPositioner.onDragPositioningStart(
+            testCase.ctrlType, DISPLAY_ID, originalX, originalY)
 
         val capturedValues = getLatestOnStartArguments()
         assertThat(capturedValues.ctrlType).isEqualTo(testCase.ctrlType)
@@ -119,7 +121,7 @@
             testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds)
 
         decoratedTaskPositioner.onDragPositioningStart(
-            testCase.ctrlType, startingPoint.x, startingPoint.y)
+            testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y)
 
         val adjustedCtrlType = testCase.ctrlType + testCase.additionalEdgeCtrlType
         val capturedValues = getLatestOnStartArguments()
@@ -134,13 +136,14 @@
     ) {
         val originalX = 0f
         val originalY = 0f
-        decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalX)
+        decoratedTaskPositioner.onDragPositioningStart(
+            testCase.ctrlType, DISPLAY_ID, originalX, originalX)
         mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
             isResizeable = testCase.isResizeable
         }
 
         decoratedTaskPositioner.onDragPositioningMove(
-            originalX + SMALL_DELTA, originalY + SMALL_DELTA)
+            DISPLAY_ID, originalX + SMALL_DELTA, originalY + SMALL_DELTA)
 
         val capturedValues = getLatestOnMoveArguments()
         assertThat(capturedValues.x).isEqualTo(originalX + SMALL_DELTA)
@@ -156,13 +159,14 @@
         val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds)
 
         decoratedTaskPositioner.onDragPositioningStart(
-            testCase.ctrlType, startingPoint.x, startingPoint.y)
+            testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y)
 
         val updatedBounds = decoratedTaskPositioner.onDragPositioningMove(
+            DISPLAY_ID,
             startingPoint.x + testCase.dragDelta.x,
             startingPoint.y + testCase.dragDelta.y)
 
-        verify(mockTaskPositioner, never()).onDragPositioningMove(any(), any())
+        verify(mockTaskPositioner, never()).onDragPositioningMove(any(), any(), any())
         assertThat(updatedBounds).isEqualTo(startingBounds)
     }
 
@@ -176,10 +180,12 @@
         val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds)
 
         decoratedTaskPositioner.onDragPositioningStart(
-            testCase.ctrlType, startingPoint.x, startingPoint.y)
+            testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y)
 
         decoratedTaskPositioner.onDragPositioningMove(
-            startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y)
+            DISPLAY_ID,
+            startingPoint.x + testCase.dragDelta.x,
+            startingPoint.y + testCase.dragDelta.y)
 
         val adjustedDragDelta = calculateAdjustedDelta(
             testCase.ctrlType, testCase.dragDelta, orientation)
@@ -202,9 +208,10 @@
             testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds)
 
         decoratedTaskPositioner.onDragPositioningStart(
-            testCase.ctrlType, startingPoint.x, startingPoint.y)
+            testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y)
 
         decoratedTaskPositioner.onDragPositioningMove(
+            DISPLAY_ID,
             startingPoint.x + testCase.dragDelta.x,
             startingPoint.y + testCase.dragDelta.y)
 
@@ -227,13 +234,14 @@
     ) {
         val originalX = 0f
         val originalY = 0f
-        decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalX)
+        decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, DISPLAY_ID,
+            originalX, originalX)
         mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
             isResizeable = testCase.isResizeable
         }
 
         decoratedTaskPositioner.onDragPositioningEnd(
-            originalX + SMALL_DELTA, originalY + SMALL_DELTA)
+            DISPLAY_ID, originalX + SMALL_DELTA, originalY + SMALL_DELTA)
 
         val capturedValues = getLatestOnEndArguments()
         assertThat(capturedValues.x).isEqualTo(originalX + SMALL_DELTA)
@@ -249,9 +257,10 @@
         val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds)
 
         decoratedTaskPositioner.onDragPositioningStart(
-            testCase.ctrlType, startingPoint.x, startingPoint.y)
+            testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y)
 
         decoratedTaskPositioner.onDragPositioningEnd(
+            DISPLAY_ID,
             startingPoint.x + testCase.dragDelta.x,
             startingPoint.y + testCase.dragDelta.y)
 
@@ -269,10 +278,12 @@
         val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds)
 
         decoratedTaskPositioner.onDragPositioningStart(
-            testCase.ctrlType, startingPoint.x, startingPoint.y)
+            testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y)
 
         decoratedTaskPositioner.onDragPositioningEnd(
-            startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y)
+            DISPLAY_ID,
+            startingPoint.x + testCase.dragDelta.x,
+            startingPoint.y + testCase.dragDelta.y)
 
         val adjustedDragDelta = calculateAdjustedDelta(
             testCase.ctrlType, testCase.dragDelta, orientation)
@@ -295,9 +306,10 @@
             testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds)
 
         decoratedTaskPositioner.onDragPositioningStart(
-            testCase.ctrlType, startingPoint.x, startingPoint.y)
+            testCase.ctrlType, DISPLAY_ID, startingPoint.x, startingPoint.y)
 
         decoratedTaskPositioner.onDragPositioningEnd(
+            DISPLAY_ID,
             startingPoint.x + testCase.dragDelta.x,
             startingPoint.y + testCase.dragDelta.y)
 
@@ -322,7 +334,7 @@
         val captorCtrlType = argumentCaptor<Int>()
         val captorCoordinates = argumentCaptor<Float>()
         verify(mockTaskPositioner).onDragPositioningStart(
-            captorCtrlType.capture(), captorCoordinates.capture(), captorCoordinates.capture())
+            captorCtrlType.capture(), any(), captorCoordinates.capture(), captorCoordinates.capture())
 
         return CtrlCoordinateCapture(captorCtrlType.firstValue, captorCoordinates.firstValue,
             captorCoordinates.secondValue)
@@ -335,7 +347,7 @@
     private fun getLatestOnMoveArguments(): PointF {
         val captorCoordinates = argumentCaptor<Float>()
         verify(mockTaskPositioner).onDragPositioningMove(
-            captorCoordinates.capture(), captorCoordinates.capture())
+            any(), captorCoordinates.capture(), captorCoordinates.capture())
 
         return PointF(captorCoordinates.firstValue, captorCoordinates.secondValue)
     }
@@ -347,7 +359,7 @@
     private fun getLatestOnEndArguments(): PointF {
         val captorCoordinates = argumentCaptor<Float>()
         verify(mockTaskPositioner).onDragPositioningEnd(
-            captorCoordinates.capture(), captorCoordinates.capture())
+            any(), captorCoordinates.capture(), captorCoordinates.capture())
 
         return PointF(captorCoordinates.firstValue, captorCoordinates.secondValue)
     }
@@ -358,7 +370,7 @@
     private fun getAndMockBounds(orientation: Orientation): Rect {
         val mockBounds = if (orientation.isPortrait) PORTRAIT_BOUNDS else LANDSCAPE_BOUNDS
         doReturn(mockBounds).`when`(mockTaskPositioner).onDragPositioningStart(
-            any(), any(), any())
+            any(), any(), any(), any())
         doReturn(mockBounds).`when`(decoratedTaskPositioner).getBounds(any())
         return mockBounds
     }
@@ -458,6 +470,7 @@
         private val STARTING_ASPECT_RATIO = PORTRAIT_BOUNDS.height() / PORTRAIT_BOUNDS.width()
         private const val LARGE_DELTA = 50f
         private const val SMALL_DELTA = 30f
+        private const val DISPLAY_ID = 1
 
         enum class Orientation(
             val isPortrait: Boolean
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index 3b80cb4..cec5251 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -150,11 +150,13 @@
     fun testDragResize_notMove_skipsTransitionOnEnd() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
 
         taskPositioner.onDragPositioningEnd(
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat() + 10,
                 STARTING_BOUNDS.top.toFloat() + 10
         )
@@ -171,11 +173,13 @@
     fun testDragResize_noEffectiveMove_skipsTransitionOnMoveAndEnd() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
 
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -188,6 +192,7 @@
         })
 
         taskPositioner.onDragPositioningEnd(
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat() + 10,
                 STARTING_BOUNDS.top.toFloat() + 10
         )
@@ -204,11 +209,13 @@
     fun testDragResize_hasEffectiveMove_issuesTransitionOnMoveAndEnd() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
 
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat() + 10,
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -224,6 +231,7 @@
         verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID))
 
         taskPositioner.onDragPositioningEnd(
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat() + 10,
                 STARTING_BOUNDS.top.toFloat() + 10
         )
@@ -242,6 +250,7 @@
     fun testDragResize_move_skipsDragResizingFlag() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_UNDEFINED, // Move
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -250,11 +259,12 @@
         val newX = STARTING_BOUNDS.left.toFloat() + 10
         val newY = STARTING_BOUNDS.top.toFloat()
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -276,6 +286,7 @@
     fun testDragResize_resize_setsDragResizingFlag() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT, // Resize right
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -284,11 +295,12 @@
         val newX = STARTING_BOUNDS.right.toFloat() + 10
         val newY = STARTING_BOUNDS.top.toFloat()
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -310,6 +322,7 @@
     fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenLessThanMin() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                DISPLAY_ID,
                 STARTING_BOUNDS.right.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -318,11 +331,12 @@
         val newX = STARTING_BOUNDS.right.toFloat() - 5
         val newY = STARTING_BOUNDS.top.toFloat() + 95
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -340,6 +354,7 @@
     fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenLessThanMin() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                DISPLAY_ID,
                 STARTING_BOUNDS.right.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -348,11 +363,12 @@
         val newX = STARTING_BOUNDS.right.toFloat() - 95
         val newY = STARTING_BOUNDS.top.toFloat() + 5
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -370,6 +386,7 @@
     fun testDragResize_resize_setBoundsDoesNotChangeHeightWhenNegative() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                DISPLAY_ID,
                 STARTING_BOUNDS.right.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -378,11 +395,12 @@
         val newX = STARTING_BOUNDS.right.toFloat() - 5
         val newY = STARTING_BOUNDS.top.toFloat() + 105
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -400,6 +418,7 @@
     fun testDragResize_resize_setBoundsDoesNotChangeWidthWhenNegative() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                DISPLAY_ID,
                 STARTING_BOUNDS.right.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -408,11 +427,12 @@
         val newX = STARTING_BOUNDS.right.toFloat() - 105
         val newY = STARTING_BOUNDS.top.toFloat() + 5
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -430,6 +450,7 @@
     fun testDragResize_resize_setBoundsRunsWhenResizeBoundsValid() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                DISPLAY_ID,
                 STARTING_BOUNDS.right.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -438,11 +459,12 @@
         val newX = STARTING_BOUNDS.right.toFloat() - 80
         val newY = STARTING_BOUNDS.top.toFloat() + 80
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -456,6 +478,7 @@
     fun testDragResize_resize_setBoundsDoesNotRunWithNegativeHeightAndWidth() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                DISPLAY_ID,
                 STARTING_BOUNDS.right.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -464,11 +487,12 @@
         val newX = STARTING_BOUNDS.right.toFloat() - 95
         val newY = STARTING_BOUNDS.top.toFloat() + 95
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -484,6 +508,7 @@
 
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                DISPLAY_ID,
                 STARTING_BOUNDS.right.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -492,11 +517,12 @@
         val newX = STARTING_BOUNDS.right.toFloat() - 97
         val newY = STARTING_BOUNDS.top.toFloat() + 97
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -510,6 +536,7 @@
     fun testDragResize_resize_useMinWidthWhenValid() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, // Resize right and top
+                DISPLAY_ID,
                 STARTING_BOUNDS.right.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -518,11 +545,12 @@
         val newX = STARTING_BOUNDS.right.toFloat() - 93
         val newY = STARTING_BOUNDS.top.toFloat() + 93
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -535,6 +563,7 @@
     fun testDragResize_toDisallowedBounds_freezesAtLimit() {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, // Resize right-bottom corner
+                DISPLAY_ID,
                 STARTING_BOUNDS.right.toFloat(),
                 STARTING_BOUNDS.bottom.toFloat()
         )
@@ -546,6 +575,7 @@
                 STARTING_BOUNDS.right + 10,
                 STARTING_BOUNDS.bottom + 10)
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newBounds.right.toFloat(),
                 newBounds.bottom.toFloat()
         )
@@ -559,11 +589,13 @@
                 DISALLOWED_RESIZE_AREA.top
         )
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newBounds2.right.toFloat(),
                 newBounds2.bottom.toFloat()
         )
 
-        taskPositioner.onDragPositioningEnd(newBounds2.right.toFloat(), newBounds2.bottom.toFloat())
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newBounds2.right.toFloat(),
+            newBounds2.bottom.toFloat())
 
         // The first resize falls in the allowed area, verify there's a change for it.
         verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
@@ -629,6 +661,7 @@
         mockWindowDecoration.mHasGlobalFocus = false
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT, // Resize right
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -645,6 +678,7 @@
         mockWindowDecoration.mHasGlobalFocus = true
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT, // Resize right
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -661,6 +695,7 @@
         mockWindowDecoration.mHasGlobalFocus = false
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_UNDEFINED, // drag
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -729,11 +764,13 @@
 
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
 
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat() - 20,
                 STARTING_BOUNDS.top.toFloat() - 20
         )
@@ -742,6 +779,7 @@
         assertTrue(taskPositioner.isResizingOrAnimating)
 
         taskPositioner.onDragPositioningEnd(
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -785,15 +823,18 @@
     ) {
         taskPositioner.onDragPositioningStart(
             ctrlType,
+            DISPLAY_ID,
             startX,
             startY
         )
         taskPositioner.onDragPositioningMove(
+            DISPLAY_ID,
             endX,
             endY
         )
 
         taskPositioner.onDragPositioningEnd(
+            DISPLAY_ID,
             endX,
             endY
         )
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index e7df864..eb8c0dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -168,12 +168,14 @@
     fun testDragResize_noMove_doesNotShowResizeVeil() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
             CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+            DISPLAY_ID,
             STARTING_BOUNDS.left.toFloat(),
             STARTING_BOUNDS.top.toFloat()
         )
         verify(mockDesktopWindowDecoration, never()).showResizeVeil(STARTING_BOUNDS)
 
         taskPositioner.onDragPositioningEnd(
+            DISPLAY_ID,
             STARTING_BOUNDS.left.toFloat(),
             STARTING_BOUNDS.top.toFloat()
         )
@@ -191,11 +193,13 @@
     fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
             CTRL_TYPE_UNDEFINED,
+            DISPLAY_ID,
             STARTING_BOUNDS.left.toFloat(),
             STARTING_BOUNDS.top.toFloat()
         )
 
         taskPositioner.onDragPositioningMove(
+            DISPLAY_ID,
             STARTING_BOUNDS.left.toFloat() + 60,
             STARTING_BOUNDS.top.toFloat() + 100
         )
@@ -208,6 +212,7 @@
                 eq(rectAfterMove.top.toFloat()))
 
         val endBounds = taskPositioner.onDragPositioningEnd(
+            DISPLAY_ID,
             STARTING_BOUNDS.left.toFloat() + 70,
             STARTING_BOUNDS.top.toFloat() + 20
         )
@@ -226,11 +231,13 @@
     fun testDragResize_resize_boundsUpdateOnEnd() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
             CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+            DISPLAY_ID,
             STARTING_BOUNDS.right.toFloat(),
             STARTING_BOUNDS.top.toFloat()
         )
 
         taskPositioner.onDragPositioningMove(
+            DISPLAY_ID,
             STARTING_BOUNDS.right.toFloat() + 10,
             STARTING_BOUNDS.top.toFloat() + 10
         )
@@ -248,6 +255,7 @@
         })
 
         taskPositioner.onDragPositioningEnd(
+            DISPLAY_ID,
             STARTING_BOUNDS.right.toFloat() + 20,
             STARTING_BOUNDS.top.toFloat() + 20
         )
@@ -266,17 +274,20 @@
     @Test
     fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
+            DISPLAY_ID,
             CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
             STARTING_BOUNDS.left.toFloat(),
             STARTING_BOUNDS.top.toFloat()
         )
 
         taskPositioner.onDragPositioningMove(
+            DISPLAY_ID,
             STARTING_BOUNDS.left.toFloat(),
             STARTING_BOUNDS.top.toFloat()
         )
 
         taskPositioner.onDragPositioningEnd(
+            DISPLAY_ID,
             STARTING_BOUNDS.left.toFloat() + 10,
             STARTING_BOUNDS.top.toFloat() + 10
         )
@@ -300,6 +311,7 @@
     fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread {
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_UNDEFINED, // drag
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -307,11 +319,12 @@
         val newX = STARTING_BOUNDS.left.toFloat() + 5
         val newY = DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT.toFloat() - 1
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 newX,
                 newY
         )
 
-        taskPositioner.onDragPositioningEnd(newX, newY)
+        taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY)
 
         verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
             return@argThat wct.changes.any { (token, change) ->
@@ -326,6 +339,7 @@
         mockDesktopWindowDecoration.mHasGlobalFocus = false
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT, // Resize right
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -342,6 +356,7 @@
         mockDesktopWindowDecoration.mHasGlobalFocus = true
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_RIGHT, // Resize right
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -358,6 +373,7 @@
         mockDesktopWindowDecoration.mHasGlobalFocus = false
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_UNDEFINED, // drag
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -422,11 +438,13 @@
 
         taskPositioner.onDragPositioningStart(
                 CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
 
         taskPositioner.onDragPositioningMove(
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat() - 20,
                 STARTING_BOUNDS.top.toFloat() - 20
         )
@@ -436,6 +454,7 @@
         verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID))
 
         taskPositioner.onDragPositioningEnd(
+                DISPLAY_ID,
                 STARTING_BOUNDS.left.toFloat(),
                 STARTING_BOUNDS.top.toFloat()
         )
@@ -501,15 +520,18 @@
     ) {
         taskPositioner.onDragPositioningStart(
             ctrlType,
+            DISPLAY_ID,
             startX,
             startY
         )
         taskPositioner.onDragPositioningMove(
+            DISPLAY_ID,
             endX,
             endY
         )
 
         taskPositioner.onDragPositioningEnd(
+            DISPLAY_ID,
             endX,
             endY
         )
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 064cac2..7d01dfb 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -54,6 +54,9 @@
 constexpr bool query_global_priority() {
     return false;
 }
+constexpr bool early_preload_gl_context() {
+    return false;
+}
 }  // namespace hwui_flags
 #endif
 
@@ -291,5 +294,10 @@
     return sResampleGainmapRegions;
 }
 
+bool Properties::earlyPreloadGlContext() {
+    return base::GetBoolProperty(PROPERTY_EARLY_PRELOAD_GL_CONTEXT,
+                                 hwui_flags::early_preload_gl_context());
+}
+
 }  // namespace uirenderer
 }  // namespace android
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index db930f3..280a75a 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -236,6 +236,8 @@
 
 #define PROPERTY_SKIP_EGLMANAGER_TELEMETRY "debug.hwui.skip_eglmanager_telemetry"
 
+#define PROPERTY_EARLY_PRELOAD_GL_CONTEXT "debug.hwui.early_preload_gl_context"
+
 ///////////////////////////////////////////////////////////////////////////////
 // Misc
 ///////////////////////////////////////////////////////////////////////////////
@@ -381,6 +383,7 @@
 
     static bool initializeGlAlways();
     static bool resampleGainmapRegions();
+    static bool earlyPreloadGlContext();
 
 private:
     static StretchEffectBehavior stretchEffectBehavior;
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index e497ea1..76ad2ac 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -166,4 +166,11 @@
   metadata {
     purpose: PURPOSE_BUGFIX
   }
+}
+
+flag {
+  name: "early_preload_gl_context"
+  namespace: "core_graphics"
+  description: "Initialize GL context and GraphicBufferAllocater init on renderThread preload. This improves app startup time for apps using GL."
+  bug: "383612849"
 }
\ No newline at end of file
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 92c6ad1..69fe40c 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -25,6 +25,7 @@
 #include <private/android/choreographer.h>
 #include <sys/resource.h>
 #include <ui/FatVector.h>
+#include <ui/GraphicBufferAllocator.h>
 #include <utils/Condition.h>
 #include <utils/Log.h>
 #include <utils/Mutex.h>
@@ -518,11 +519,18 @@
 void RenderThread::preload() {
     // EGL driver is always preloaded only if HWUI renders with GL.
     if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaGL) {
-        std::thread eglInitThread([]() { eglGetDisplay(EGL_DEFAULT_DISPLAY); });
-        eglInitThread.detach();
+        if (Properties::earlyPreloadGlContext()) {
+            queue().post([this]() { requireGlContext(); });
+        } else {
+            std::thread eglInitThread([]() { eglGetDisplay(EGL_DEFAULT_DISPLAY); });
+            eglInitThread.detach();
+        }
     } else {
         requireVkContext();
     }
+    if (Properties::earlyPreloadGlContext()) {
+        queue().post([]() { GraphicBufferAllocator::getInstance(); });
+    }
     HardwareBitmapUploader::initialize();
 }
 
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index 4b3962e..9e9bbee 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -210,6 +210,27 @@
         return super.equals(o);
     }
 
+    /**
+     * Returns true if the AudioDevicePort passed as argument represents the same device (same
+     * type and same address). This is different from equals() in that the port IDs are not compared
+     * which allows matching devices across native audio server restarts.
+     * @param other the other audio device port to compare to.
+     * @return true if both device port correspond to the same audio device, false otherwise.
+     * @hide
+     */
+    public boolean isSameAs(AudioDevicePort other) {
+        if (mType != other.type()) {
+            return false;
+        }
+        if (mAddress == null && other.address() != null) {
+            return false;
+        }
+        if (!mAddress.equals(other.address())) {
+            return false;
+        }
+        return true;
+    }
+
     @Override
     public String toString() {
         String type = (mRole == ROLE_SOURCE ?
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 9beeef4..52eae43 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -8068,15 +8068,15 @@
             ArrayList<AudioDevicePort> ports_A, ArrayList<AudioDevicePort> ports_B, int flags) {
 
         ArrayList<AudioDevicePort> delta_ports = new ArrayList<AudioDevicePort>();
-
-        AudioDevicePort cur_port = null;
         for (int cur_index = 0; cur_index < ports_B.size(); cur_index++) {
             boolean cur_port_found = false;
-            cur_port = ports_B.get(cur_index);
+            AudioDevicePort cur_port = ports_B.get(cur_index);
             for (int prev_index = 0;
                  prev_index < ports_A.size() && !cur_port_found;
                  prev_index++) {
-                cur_port_found = (cur_port.id() == ports_A.get(prev_index).id());
+                // Do not compare devices by port ID as these change when the native
+                // audio server restarts
+                cur_port_found = cur_port.isSameAs(ports_A.get(prev_index));
             }
 
             if (!cur_port_found) {
@@ -8422,13 +8422,10 @@
          * Callback method called when the mediaserver dies
          */
         public void onServiceDied() {
-            synchronized (mDeviceCallbacks) {
-                broadcastDeviceListChange_sync(null);
-            }
+           // Nothing to do here
         }
     }
 
-
     /**
      * @hide
      * Abstract class to receive event notification about audioserver process state.
@@ -8469,9 +8466,13 @@
     /**
      * @hide
      * Registers a callback for notification of audio server state changes.
-     * @param executor {@link Executor} to handle the callbacks
-     * @param stateCallback the callback to receive the audio server state changes
-     *        To remove the callabck, pass a null reference for both executor and stateCallback.
+     * @param executor {@link Executor} to handle the callbacks. Must be non null.
+     * @param stateCallback the callback to receive the audio server state changes.
+     *                      Must be non null. To remove the callabck,
+     *                      call {@link #clearAudioServerStateCallback()}
+     * @throws IllegalArgumentException If a null argument is specified.
+     * @throws IllegalStateException If a callback is already registered
+     * *
      */
     @SystemApi
     public void setAudioServerStateCallback(@NonNull Executor executor,
diff --git a/media/java/android/media/AudioPortEventHandler.java b/media/java/android/media/AudioPortEventHandler.java
index 763eb29..5685710 100644
--- a/media/java/android/media/AudioPortEventHandler.java
+++ b/media/java/android/media/AudioPortEventHandler.java
@@ -97,17 +97,15 @@
 
                         ArrayList<AudioPort> ports = new ArrayList<AudioPort>();
                         ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>();
-                        if (msg.what != AUDIOPORT_EVENT_SERVICE_DIED) {
-                            int status = AudioManager.updateAudioPortCache(ports, patches, null);
-                            if (status != AudioManager.SUCCESS) {
-                                // Since audio ports and audio patches are not null, the return
-                                // value could be ERROR due to inconsistency between port generation
-                                // and patch generation. In this case, we need to reschedule the
-                                // message to make sure the native callback is done.
-                                sendMessageDelayed(obtainMessage(msg.what, msg.obj),
-                                        RESCHEDULE_MESSAGE_DELAY_MS);
-                                return;
-                            }
+                        int status = AudioManager.updateAudioPortCache(ports, patches, null);
+                        if (status != AudioManager.SUCCESS) {
+                            // Since audio ports and audio patches are not null, the return
+                            // value could be ERROR due to inconsistency between port generation
+                            // and patch generation. In this case, we need to reschedule the
+                            // message to make sure the native callback is done.
+                            sendMessageDelayed(obtainMessage(msg.what, msg.obj),
+                                    RESCHEDULE_MESSAGE_DELAY_MS);
+                            return;
                         }
 
                         switch (msg.what) {
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index 5038754..bcb7001 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -53,7 +53,7 @@
  * The format of the media data is specified as key/value pairs. Keys are strings. Values can
  * be integer, long, float, String or ByteBuffer.
  * <p>
- * The feature metadata is specificed as string/boolean pairs.
+ * The feature metadata is specified as string/boolean pairs.
  * <p>
  * Keys common to all audio/video formats, <b>all keys not marked optional are mandatory</b>:
  *
@@ -1244,12 +1244,12 @@
 
     /**
     * An optional key describing the desired encoder latency in frames. This is an optional
-    * parameter that applies only to video encoders. If encoder supports it, it should ouput
+    * parameter that applies only to video encoders. If encoder supports it, it should output
     * at least one output frame after being queued the specified number of frames. This key
     * is ignored if the video encoder does not support the latency feature. Use the output
     * format to verify that this feature was enabled and the actual value used by the encoder.
     * <p>
-    * If the key is not specified, the default latency will be implenmentation specific.
+    * If the key is not specified, the default latency will be implementation specific.
     * The associated value is an integer.
     */
     public static final String KEY_LATENCY = "latency";
@@ -1507,16 +1507,16 @@
      */
     public static final String KEY_COLOR_STANDARD = "color-standard";
 
-    /** BT.709 color chromacity coordinates with KR = 0.2126, KB = 0.0722. */
+    /** BT.709 color chromaticity coordinates with KR = 0.2126, KB = 0.0722. */
     public static final int COLOR_STANDARD_BT709 = 1;
 
-    /** BT.601 625 color chromacity coordinates with KR = 0.299, KB = 0.114. */
+    /** BT.601 625 color chromaticity coordinates with KR = 0.299, KB = 0.114. */
     public static final int COLOR_STANDARD_BT601_PAL = 2;
 
-    /** BT.601 525 color chromacity coordinates with KR = 0.299, KB = 0.114. */
+    /** BT.601 525 color chromaticity coordinates with KR = 0.299, KB = 0.114. */
     public static final int COLOR_STANDARD_BT601_NTSC = 4;
 
-    /** BT.2020 color chromacity coordinates with KR = 0.2627, KB = 0.0593. */
+    /** BT.2020 color chromaticity coordinates with KR = 0.2627, KB = 0.0593. */
     public static final int COLOR_STANDARD_BT2020 = 6;
 
     /** @hide */
@@ -2150,7 +2150,7 @@
      * Sets the value of a string key.
      * <p>
      * If value is {@code null}, it sets a null value that behaves similarly to a missing key.
-     * This could be used prior to API level {@link android os.Build.VERSION_CODES#Q} to effectively
+     * This could be used prior to API level {@link android.os.Build.VERSION_CODES#Q} to effectively
      * remove a key.
      */
     public final void setString(@NonNull String name, @Nullable String value) {
@@ -2161,7 +2161,7 @@
      * Sets the value of a ByteBuffer key.
      * <p>
      * If value is {@code null}, it sets a null value that behaves similarly to a missing key.
-     * This could be used prior to API level {@link android os.Build.VERSION_CODES#Q} to effectively
+     * This could be used prior to API level {@link android.os.Build.VERSION_CODES#Q} to effectively
      * remove a key.
      */
     public final void setByteBuffer(@NonNull String name, @Nullable ByteBuffer bytes) {
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 0db99ff..9257901 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -22,6 +22,7 @@
 #include <aidl/android/hardware/power/SessionHint.h>
 #include <aidl/android/hardware/power/SessionMode.h>
 #include <aidl/android/hardware/power/SessionTag.h>
+#include <aidl/android/hardware/power/SupportInfo.h>
 #include <aidl/android/hardware/power/WorkDuration.h>
 #include <aidl/android/hardware/power/WorkDurationFixedV1.h>
 #include <aidl/android/os/IHintManager.h>
@@ -148,10 +149,36 @@
     std::future<bool> mChannelCreationFinished;
 };
 
+class SupportInfoWrapper {
+public:
+    SupportInfoWrapper(hal::SupportInfo& info);
+    bool isSessionModeSupported(hal::SessionMode mode);
+    bool isSessionHintSupported(hal::SessionHint hint);
+
+private:
+    template <class T>
+    bool getEnumSupportFromBitfield(T& enumValue, int64_t& supportBitfield) {
+        // extract the bit corresponding to the enum by shifting the bitfield
+        // over that much and cutting off any extra values
+        return (supportBitfield >> static_cast<int>(enumValue)) % 2;
+    }
+    hal::SupportInfo mSupportInfo;
+};
+
+class HintManagerClient : public IHintManager::BnHintManagerClient {
+public:
+    // Currently a no-op that exists for FMQ init to call in the future
+    ndk::ScopedAStatus receiveChannelConfig(const hal::ChannelConfig&) {
+        return ndk::ScopedAStatus::ok();
+    }
+};
+
 struct APerformanceHintManager {
 public:
     static APerformanceHintManager* getInstance();
-    APerformanceHintManager(std::shared_ptr<IHintManager>& service, int64_t preferredRateNanos);
+    APerformanceHintManager(std::shared_ptr<IHintManager>& service,
+                            IHintManager::HintManagerClientData&& clientData,
+                            std::shared_ptr<HintManagerClient> callbackClient);
     APerformanceHintManager() = delete;
     ~APerformanceHintManager();
 
@@ -169,29 +196,21 @@
     FMQWrapper& getFMQWrapper();
     bool canSendLoadHints(std::vector<hal::SessionHint>& hints, int64_t now) REQUIRES(sHintMutex);
     void initJava(JNIEnv* _Nonnull env);
-    ndk::ScopedAIBinder_Weak x;
     template <class T>
     static void layersFromNativeSurfaces(ANativeWindow** windows, int numWindows,
                                          ASurfaceControl** controls, int numSurfaceControls,
                                          std::vector<T>& out);
+    ndk::SpAIBinder& getToken();
+    SupportInfoWrapper& getSupportInfo();
 
 private:
-    // Necessary to create an empty binder object
-    static void* tokenStubOnCreate(void*) {
-        return nullptr;
-    }
-    static void tokenStubOnDestroy(void*) {}
-    static binder_status_t tokenStubOnTransact(AIBinder*, transaction_code_t, const AParcel*,
-                                               AParcel*) {
-        return STATUS_OK;
-    }
-
     static APerformanceHintManager* create(std::shared_ptr<IHintManager> iHintManager);
 
     std::shared_ptr<IHintManager> mHintManager;
+    std::shared_ptr<HintManagerClient> mCallbackClient;
+    IHintManager::HintManagerClientData mClientData;
+    SupportInfoWrapper mSupportInfoWrapper;
     ndk::SpAIBinder mToken;
-    const int64_t mPreferredRateNanos;
-    std::optional<int32_t> mMaxGraphicsPipelineThreadsCount;
     FMQWrapper mFMQWrapper;
     double mHintBudget = kMaxLoadHintsPerInterval;
     int64_t mLastBudgetReplenish = 0;
@@ -273,14 +292,27 @@
     return APerformanceHintManager::getInstance()->getFMQWrapper();
 }
 
+// ===================================== SupportInfoWrapper implementation
+
+SupportInfoWrapper::SupportInfoWrapper(hal::SupportInfo& info) : mSupportInfo(info) {}
+
+bool SupportInfoWrapper::isSessionHintSupported(hal::SessionHint hint) {
+    return getEnumSupportFromBitfield(hint, mSupportInfo.sessionHints);
+}
+
+bool SupportInfoWrapper::isSessionModeSupported(hal::SessionMode mode) {
+    return getEnumSupportFromBitfield(mode, mSupportInfo.sessionModes);
+}
+
 // ===================================== APerformanceHintManager implementation
 APerformanceHintManager::APerformanceHintManager(std::shared_ptr<IHintManager>& manager,
-                                                 int64_t preferredRateNanos)
-      : mHintManager(std::move(manager)), mPreferredRateNanos(preferredRateNanos) {
-    static AIBinder_Class* tokenBinderClass =
-            AIBinder_Class_define("phm_token", tokenStubOnCreate, tokenStubOnDestroy,
-                                  tokenStubOnTransact);
-    mToken = ndk::SpAIBinder(AIBinder_new(tokenBinderClass, nullptr));
+                                                 IHintManager::HintManagerClientData&& clientData,
+                                                 std::shared_ptr<HintManagerClient> callbackClient)
+      : mHintManager(std::move(manager)),
+        mCallbackClient(callbackClient),
+        mClientData(clientData),
+        mSupportInfoWrapper(clientData.supportInfo),
+        mToken(callbackClient->asBinder()) {
     if (mFMQWrapper.isSupported()) {
         mFMQWrapper.setToken(mToken);
         mFMQWrapper.startChannel(mHintManager.get());
@@ -315,16 +347,17 @@
         ALOGE("%s: PerformanceHint service is not ready ", __FUNCTION__);
         return nullptr;
     }
-    int64_t preferredRateNanos = -1L;
-    ndk::ScopedAStatus ret = manager->getHintSessionPreferredRate(&preferredRateNanos);
+    std::shared_ptr<HintManagerClient> client = ndk::SharedRefBase::make<HintManagerClient>();
+    IHintManager::HintManagerClientData clientData;
+    ndk::ScopedAStatus ret = manager->registerClient(client, &clientData);
     if (!ret.isOk()) {
-        ALOGE("%s: PerformanceHint cannot get preferred rate. %s", __FUNCTION__, ret.getMessage());
+        ALOGE("%s: PerformanceHint is not supported. %s", __FUNCTION__, ret.getMessage());
         return nullptr;
     }
-    if (preferredRateNanos <= 0) {
-        preferredRateNanos = -1L;
+    if (clientData.preferredRateNanos <= 0) {
+        clientData.preferredRateNanos = -1L;
     }
-    return new APerformanceHintManager(manager, preferredRateNanos);
+    return new APerformanceHintManager(manager, std::move(clientData), client);
 }
 
 bool APerformanceHintManager::canSendLoadHints(std::vector<hal::SessionHint>& hints, int64_t now) {
@@ -389,7 +422,9 @@
         ALOGE("%s: PerformanceHint cannot create session. %s", __FUNCTION__, ret.getMessage());
         return nullptr;
     }
-    auto out = new APerformanceHintSession(mHintManager, std::move(session), mPreferredRateNanos,
+
+    auto out = new APerformanceHintSession(mHintManager, std::move(session),
+                                           mClientData.preferredRateNanos,
                                            sessionCreationConfig->targetWorkDurationNanos, isJava,
                                            sessionConfig.id == -1
                                                    ? std::nullopt
@@ -416,24 +451,11 @@
 }
 
 int64_t APerformanceHintManager::getPreferredRateNanos() const {
-    return mPreferredRateNanos;
+    return mClientData.preferredRateNanos;
 }
 
 int32_t APerformanceHintManager::getMaxGraphicsPipelineThreadsCount() {
-    if (!mMaxGraphicsPipelineThreadsCount.has_value()) {
-        int32_t threadsCount = -1;
-        ndk::ScopedAStatus ret = mHintManager->getMaxGraphicsPipelineThreadsCount(&threadsCount);
-        if (!ret.isOk()) {
-            ALOGE("%s: PerformanceHint cannot get max graphics pipeline threads count. %s",
-                  __FUNCTION__, ret.getMessage());
-            return -1;
-        }
-        if (threadsCount <= 0) {
-            threadsCount = -1;
-        }
-        mMaxGraphicsPipelineThreadsCount.emplace(threadsCount);
-    }
-    return mMaxGraphicsPipelineThreadsCount.value();
+    return mClientData.maxGraphicsPipelineThreads;
 }
 
 FMQWrapper& APerformanceHintManager::getFMQWrapper() {
@@ -450,6 +472,14 @@
     mJavaInitialized = true;
 }
 
+ndk::SpAIBinder& APerformanceHintManager::getToken() {
+    return mToken;
+}
+
+SupportInfoWrapper& APerformanceHintManager::getSupportInfo() {
+    return mSupportInfoWrapper;
+}
+
 // ===================================== APerformanceHintSession implementation
 
 constexpr int kNumEnums = enum_size<hal::SessionHint>();
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index c166e73..e3c10f6 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -56,9 +56,6 @@
                  const SessionCreationConfig& creationConfig, hal::SessionConfig* config,
                  std::shared_ptr<IHintSession>* _aidl_return),
                 (override));
-    MOCK_METHOD(ScopedAStatus, getHintSessionPreferredRate, (int64_t * _aidl_return), (override));
-    MOCK_METHOD(ScopedAStatus, getMaxGraphicsPipelineThreadsCount, (int32_t* _aidl_return),
-                (override));
     MOCK_METHOD(ScopedAStatus, setHintSessionThreads,
                 (const std::shared_ptr<IHintSession>& hintSession,
                  const ::std::vector<int32_t>& tids),
@@ -84,6 +81,11 @@
     MOCK_METHOD(ScopedAStatus, getGpuHeadroomMinIntervalMillis, (int64_t* _aidl_return),
                 (override));
     MOCK_METHOD(ScopedAStatus, passSessionManagerBinder, (const SpAIBinder& sessionManager));
+    MOCK_METHOD(ScopedAStatus, registerClient,
+                (const std::shared_ptr<::aidl::android::os::IHintManager::IHintManagerClient>&
+                         clientDataIn,
+                 ::aidl::android::os::IHintManager::HintManagerClientData* _aidl_return),
+                (override));
     MOCK_METHOD(SpAIBinder, asBinder, (), (override));
     MOCK_METHOD(bool, isRemote, (), (override));
 };
@@ -125,10 +127,9 @@
 
     APerformanceHintManager* createManager() {
         APerformanceHint_setUseFMQForTesting(mUsingFMQ);
-        ON_CALL(*mMockIHintManager, getHintSessionPreferredRate(_))
-                .WillByDefault(DoAll(SetArgPointee<0>(123L), [] { return ScopedAStatus::ok(); }));
-        ON_CALL(*mMockIHintManager, getMaxGraphicsPipelineThreadsCount(_))
-                .WillByDefault(DoAll(SetArgPointee<0>(5), [] { return ScopedAStatus::ok(); }));
+        ON_CALL(*mMockIHintManager, registerClient(_, _))
+                .WillByDefault(
+                        DoAll(SetArgPointee<1>(mClientData), [] { return ScopedAStatus::ok(); }));
         return APerformanceHint_getManager();
     }
 
@@ -238,6 +239,20 @@
     int kMockQueueSize = 20;
     bool mUsingFMQ = false;
 
+    IHintManager::HintManagerClientData mClientData{
+            .powerHalVersion = 6,
+            .maxGraphicsPipelineThreads = 5,
+            .preferredRateNanos = 123L,
+            .supportInfo{
+                    .usesSessions = true,
+                    .boosts = 0,
+                    .modes = 0,
+                    .sessionHints = -1,
+                    .sessionModes = -1,
+                    .sessionTags = -1,
+            },
+    };
+
     int32_t mMaxLoadHintsPerInterval;
     int64_t mLoadHintInterval;
 
@@ -256,12 +271,6 @@
             lhs.gpuDurationNanos == rhs.gpuDurationNanos && lhs.durationNanos == rhs.durationNanos;
 }
 
-TEST_F(PerformanceHintTest, TestGetPreferredUpdateRateNanos) {
-    APerformanceHintManager* manager = createManager();
-    int64_t preferredUpdateRateNanos = APerformanceHint_getPreferredUpdateRateNanos(manager);
-    EXPECT_EQ(123L, preferredUpdateRateNanos);
-}
-
 TEST_F(PerformanceHintTest, TestSession) {
     APerformanceHintManager* manager = createManager();
     APerformanceHintSession* session = createSession(manager);
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index 0ee81cb..c8c479a 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -211,7 +211,7 @@
     method public boolean isDefaultServiceForCategory(android.content.ComponentName, String);
     method @FlaggedApi("android.nfc.enable_card_emulation_euicc") public boolean isEuiccSupported();
     method public boolean registerAidsForService(android.content.ComponentName, String, java.util.List<java.lang.String>);
-    method @FlaggedApi("android.nfc.nfc_event_listener") public void registerNfcEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.cardemulation.CardEmulation.NfcEventListener);
+    method @FlaggedApi("android.nfc.nfc_event_listener") public void registerNfcEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.cardemulation.CardEmulation.NfcEventCallback);
     method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean registerPollingLoopFilterForService(@NonNull android.content.ComponentName, @NonNull String, boolean);
     method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean registerPollingLoopPatternFilterForService(@NonNull android.content.ComponentName, @NonNull String, boolean);
     method public boolean removeAidsForService(android.content.ComponentName, String);
@@ -221,7 +221,7 @@
     method public boolean setPreferredService(android.app.Activity, android.content.ComponentName);
     method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setShouldDefaultToObserveModeForService(@NonNull android.content.ComponentName, boolean);
     method public boolean supportsAidPrefixRegistration();
-    method @FlaggedApi("android.nfc.nfc_event_listener") public void unregisterNfcEventListener(@NonNull android.nfc.cardemulation.CardEmulation.NfcEventListener);
+    method @FlaggedApi("android.nfc.nfc_event_listener") public void unregisterNfcEventCallback(@NonNull android.nfc.cardemulation.CardEmulation.NfcEventCallback);
     method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean unsetOffHostForService(@NonNull android.content.ComponentName);
     method public boolean unsetPreferredService(android.app.Activity);
     field @Deprecated public static final String ACTION_CHANGE_DEFAULT = "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT";
@@ -244,7 +244,7 @@
     field public static final int SELECTION_MODE_PREFER_DEFAULT = 0; // 0x0
   }
 
-  @FlaggedApi("android.nfc.nfc_event_listener") public static interface CardEmulation.NfcEventListener {
+  @FlaggedApi("android.nfc.nfc_event_listener") public static interface CardEmulation.NfcEventCallback {
     method @FlaggedApi("android.nfc.nfc_event_listener") public default void onAidConflictOccurred(@NonNull String);
     method @FlaggedApi("android.nfc.nfc_event_listener") public default void onAidNotRouted(@NonNull String);
     method @FlaggedApi("android.nfc.nfc_event_listener") public default void onInternalErrorReported(int);
diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl
index bb9fe95..00ceaa9 100644
--- a/nfc/java/android/nfc/INfcCardEmulation.aidl
+++ b/nfc/java/android/nfc/INfcCardEmulation.aidl
@@ -17,7 +17,7 @@
 package android.nfc;
 
 import android.content.ComponentName;
-import android.nfc.INfcEventListener;
+import android.nfc.INfcEventCallback;
 
 import android.nfc.cardemulation.AidGroup;
 import android.nfc.cardemulation.ApduServiceInfo;
@@ -60,6 +60,6 @@
     List<String> getRoutingStatus();
     void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech, String sc);
 
-    void registerNfcEventListener(in INfcEventListener listener);
-    void unregisterNfcEventListener(in INfcEventListener listener);
+    void registerNfcEventCallback(in INfcEventCallback listener);
+    void unregisterNfcEventCallback(in INfcEventCallback listener);
 }
diff --git a/nfc/java/android/nfc/INfcEventListener.aidl b/nfc/java/android/nfc/INfcEventCallback.aidl
similarity index 92%
rename from nfc/java/android/nfc/INfcEventListener.aidl
rename to nfc/java/android/nfc/INfcEventCallback.aidl
index 774d8f8..af1fa2fb 100644
--- a/nfc/java/android/nfc/INfcEventListener.aidl
+++ b/nfc/java/android/nfc/INfcEventCallback.aidl
@@ -5,7 +5,7 @@
 /**
  * @hide
  */
-oneway interface INfcEventListener {
+oneway interface INfcEventCallback {
     void onPreferredServiceChanged(in ComponentNameAndUser ComponentNameAndUser);
     void onObserveModeStateChanged(boolean isEnabled);
     void onAidConflictOccurred(in String aid);
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 89ce423..63397c2 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -1789,6 +1789,11 @@
      * @param listenTechnology Flags indicating listen technologies.
      * @throws UnsupportedOperationException if FEATURE_NFC,
      * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF are unavailable.
+     *
+     * NOTE: This API overrides all technology flags regardless of the current device state,
+     *       it is incompatible with enableReaderMode() API and the others that either update
+     *       or assume any techlology flag set by the OS.
+     *       Please use with care.
      */
 
     @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH)
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index baae05b..fee9c5b 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -39,7 +39,7 @@
 import android.nfc.Constants;
 import android.nfc.Flags;
 import android.nfc.INfcCardEmulation;
-import android.nfc.INfcEventListener;
+import android.nfc.INfcEventCallback;
 import android.nfc.NfcAdapter;
 import android.os.Build;
 import android.os.RemoteException;
@@ -1304,7 +1304,7 @@
 
     /** Listener for preferred service state changes. */
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
-    public interface NfcEventListener {
+    public interface NfcEventCallback {
         /**
          * This method is called when this package gains or loses preferred Nfc service status,
          * either the Default Wallet Role holder (see {@link
@@ -1380,10 +1380,10 @@
         default void onInternalErrorReported(@NfcInternalErrorType int errorType) {}
     }
 
-    private final ArrayMap<NfcEventListener, Executor> mNfcEventListeners = new ArrayMap<>();
+    private final ArrayMap<NfcEventCallback, Executor> mNfcEventCallbacks = new ArrayMap<>();
 
-    final INfcEventListener mINfcEventListener =
-            new INfcEventListener.Stub() {
+    final INfcEventCallback mINfcEventCallback =
+            new INfcEventCallback.Stub() {
                 public void onPreferredServiceChanged(ComponentNameAndUser componentNameAndUser) {
                     if (!android.nfc.Flags.nfcEventListener()) {
                         return;
@@ -1443,12 +1443,12 @@
                 }
 
                 interface ListenerCall {
-                    void invoke(NfcEventListener listener);
+                    void invoke(NfcEventCallback listener);
                 }
 
                 private void callListeners(ListenerCall listenerCall) {
-                    synchronized (mNfcEventListeners) {
-                        mNfcEventListeners.forEach(
+                    synchronized (mNfcEventCallbacks) {
+                        mNfcEventCallbacks.forEach(
                             (listener, executor) -> {
                                 executor.execute(() -> listenerCall.invoke(listener));
                             });
@@ -1463,34 +1463,34 @@
      * @param listener The listener to register
      */
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
-    public void registerNfcEventListener(
-            @NonNull @CallbackExecutor Executor executor, @NonNull NfcEventListener listener) {
+    public void registerNfcEventCallback(
+            @NonNull @CallbackExecutor Executor executor, @NonNull NfcEventCallback listener) {
         if (!android.nfc.Flags.nfcEventListener()) {
             return;
         }
-        synchronized (mNfcEventListeners) {
-            mNfcEventListeners.put(listener, executor);
-            if (mNfcEventListeners.size() == 1) {
-                callService(() -> sService.registerNfcEventListener(mINfcEventListener));
+        synchronized (mNfcEventCallbacks) {
+            mNfcEventCallbacks.put(listener, executor);
+            if (mNfcEventCallbacks.size() == 1) {
+                callService(() -> sService.registerNfcEventCallback(mINfcEventCallback));
             }
         }
     }
 
     /**
      * Unregister a preferred service listener that was previously registered with {@link
-     * #registerNfcEventListener(Executor, NfcEventListener)}
+     * #registerNfcEventCallback(Executor, NfcEventCallback)}
      *
      * @param listener The previously registered listener to unregister
      */
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_EVENT_LISTENER)
-    public void unregisterNfcEventListener(@NonNull NfcEventListener listener) {
+    public void unregisterNfcEventCallback(@NonNull NfcEventCallback listener) {
         if (!android.nfc.Flags.nfcEventListener()) {
             return;
         }
-        synchronized (mNfcEventListeners) {
-            mNfcEventListeners.remove(listener);
-            if (mNfcEventListeners.size() == 0) {
-                callService(() -> sService.unregisterNfcEventListener(mINfcEventListener));
+        synchronized (mNfcEventCallbacks) {
+            mNfcEventCallbacks.remove(listener);
+            if (mNfcEventCallbacks.size() == 0) {
+                callService(() -> sService.unregisterNfcEventCallback(mINfcEventCallback));
             }
         }
     }
diff --git a/nfc/tests/src/android/nfc/NfcAntennaInfoTest.java b/nfc/tests/src/android/nfc/NfcAntennaInfoTest.java
new file mode 100644
index 0000000..c24816d
--- /dev/null
+++ b/nfc/tests/src/android/nfc/NfcAntennaInfoTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.nfc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NfcAntennaInfoTest {
+    private NfcAntennaInfo mNfcAntennaInfo;
+
+
+    @Before
+    public void setUp() {
+        AvailableNfcAntenna availableNfcAntenna = mock(AvailableNfcAntenna.class);
+        List<AvailableNfcAntenna> antennas = new ArrayList<>();
+        antennas.add(availableNfcAntenna);
+        mNfcAntennaInfo = new NfcAntennaInfo(1, 1, false, antennas);
+    }
+
+    @After
+    public void tearDown() {
+    }
+
+    @Test
+    public void testGetDeviceHeight() {
+        int height = mNfcAntennaInfo.getDeviceHeight();
+        assertThat(height).isEqualTo(1);
+    }
+
+    @Test
+    public void testGetDeviceWidth() {
+        int width = mNfcAntennaInfo.getDeviceWidth();
+        assertThat(width).isEqualTo(1);
+    }
+
+    @Test
+    public void testIsDeviceFoldable() {
+        boolean foldable = mNfcAntennaInfo.isDeviceFoldable();
+        assertThat(foldable).isFalse();
+    }
+
+    @Test
+    public void testGetAvailableNfcAntennas() {
+        List<AvailableNfcAntenna> antennas = mNfcAntennaInfo.getAvailableNfcAntennas();
+        assertThat(antennas).isNotNull();
+        assertThat(antennas.size()).isEqualTo(1);
+    }
+
+}
diff --git a/packages/NeuralNetworks/framework/Android.bp b/packages/NeuralNetworks/framework/Android.bp
index 6f45daa..af071ba 100644
--- a/packages/NeuralNetworks/framework/Android.bp
+++ b/packages/NeuralNetworks/framework/Android.bp
@@ -19,10 +19,21 @@
 filegroup {
     name: "framework-ondeviceintelligence-sources",
     srcs: [
-        "java/**/*.aidl",
-        "java/**/*.java",
+        "module/java/**/*.aidl",
+        "module/java/**/*.java",
     ],
-    path: "java",
+    visibility: [
+        "//frameworks/base:__subpackages__",
+        "//packages/modules/NeuralNetworks:__subpackages__",
+    ],
+}
+
+filegroup {
+    name: "framework-ondeviceintelligence-sources-platform",
+    srcs: [
+        "platform/java/**/*.aidl",
+        "platform/java/**/*.java",
+    ],
     visibility: [
         "//frameworks/base:__subpackages__",
         "//packages/modules/NeuralNetworks:__subpackages__",
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/DownloadCallback.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/DownloadCallback.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/DownloadCallback.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/DownloadCallback.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/Feature.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/Feature.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/Feature.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/Feature.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/Feature.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/Feature.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/Feature.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/Feature.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/FeatureDetails.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/FeatureDetails.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/FeatureDetails.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/FeatureDetails.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/FeatureDetails.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/FeatureDetails.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/FeatureDetails.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/FeatureDetails.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ICancellationSignal.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ICancellationSignal.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ICancellationSignal.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ICancellationSignal.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IDownloadCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IDownloadCallback.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IDownloadCallback.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IDownloadCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IFeatureCallback.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureCallback.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IFeatureCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IProcessingSignal.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IProcessingSignal.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IProcessingSignal.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IProcessingSignal.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IRemoteCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IRemoteCallback.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IRemoteCallback.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IRemoteCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IResponseCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IResponseCallback.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IResponseCallback.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IResponseCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/InferenceInfo.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingCallback.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ProcessingCallback.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingCallback.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ProcessingCallback.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingSignal.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ProcessingSignal.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingSignal.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/ProcessingSignal.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.aidl b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/TokenInfo.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.aidl
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/TokenInfo.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/TokenInfo.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/TokenInfo.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/utils/BinderUtils.java b/packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/utils/BinderUtils.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/utils/BinderUtils.java
rename to packages/NeuralNetworks/framework/module/java/android/app/ondeviceintelligence/utils/BinderUtils.java
diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
rename to packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
rename to packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl
rename to packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl
rename to packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl
rename to packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
rename to packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
similarity index 100%
rename from packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
rename to packages/NeuralNetworks/framework/module/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/DownloadCallback.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/DownloadCallback.java
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/DownloadCallback.java
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/DownloadCallback.java
diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.aidl
new file mode 100644
index 0000000..18494d7
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.aidl
@@ -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 android.app.ondeviceintelligence;
+
+/**
+  * @hide
+  */
+parcelable Feature;
diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.java
new file mode 100644
index 0000000..bcc56073
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/Feature.java
@@ -0,0 +1,267 @@
+/*
+ * 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.app.ondeviceintelligence;
+
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+
+/**
+ * Represents a typical feature associated with on-device intelligence.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
+public final class Feature implements Parcelable {
+    private final int mId;
+    @Nullable
+    private final String mName;
+    @Nullable
+    private final String mModelName;
+    private final int mType;
+    private final int mVariant;
+    @NonNull
+    private final PersistableBundle mFeatureParams;
+
+    /* package-private */ Feature(
+            int id,
+            @Nullable String name,
+            @Nullable String modelName,
+            int type,
+            int variant,
+            @NonNull PersistableBundle featureParams) {
+        this.mId = id;
+        this.mName = name;
+        this.mModelName = modelName;
+        this.mType = type;
+        this.mVariant = variant;
+        this.mFeatureParams = featureParams;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mFeatureParams);
+    }
+
+    /** Returns the unique and immutable identifier of this feature. */
+    public int getId() {
+        return mId;
+    }
+
+    /** Returns human-readable name of this feature. */
+    public @Nullable String getName() {
+        return mName;
+    }
+
+    /** Returns base model name of this feature. */
+    public @Nullable String getModelName() {
+        return mModelName;
+    }
+
+    /** Returns type identifier of this feature. */
+    public int getType() {
+        return mType;
+    }
+
+    /** Returns variant kind for this feature. */
+    public int getVariant() {
+        return mVariant;
+    }
+
+    public @NonNull PersistableBundle getFeatureParams() {
+        return mFeatureParams;
+    }
+
+    @Override
+    public String toString() {
+        return "Feature { " +
+                "id = " + mId + ", " +
+                "name = " + mName + ", " +
+                "modelName = " + mModelName + ", " +
+                "type = " + mType + ", " +
+                "variant = " + mVariant + ", " +
+                "featureParams = " + mFeatureParams +
+                " }";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        Feature that = (Feature) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && mId == that.mId
+                && java.util.Objects.equals(mName, that.mName)
+                && java.util.Objects.equals(mModelName, that.mModelName)
+                && mType == that.mType
+                && mVariant == that.mVariant
+                && java.util.Objects.equals(mFeatureParams, that.mFeatureParams);
+    }
+
+    @Override
+    public int hashCode() {
+        int _hash = 1;
+        _hash = 31 * _hash + mId;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mName);
+        _hash = 31 * _hash + java.util.Objects.hashCode(mModelName);
+        _hash = 31 * _hash + mType;
+        _hash = 31 * _hash + mVariant;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mFeatureParams);
+        return _hash;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        byte flg = 0;
+        if (mName != null) flg |= 0x2;
+        if (mModelName != null) flg |= 0x4;
+        dest.writeByte(flg);
+        dest.writeInt(mId);
+        if (mName != null) dest.writeString(mName);
+        if (mModelName != null) dest.writeString(mModelName);
+        dest.writeInt(mType);
+        dest.writeInt(mVariant);
+        dest.writeTypedObject(mFeatureParams, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    /* package-private */ Feature(@NonNull Parcel in) {
+        byte flg = in.readByte();
+        int id = in.readInt();
+        String name = (flg & 0x2) == 0 ? null : in.readString();
+        String modelName = (flg & 0x4) == 0 ? null : in.readString();
+        int type = in.readInt();
+        int variant = in.readInt();
+        PersistableBundle featureParams = (PersistableBundle) in.readTypedObject(
+                PersistableBundle.CREATOR);
+
+        this.mId = id;
+        this.mName = name;
+        this.mModelName = modelName;
+        this.mType = type;
+        this.mVariant = variant;
+        this.mFeatureParams = featureParams;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mFeatureParams);
+    }
+
+    public static final @NonNull Parcelable.Creator<Feature> CREATOR
+            = new Parcelable.Creator<Feature>() {
+        @Override
+        public Feature[] newArray(int size) {
+            return new Feature[size];
+        }
+
+        @Override
+        public Feature createFromParcel(@NonNull Parcel in) {
+            return new Feature(in);
+        }
+    };
+
+    /**
+     * A builder for {@link Feature}
+     */
+    @SuppressWarnings("WeakerAccess")
+    public static final class Builder {
+        private int mId;
+        private @Nullable String mName;
+        private @Nullable String mModelName;
+        private int mType;
+        private int mVariant;
+        private @NonNull PersistableBundle mFeatureParams;
+
+        private long mBuilderFieldsSet = 0L;
+
+        /**
+         * Provides a builder instance to create a feature for given id.
+         * @param id the unique identifier for the feature.
+         */
+        public Builder(int id) {
+            mId = id;
+            mFeatureParams = new PersistableBundle();
+        }
+
+        public @NonNull Builder setName(@NonNull String value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x2;
+            mName = value;
+            return this;
+        }
+
+        public @NonNull Builder setModelName(@NonNull String value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x4;
+            mModelName = value;
+            return this;
+        }
+
+        public @NonNull Builder setType(int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x8;
+            mType = value;
+            return this;
+        }
+
+        public @NonNull Builder setVariant(int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x10;
+            mVariant = value;
+            return this;
+        }
+
+        public @NonNull Builder setFeatureParams(@NonNull PersistableBundle value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x20;
+            mFeatureParams = value;
+            return this;
+        }
+
+        /** Builds the instance. This builder should not be touched after calling this! */
+        public @NonNull Feature build() {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x40; // Mark builder used
+
+            Feature o = new Feature(
+                    mId,
+                    mName,
+                    mModelName,
+                    mType,
+                    mVariant,
+                    mFeatureParams);
+            return o;
+        }
+
+        private void checkNotUsed() {
+            if ((mBuilderFieldsSet & 0x40) != 0) {
+                throw new IllegalStateException(
+                        "This Builder should not be reused. Use a new Builder instance instead");
+            }
+        }
+    }
+}
diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.aidl
new file mode 100644
index 0000000..0589bf8
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.aidl
@@ -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 android.app.ondeviceintelligence;
+
+/**
+  * @hide
+  */
+parcelable FeatureDetails;
diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.java
new file mode 100644
index 0000000..0ee0cc3
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/FeatureDetails.java
@@ -0,0 +1,178 @@
+/*
+ * 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.app.ondeviceintelligence;
+
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.text.MessageFormat;
+
+/**
+ * Represents a status of a requested {@link Feature}.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
+public final class FeatureDetails implements Parcelable {
+    @Status
+    private final int mFeatureStatus;
+    @NonNull
+    private final PersistableBundle mFeatureDetailParams;
+
+    /** Invalid or unavailable {@code AiFeature}. */
+    public static final int FEATURE_STATUS_UNAVAILABLE = 0;
+
+    /** Feature can be downloaded on request. */
+    public static final int FEATURE_STATUS_DOWNLOADABLE = 1;
+
+    /** Feature is being downloaded. */
+    public static final int FEATURE_STATUS_DOWNLOADING = 2;
+
+    /** Feature is fully downloaded and ready to use. */
+    public static final int FEATURE_STATUS_AVAILABLE = 3;
+
+    /** Underlying service is unavailable and feature status cannot be fetched. */
+    public static final int FEATURE_STATUS_SERVICE_UNAVAILABLE = 4;
+
+    /**
+     * @hide
+     */
+    @IntDef(value = {
+            FEATURE_STATUS_UNAVAILABLE,
+            FEATURE_STATUS_DOWNLOADABLE,
+            FEATURE_STATUS_DOWNLOADING,
+            FEATURE_STATUS_AVAILABLE,
+            FEATURE_STATUS_SERVICE_UNAVAILABLE
+    })
+    @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Status {
+    }
+
+    public FeatureDetails(
+            @Status int featureStatus,
+            @NonNull PersistableBundle featureDetailParams) {
+        this.mFeatureStatus = featureStatus;
+        com.android.internal.util.AnnotationValidations.validate(
+                Status.class, null, mFeatureStatus);
+        this.mFeatureDetailParams = featureDetailParams;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mFeatureDetailParams);
+    }
+
+    public FeatureDetails(
+            @Status int featureStatus) {
+        this.mFeatureStatus = featureStatus;
+        com.android.internal.util.AnnotationValidations.validate(
+                Status.class, null, mFeatureStatus);
+        this.mFeatureDetailParams = new PersistableBundle();
+    }
+
+
+    /**
+     * Returns an integer value associated with the feature status.
+     */
+    public @Status int getFeatureStatus() {
+        return mFeatureStatus;
+    }
+
+
+    /**
+     * Returns a persistable bundle contain any additional status related params.
+     */
+    public @NonNull PersistableBundle getFeatureDetailParams() {
+        return mFeatureDetailParams;
+    }
+
+    @Override
+    public String toString() {
+        return MessageFormat.format("FeatureDetails '{' status = {0}, "
+                        + "persistableBundle = {1} '}'",
+                mFeatureStatus,
+                mFeatureDetailParams);
+    }
+
+    @Override
+    public boolean equals(@android.annotation.Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        FeatureDetails that = (FeatureDetails) o;
+        return mFeatureStatus == that.mFeatureStatus
+                && java.util.Objects.equals(mFeatureDetailParams, that.mFeatureDetailParams);
+    }
+
+    @Override
+    public int hashCode() {
+        int _hash = 1;
+        _hash = 31 * _hash + mFeatureStatus;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mFeatureDetailParams);
+        return _hash;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        dest.writeInt(mFeatureStatus);
+        dest.writeTypedObject(mFeatureDetailParams, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    FeatureDetails(@NonNull android.os.Parcel in) {
+        int status = in.readInt();
+        PersistableBundle persistableBundle = (PersistableBundle) in.readTypedObject(
+                PersistableBundle.CREATOR);
+
+        this.mFeatureStatus = status;
+        com.android.internal.util.AnnotationValidations.validate(
+                Status.class, null, mFeatureStatus);
+        this.mFeatureDetailParams = persistableBundle;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mFeatureDetailParams);
+    }
+
+
+    public static final @NonNull Parcelable.Creator<FeatureDetails> CREATOR =
+            new Parcelable.Creator<>() {
+                @Override
+                public FeatureDetails[] newArray(int size) {
+                    return new FeatureDetails[size];
+                }
+
+                @Override
+                public FeatureDetails createFromParcel(@NonNull android.os.Parcel in) {
+                    return new FeatureDetails(in);
+                }
+            };
+
+}
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ICancellationSignal.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ICancellationSignal.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ICancellationSignal.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ICancellationSignal.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IDownloadCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IDownloadCallback.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IDownloadCallback.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IDownloadCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureCallback.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureCallback.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IFeatureDetailsCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IListFeaturesCallback.aidl
diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
new file mode 100644
index 0000000..1977a39
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
@@ -0,0 +1,79 @@
+/*
+ * 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 android.app.ondeviceintelligence;
+
+ import com.android.internal.infra.AndroidFuture;
+ import android.os.ICancellationSignal;
+ import android.os.ParcelFileDescriptor;
+ import android.os.PersistableBundle;
+ import android.os.RemoteCallback;
+ import android.os.Bundle;
+ import android.app.ondeviceintelligence.Feature;
+ import android.app.ondeviceintelligence.FeatureDetails;
+ import android.app.ondeviceintelligence.InferenceInfo;
+ import java.util.List;
+ import android.app.ondeviceintelligence.IDownloadCallback;
+ import android.app.ondeviceintelligence.IListFeaturesCallback;
+ import android.app.ondeviceintelligence.IFeatureCallback;
+ import android.app.ondeviceintelligence.IFeatureDetailsCallback;
+ import android.app.ondeviceintelligence.IResponseCallback;
+ import android.app.ondeviceintelligence.IStreamingResponseCallback;
+ import android.app.ondeviceintelligence.IProcessingSignal;
+ import android.app.ondeviceintelligence.ITokenInfoCallback;
+
+
+ /**
+  * Interface for a OnDeviceIntelligenceManager for managing OnDeviceIntelligenceService and OnDeviceSandboxedInferenceService.
+  *
+  * @hide
+  */
+interface IOnDeviceIntelligenceManager {
+      @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
+      void getVersion(in RemoteCallback remoteCallback) = 1;
+
+      @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
+      void getFeature(in int featureId, in IFeatureCallback remoteCallback) = 2;
+
+      @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
+      void listFeatures(in IListFeaturesCallback listFeaturesCallback) = 3;
+
+      @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
+      void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback) = 4;
+
+      @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
+      void requestFeatureDownload(in Feature feature, in  AndroidFuture cancellationSignalFuture, in IDownloadCallback callback) = 5;
+
+      @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
+      void requestTokenInfo(in Feature feature, in Bundle requestBundle, in  AndroidFuture cancellationSignalFuture,
+                                                        in ITokenInfoCallback tokenInfocallback) = 6;
+
+      @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
+      void processRequest(in Feature feature, in Bundle requestBundle, int requestType,
+                                                in  AndroidFuture cancellationSignalFuture,
+                                                in AndroidFuture processingSignalFuture,
+                                                in IResponseCallback responseCallback) = 7;
+
+      @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
+      void processRequestStreaming(in Feature feature,
+                    in Bundle requestBundle, int requestType, in  AndroidFuture cancellationSignalFuture,
+                    in  AndroidFuture processingSignalFuture,
+                    in IStreamingResponseCallback streamingCallback) = 8;
+
+      String getRemoteServicePackageName() = 9;
+
+      List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) = 10;
+ }
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IProcessingSignal.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IProcessingSignal.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IProcessingSignal.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IProcessingSignal.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IRemoteCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IRemoteCallback.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IRemoteCallback.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IRemoteCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IResponseCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IResponseCallback.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IResponseCallback.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IResponseCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/IStreamingResponseCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ITokenInfoCallback.aidl
diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.aidl
new file mode 100644
index 0000000..6d70fc4
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.aidl
@@ -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 android.app.ondeviceintelligence;
+
+/**
+  * @hide
+  */
+parcelable InferenceInfo;
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.java
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/InferenceInfo.java
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/InferenceInfo.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceException.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceFrameworkInitializer.java
diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
new file mode 100644
index 0000000..78cf1d7
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
@@ -0,0 +1,642 @@
+/*
+ * 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.app.ondeviceintelligence;
+
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE;
+
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.OutcomeReceiver;
+import android.os.PersistableBundle;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.LongConsumer;
+
+/**
+ * Allows granted apps to manage on-device intelligence service configured on the device. Typical
+ * calling pattern will be to query and setup a required feature before proceeding to request
+ * processing.
+ *
+ * The contracts in this Manager class are designed to be open-ended in general, to allow
+ * interoperability. Therefore, it is recommended that implementations of this system-service
+ * expose this API to the clients via a separate sdk or library which has more defined contract.
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.ON_DEVICE_INTELLIGENCE_SERVICE)
+@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
+public final class OnDeviceIntelligenceManager {
+    /**
+     * @hide
+     */
+    public static final String API_VERSION_BUNDLE_KEY = "ApiVersionBundleKey";
+
+    /**
+     * @hide
+     */
+    public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY =
+            "AugmentRequestContentBundleKey";
+
+    private static final String TAG = "OnDeviceIntelligence";
+    private final Context mContext;
+    private final IOnDeviceIntelligenceManager mService;
+
+    /**
+     * @hide
+     */
+    public OnDeviceIntelligenceManager(Context context, IOnDeviceIntelligenceManager service) {
+        mContext = context;
+        mService = service;
+    }
+
+    /**
+     * Asynchronously get the version of the underlying remote implementation.
+     *
+     * @param versionConsumer  consumer to populate the version of remote implementation.
+     * @param callbackExecutor executor to run the callback on.
+     */
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void getVersion(
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull LongConsumer versionConsumer) {
+        try {
+            RemoteCallback callback = new RemoteCallback(result -> {
+                if (result == null) {
+                    Binder.withCleanCallingIdentity(
+                            () -> callbackExecutor.execute(() -> versionConsumer.accept(0)));
+                }
+                long version = result.getLong(API_VERSION_BUNDLE_KEY);
+                Binder.withCleanCallingIdentity(
+                        () -> callbackExecutor.execute(() -> versionConsumer.accept(version)));
+            });
+            mService.getVersion(callback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
+     * Get package name configured for providing the remote implementation for this system service.
+     */
+    @Nullable
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public String getRemoteServicePackageName() {
+        String result;
+        try {
+            result = mService.getRemoteServicePackageName();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return result;
+    }
+
+    /**
+     * Asynchronously get feature for a given id.
+     *
+     * @param featureId        the identifier pointing to the feature.
+     * @param featureReceiver  callback to populate the feature object for given identifier.
+     * @param callbackExecutor executor to run the callback on.
+     */
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void getFeature(
+            int featureId,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull OutcomeReceiver<Feature, OnDeviceIntelligenceException> featureReceiver) {
+        try {
+            IFeatureCallback callback =
+                    new IFeatureCallback.Stub() {
+                        @Override
+                        public void onSuccess(Feature result) {
+                            Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                                    () -> featureReceiver.onResult(result)));
+                        }
+
+                        @Override
+                        public void onFailure(int errorCode, String errorMessage,
+                                PersistableBundle errorParams) {
+                            Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                                    () -> featureReceiver.onError(
+                                            new OnDeviceIntelligenceException(
+                                                    errorCode, errorMessage, errorParams))));
+                        }
+                    };
+            mService.getFeature(featureId, callback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Asynchronously get a list of features that are supported for the caller.
+     *
+     * @param featureListReceiver callback to populate the list of features.
+     * @param callbackExecutor    executor to run the callback on.
+     */
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void listFeatures(
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull OutcomeReceiver<List<Feature>, OnDeviceIntelligenceException> featureListReceiver) {
+        try {
+            IListFeaturesCallback callback =
+                    new IListFeaturesCallback.Stub() {
+                        @Override
+                        public void onSuccess(List<Feature> result) {
+                            Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                                    () -> featureListReceiver.onResult(result)));
+                        }
+
+                        @Override
+                        public void onFailure(int errorCode, String errorMessage,
+                                PersistableBundle errorParams) {
+                            Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                                    () -> featureListReceiver.onError(
+                                            new OnDeviceIntelligenceException(
+                                                    errorCode, errorMessage, errorParams))));
+                        }
+                    };
+            mService.listFeatures(callback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * This method should be used to fetch details about a feature which need some additional
+     * computation, that can be inefficient to return in all calls to {@link #getFeature}. Callers
+     * and implementation can utilize the {@link Feature#getFeatureParams()} to pass hint on what
+     * details are expected by the caller.
+     *
+     * @param feature                the feature to check status for.
+     * @param featureDetailsReceiver callback to populate the feature details to.
+     * @param callbackExecutor       executor to run the callback on.
+     */
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void getFeatureDetails(@NonNull Feature feature,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull OutcomeReceiver<FeatureDetails, OnDeviceIntelligenceException> featureDetailsReceiver) {
+        try {
+            IFeatureDetailsCallback callback = new IFeatureDetailsCallback.Stub() {
+
+                @Override
+                public void onSuccess(FeatureDetails result) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> featureDetailsReceiver.onResult(result)));
+                }
+
+                @Override
+                public void onFailure(int errorCode, String errorMessage,
+                        PersistableBundle errorParams) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> featureDetailsReceiver.onError(
+                                    new OnDeviceIntelligenceException(errorCode,
+                                            errorMessage, errorParams))));
+                }
+            };
+            mService.getFeatureDetails(feature, callback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * This method handles downloading all model and config files required to process requests
+     * sent against a given feature. The caller can listen to updates on the download status via
+     * the callback.
+     *
+     * Note: If a feature was already requested for downloaded previously, the onDownloadFailed
+     * callback would be invoked with {@link DownloadCallback#DOWNLOAD_FAILURE_STATUS_DOWNLOADING}.
+     * In such cases, clients should query the feature status via {@link #getFeatureDetails} to
+     * check on the feature's download status.
+     *
+     * @param feature            feature to request download for.
+     * @param callback           callback to populate updates about download status.
+     * @param cancellationSignal signal to invoke cancellation on the operation in the remote
+     *                           implementation.
+     * @param callbackExecutor   executor to run the callback on.
+     */
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void requestFeatureDownload(@NonNull Feature feature,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull DownloadCallback callback) {
+        try {
+            IDownloadCallback downloadCallback = new IDownloadCallback.Stub() {
+
+                @Override
+                public void onDownloadStarted(long bytesToDownload) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> callback.onDownloadStarted(bytesToDownload)));
+                }
+
+                @Override
+                public void onDownloadProgress(long bytesDownloaded) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> callback.onDownloadProgress(bytesDownloaded)));
+                }
+
+                @Override
+                public void onDownloadFailed(int failureStatus, String errorMessage,
+                        PersistableBundle errorParams) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> callback.onDownloadFailed(failureStatus, errorMessage,
+                                    errorParams)));
+                }
+
+                @Override
+                public void onDownloadCompleted(PersistableBundle downloadParams) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> callback.onDownloadCompleted(downloadParams)));
+                }
+            };
+
+            mService.requestFeatureDownload(feature,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    downloadCallback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
+     * The methods computes the token related information for a given request payload using the
+     * provided {@link Feature}.
+     *
+     * @param feature            feature associated with the request.
+     * @param request            request and associated params represented by the Bundle
+     *                           data.
+     * @param outcomeReceiver    callback to populate the token info or exception in case of
+     *                           failure.
+     * @param cancellationSignal signal to invoke cancellation on the operation in the remote
+     *                           implementation.
+     * @param callbackExecutor   executor to run the callback on.
+     */
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void requestTokenInfo(@NonNull Feature feature, @NonNull @InferenceParams Bundle request,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull OutcomeReceiver<TokenInfo,
+                    OnDeviceIntelligenceException> outcomeReceiver) {
+        try {
+            ITokenInfoCallback callback = new ITokenInfoCallback.Stub() {
+                @Override
+                public void onSuccess(TokenInfo tokenInfo) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> outcomeReceiver.onResult(tokenInfo)));
+                }
+
+                @Override
+                public void onFailure(int errorCode, String errorMessage,
+                        PersistableBundle errorParams) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> outcomeReceiver.onError(
+                                    new OnDeviceIntelligenceException(
+                                            errorCode, errorMessage, errorParams))));
+                }
+            };
+
+            mService.requestTokenInfo(feature, request,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    callback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
+     * Asynchronously Process a request based on the associated params, to populate a
+     * response in
+     * {@link OutcomeReceiver#onResult} callback or failure callback status code if there
+     * was a
+     * failure.
+     *
+     * @param feature            feature associated with the request.
+     * @param request            request and associated params represented by the Bundle
+     *                           data.
+     * @param requestType        type of request being sent for processing the content.
+     * @param cancellationSignal signal to invoke cancellation.
+     * @param processingSignal   signal to send custom signals in the
+     *                           remote implementation.
+     * @param callbackExecutor   executor to run the callback on.
+     * @param processingCallback callback to populate the response content and
+     *                           associated params.
+     */
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void processRequest(@NonNull Feature feature, @NonNull @InferenceParams Bundle request,
+            @RequestType int requestType,
+            @Nullable CancellationSignal cancellationSignal,
+            @Nullable ProcessingSignal processingSignal,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull ProcessingCallback processingCallback) {
+        try {
+            IResponseCallback callback = new IResponseCallback.Stub() {
+                @Override
+                public void onSuccess(@InferenceParams Bundle result) {
+                    Binder.withCleanCallingIdentity(() -> {
+                        callbackExecutor.execute(() -> processingCallback.onResult(result));
+                    });
+                }
+
+                @Override
+                public void onFailure(int errorCode, String errorMessage,
+                        PersistableBundle errorParams) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> processingCallback.onError(
+                                    new OnDeviceIntelligenceException(
+                                            errorCode, errorMessage, errorParams))));
+                }
+
+                @Override
+                public void onDataAugmentRequest(@NonNull @InferenceParams Bundle request,
+                        @NonNull RemoteCallback contentCallback) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> processingCallback.onDataAugmentRequest(request, result -> {
+                                Bundle bundle = new Bundle();
+                                bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY, result);
+                                callbackExecutor.execute(() -> contentCallback.sendResult(bundle));
+                            })));
+                }
+            };
+
+
+            mService.processRequest(feature, request, requestType,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor),
+                    callback);
+
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Variation of {@link #processRequest} that asynchronously processes a request in a
+     * streaming
+     * fashion, where new content is pushed to caller in chunks via the
+     * {@link StreamingProcessingCallback#onPartialResult}. After the streaming is complete,
+     * the service should call {@link StreamingProcessingCallback#onResult} and can optionally
+     * populate the complete the full response {@link Bundle} as part of the callback in cases
+     * when the final response contains an enhanced aggregation of the contents already
+     * streamed.
+     *
+     * @param feature                     feature associated with the request.
+     * @param request                     request and associated params represented by the Bundle
+     *                                    data.
+     * @param requestType                 type of request being sent for processing the content.
+     * @param cancellationSignal          signal to invoke cancellation.
+     * @param processingSignal            signal to send custom signals in the
+     *                                    remote implementation.
+     * @param streamingProcessingCallback streaming callback to populate the response content and
+     *                                    associated params.
+     * @param callbackExecutor            executor to run the callback on.
+     */
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void processRequestStreaming(@NonNull Feature feature,
+            @NonNull @InferenceParams Bundle request,
+            @RequestType int requestType,
+            @Nullable CancellationSignal cancellationSignal,
+            @Nullable ProcessingSignal processingSignal,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull StreamingProcessingCallback streamingProcessingCallback) {
+        try {
+            IStreamingResponseCallback callback = new IStreamingResponseCallback.Stub() {
+                @Override
+                public void onNewContent(@InferenceParams Bundle result) {
+                    Binder.withCleanCallingIdentity(() -> {
+                        callbackExecutor.execute(
+                                () -> streamingProcessingCallback.onPartialResult(result));
+                    });
+                }
+
+                @Override
+                public void onSuccess(@InferenceParams Bundle result) {
+                    Binder.withCleanCallingIdentity(() -> {
+                        callbackExecutor.execute(
+                                () -> streamingProcessingCallback.onResult(result));
+                    });
+                }
+
+                @Override
+                public void onFailure(int errorCode, String errorMessage,
+                        PersistableBundle errorParams) {
+                    Binder.withCleanCallingIdentity(() -> {
+                        callbackExecutor.execute(
+                                () -> streamingProcessingCallback.onError(
+                                        new OnDeviceIntelligenceException(
+                                                errorCode, errorMessage, errorParams)));
+                    });
+                }
+
+
+                @Override
+                public void onDataAugmentRequest(@NonNull @InferenceParams Bundle content,
+                        @NonNull RemoteCallback contentCallback) {
+                    Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                            () -> streamingProcessingCallback.onDataAugmentRequest(content,
+                                    contentResponse -> {
+                                        Bundle bundle = new Bundle();
+                                        bundle.putParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY,
+                                                contentResponse);
+                                        callbackExecutor.execute(
+                                                () -> contentCallback.sendResult(bundle));
+                                    })));
+                }
+            };
+
+            mService.processRequestStreaming(
+                    feature, request, requestType,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor),
+                    callback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * This is primarily intended to be used to attribute/blame on-device intelligence power usage,
+     * via the configured remote implementation, to its actual caller.
+     *
+     * @param startTimeEpochMillis epoch millis used to filter the InferenceInfo events.
+     * @return InferenceInfo events since the passed in startTimeEpochMillis.
+     */
+    @RequiresPermission(Manifest.permission.DUMP)
+    @FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE)
+    public @NonNull List<InferenceInfo> getLatestInferenceInfo(@CurrentTimeMillisLong long startTimeEpochMillis) {
+        try {
+            return mService.getLatestInferenceInfo(startTimeEpochMillis);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
+    /** Request inference with provided Bundle and Params. */
+    public static final int REQUEST_TYPE_INFERENCE = 0;
+
+    /**
+     * Prepares the remote implementation environment for e.g.loading inference runtime etc
+     * .which
+     * are time consuming beforehand to remove overhead and allow quick processing of requests
+     * thereof.
+     */
+    public static final int REQUEST_TYPE_PREPARE = 1;
+
+    /** Request Embeddings of the passed-in Bundle. */
+    public static final int REQUEST_TYPE_EMBEDDINGS = 2;
+
+    /**
+     * @hide
+     */
+    @IntDef(value = {
+            REQUEST_TYPE_INFERENCE,
+            REQUEST_TYPE_PREPARE,
+            REQUEST_TYPE_EMBEDDINGS
+    })
+    @Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.PARAMETER,
+            ElementType.FIELD})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RequestType {
+    }
+
+    /**
+     * {@link Bundle}s annotated with this type will be validated that they are in-effect read-only
+     * when passed via Binder IPC. Following restrictions apply :
+     * <ul>
+     * <li> {@link PersistableBundle}s are allowed.</li>
+     * <li> Any primitive types or their collections can be added as usual.</li>
+     * <li>IBinder objects should *not* be added.</li>
+     * <li>Parcelable data which has no active-objects, should be added as
+     * {@link Bundle#putByteArray}</li>
+     * <li>Parcelables have active-objects, only following types will be allowed</li>
+     * <ul>
+     *  <li>{@link android.os.ParcelFileDescriptor} opened in
+     *  {@link android.os.ParcelFileDescriptor#MODE_READ_ONLY}</li>
+     * </ul>
+     * </ul>
+     *
+     * In all other scenarios the system-server might throw a
+     * {@link android.os.BadParcelableException} if the Bundle validation fails.
+     *
+     * @hide
+     */
+    @Target({ElementType.PARAMETER, ElementType.FIELD})
+    public @interface StateParams {
+    }
+
+    /**
+     * This is an extension of {@link StateParams} but for purpose of inference few other types are
+     * also allowed as read-only, as listed below.
+     *
+     * <li>{@link Bitmap} set as immutable.</li>
+     * <li>{@link android.database.CursorWindow}</li>
+     * <li>{@link android.os.SharedMemory} set to {@link OsConstants#PROT_READ}</li>
+     * </ul>
+     * </ul>
+     *
+     * In all other scenarios the system-server might throw a
+     * {@link android.os.BadParcelableException} if the Bundle validation fails.
+     *
+     * @hide
+     */
+    @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE})
+    public @interface InferenceParams {
+    }
+
+    /**
+     * This is an extension of {@link StateParams} with the exception that it allows writing
+     * {@link Bitmap} as part of the response.
+     *
+     * In all other scenarios the system-server might throw a
+     * {@link android.os.BadParcelableException} if the Bundle validation fails.
+     *
+     * @hide
+     */
+    @Target({ElementType.PARAMETER, ElementType.FIELD})
+    public @interface ResponseParams {
+    }
+
+    @Nullable
+    private static AndroidFuture<IBinder> configureRemoteCancellationFuture(
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor) {
+        if (cancellationSignal == null) {
+            return null;
+        }
+        AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>();
+        cancellationFuture.whenCompleteAsync(
+                (cancellationTransport, error) -> {
+                    if (error != null || cancellationTransport == null) {
+                        Log.e(TAG, "Unable to receive the remote cancellation signal.", error);
+                    } else {
+                        cancellationSignal.setRemote(
+                                ICancellationSignal.Stub.asInterface(cancellationTransport));
+                    }
+                }, callbackExecutor);
+        return cancellationFuture;
+    }
+
+    @Nullable
+    private static AndroidFuture<IBinder> configureRemoteProcessingSignalFuture(
+            ProcessingSignal processingSignal, Executor executor) {
+        if (processingSignal == null) {
+            return null;
+        }
+        AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>();
+        processingSignalFuture.whenCompleteAsync(
+                (transport, error) -> {
+                    if (error != null || transport == null) {
+                        Log.e(TAG, "Unable to receive the remote processing signal.", error);
+                    } else {
+                        processingSignal.setRemote(IProcessingSignal.Stub.asInterface(transport));
+                    }
+                }, executor);
+        return processingSignalFuture;
+    }
+
+
+}
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingCallback.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingCallback.java
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingCallback.java
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingCallback.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingSignal.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingSignal.java
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/ProcessingSignal.java
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/ProcessingSignal.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/StreamingProcessingCallback.java
diff --git a/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.aidl b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.aidl
new file mode 100644
index 0000000..2c19c1e
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.aidl
@@ -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 android.app.ondeviceintelligence;
+
+/**
+  * @hide
+  */
+parcelable TokenInfo;
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.java
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/TokenInfo.java
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/TokenInfo.java
diff --git a/packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/utils/BinderUtils.java b/packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/utils/BinderUtils.java
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/app/ondeviceintelligence/utils/BinderUtils.java
copy to packages/NeuralNetworks/framework/platform/java/android/app/ondeviceintelligence/utils/BinderUtils.java
diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
new file mode 100644
index 0000000..45c4350
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.ondeviceintelligence;
+
+import android.os.PersistableBundle;
+import android.os.ParcelFileDescriptor;
+import android.os.ICancellationSignal;
+import android.os.RemoteCallback;
+import android.app.ondeviceintelligence.IDownloadCallback;
+import android.app.ondeviceintelligence.Feature;
+import android.app.ondeviceintelligence.IFeatureCallback;
+import android.app.ondeviceintelligence.IListFeaturesCallback;
+import android.app.ondeviceintelligence.IFeatureDetailsCallback;
+import com.android.internal.infra.AndroidFuture;
+import android.service.ondeviceintelligence.IRemoteProcessingService;
+
+
+/**
+ * Interface for a concrete implementation to provide on device intelligence services.
+ *
+ * @hide
+ */
+oneway interface IOnDeviceIntelligenceService {
+    void getVersion(in RemoteCallback remoteCallback);
+    void getFeature(int callerUid, int featureId, in IFeatureCallback featureCallback);
+    void listFeatures(int callerUid, in IListFeaturesCallback listFeaturesCallback);
+    void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback);
+    void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future);
+    void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback);
+    void requestFeatureDownload(int callerUid, in Feature feature,
+                                in AndroidFuture cancellationSignal,
+                                in IDownloadCallback downloadCallback);
+    void registerRemoteServices(in IRemoteProcessingService remoteProcessingService);
+    void notifyInferenceServiceConnected();
+    void notifyInferenceServiceDisconnected();
+    void ready();
+}
\ No newline at end of file
diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
new file mode 100644
index 0000000..1af3b0f
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.ondeviceintelligence;
+
+import android.app.ondeviceintelligence.IStreamingResponseCallback;
+import android.app.ondeviceintelligence.IResponseCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
+import android.app.ondeviceintelligence.IProcessingSignal;
+import android.app.ondeviceintelligence.Feature;
+import android.os.IRemoteCallback;
+import android.os.ICancellationSignal;
+import android.os.PersistableBundle;
+import android.os.Bundle;
+import com.android.internal.infra.AndroidFuture;
+import android.service.ondeviceintelligence.IRemoteStorageService;
+import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback;
+
+/**
+ * Interface for a concrete implementation to provide on-device sandboxed inference.
+ *
+ * @hide
+ */
+oneway interface IOnDeviceSandboxedInferenceService {
+    void registerRemoteStorageService(in IRemoteStorageService storageService,
+                                        in IRemoteCallback remoteCallback) = 0;
+    void requestTokenInfo(int callerUid, in Feature feature, in Bundle request,
+                            in AndroidFuture cancellationSignal,
+                            in ITokenInfoCallback tokenInfoCallback) = 1;
+    void processRequest(int callerUid, in Feature feature, in Bundle request, in int requestType,
+                        in AndroidFuture cancellationSignal,
+                        in AndroidFuture processingSignal,
+                        in IResponseCallback callback) = 2;
+    void processRequestStreaming(int callerUid, in Feature feature, in Bundle request, in int requestType,
+                                in AndroidFuture cancellationSignal,
+                                in AndroidFuture processingSignal,
+                                in IStreamingResponseCallback callback) = 3;
+    void updateProcessingState(in Bundle processingState,
+                                     in IProcessingUpdateStatusCallback callback) = 4;
+}
\ No newline at end of file
diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IProcessingUpdateStatusCallback.aidl
diff --git a/packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl
similarity index 100%
copy from packages/NeuralNetworks/framework/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl
copy to packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteProcessingService.aidl
diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl
new file mode 100644
index 0000000..a6f49e1
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/IRemoteStorageService.aidl
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.ondeviceintelligence;
+
+import android.app.ondeviceintelligence.Feature;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteCallback;
+
+import com.android.internal.infra.AndroidFuture;
+
+/**
+ * Interface for a concrete implementation to provide access to storage read access
+ * for the isolated process.
+ *
+ * @hide
+ */
+oneway interface IRemoteStorageService {
+    void getReadOnlyFileDescriptor(in String filePath, in AndroidFuture<ParcelFileDescriptor> future);
+    void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback);
+}
\ No newline at end of file
diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
new file mode 100644
index 0000000..618d2a0
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
@@ -0,0 +1,552 @@
+/*
+ * 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 android.service.ondeviceintelligence;
+
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.app.ondeviceintelligence.DownloadCallback;
+import android.app.ondeviceintelligence.Feature;
+import android.app.ondeviceintelligence.FeatureDetails;
+import android.app.ondeviceintelligence.IDownloadCallback;
+import android.app.ondeviceintelligence.IFeatureCallback;
+import android.app.ondeviceintelligence.IFeatureDetailsCallback;
+import android.app.ondeviceintelligence.IListFeaturesCallback;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceException;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceManager;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.StateParams;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.Looper;
+import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+import java.util.function.LongConsumer;
+
+/**
+ * Abstract base class for performing setup for on-device inference and providing file access to
+ * the isolated counter part {@link OnDeviceSandboxedInferenceService}.
+ *
+ * <p> A service that provides configuration and model files relevant to performing inference on
+ * device. The system's default OnDeviceIntelligenceService implementation is configured in
+ * {@code config_defaultOnDeviceIntelligenceService}. If this config has no value, a stub is
+ * returned.
+ *
+ * <p> Similar to {@link OnDeviceIntelligenceManager} class, the contracts in this service are
+ * defined to be open-ended in general, to allow interoperability. Therefore, it is recommended
+ * that implementations of this system-service expose this API to the clients via a library which
+ * has more defined contract.</p>
+ * <pre>
+ * {@literal
+ * <service android:name=".SampleOnDeviceIntelligenceService"
+ *          android:permission="android.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE">
+ * </service>}
+ * </pre>
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
+public abstract class OnDeviceIntelligenceService extends Service {
+    private static final String TAG = OnDeviceIntelligenceService.class.getSimpleName();
+
+    private volatile IRemoteProcessingService mRemoteProcessingService;
+    private Handler mHandler;
+
+    @CallSuper
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mHandler = new Handler(Looper.getMainLooper(), null /* callback */, true /* async */);
+    }
+
+    /**
+     * The {@link Intent} that must be declared as handled by the service. To be supported, the
+     * service must also require the
+     * {@link android.Manifest.permission#BIND_ON_DEVICE_INTELLIGENCE_SERVICE}
+     * permission so that other applications can not abuse it.
+     */
+    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+    public static final String SERVICE_INTERFACE =
+            "android.service.ondeviceintelligence.OnDeviceIntelligenceService";
+
+
+    /**
+     * @hide
+     */
+    @Nullable
+    @Override
+    public final IBinder onBind(@NonNull Intent intent) {
+        if (SERVICE_INTERFACE.equals(intent.getAction())) {
+            return new IOnDeviceIntelligenceService.Stub() {
+                /** {@inheritDoc} */
+                @Override
+                public void ready() {
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(OnDeviceIntelligenceService::onReady,
+                                    OnDeviceIntelligenceService.this));
+                }
+
+                @Override
+                public void getVersion(RemoteCallback remoteCallback) {
+                    Objects.requireNonNull(remoteCallback);
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceIntelligenceService::onGetVersion,
+                                    OnDeviceIntelligenceService.this, l -> {
+                                        Bundle b = new Bundle();
+                                        b.putLong(
+                                                OnDeviceIntelligenceManager.API_VERSION_BUNDLE_KEY,
+                                                l);
+                                        remoteCallback.sendResult(b);
+                                    }));
+                }
+
+                @Override
+                public void listFeatures(int callerUid,
+                        IListFeaturesCallback listFeaturesCallback) {
+                    Objects.requireNonNull(listFeaturesCallback);
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceIntelligenceService::onListFeatures,
+                                    OnDeviceIntelligenceService.this, callerUid,
+                                    wrapListFeaturesCallback(listFeaturesCallback)));
+                }
+
+                @Override
+                public void getFeature(int callerUid, int id, IFeatureCallback featureCallback) {
+                    Objects.requireNonNull(featureCallback);
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceIntelligenceService::onGetFeature,
+                                    OnDeviceIntelligenceService.this, callerUid,
+                                    id, wrapFeatureCallback(featureCallback)));
+                }
+
+
+                @Override
+                public void getFeatureDetails(int callerUid, Feature feature,
+                        IFeatureDetailsCallback featureDetailsCallback) {
+                    Objects.requireNonNull(feature);
+                    Objects.requireNonNull(featureDetailsCallback);
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceIntelligenceService::onGetFeatureDetails,
+                                    OnDeviceIntelligenceService.this, callerUid,
+                                    feature, wrapFeatureDetailsCallback(featureDetailsCallback)));
+                }
+
+                @Override
+                public void requestFeatureDownload(int callerUid, Feature feature,
+                        AndroidFuture cancellationSignalFuture,
+                        IDownloadCallback downloadCallback) {
+                    Objects.requireNonNull(feature);
+                    Objects.requireNonNull(downloadCallback);
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceIntelligenceService::onDownloadFeature,
+                                    OnDeviceIntelligenceService.this, callerUid,
+                                    feature,
+                                    CancellationSignal.fromTransport(transport),
+                                    wrapDownloadCallback(downloadCallback)));
+                }
+
+                @Override
+                public void getReadOnlyFileDescriptor(String fileName,
+                        AndroidFuture<ParcelFileDescriptor> future) {
+                    Objects.requireNonNull(fileName);
+                    Objects.requireNonNull(future);
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceIntelligenceService::onGetReadOnlyFileDescriptor,
+                                    OnDeviceIntelligenceService.this, fileName,
+                                    future));
+                }
+
+                @Override
+                public void getReadOnlyFeatureFileDescriptorMap(
+                        Feature feature, RemoteCallback remoteCallback) {
+                    Objects.requireNonNull(feature);
+                    Objects.requireNonNull(remoteCallback);
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceIntelligenceService::onGetReadOnlyFeatureFileDescriptorMap,
+                                    OnDeviceIntelligenceService.this, feature,
+                                    parcelFileDescriptorMap -> {
+                                        Bundle bundle = new Bundle();
+                                        parcelFileDescriptorMap.forEach(bundle::putParcelable);
+                                        remoteCallback.sendResult(bundle);
+                                        tryClosePfds(parcelFileDescriptorMap.values());
+                                    }));
+                }
+
+                @Override
+                public void registerRemoteServices(
+                        IRemoteProcessingService remoteProcessingService) {
+                    mRemoteProcessingService = remoteProcessingService;
+                }
+
+                @Override
+                public void notifyInferenceServiceConnected() {
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceIntelligenceService::onInferenceServiceConnected,
+                                    OnDeviceIntelligenceService.this));
+                }
+
+                @Override
+                public void notifyInferenceServiceDisconnected() {
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceIntelligenceService::onInferenceServiceDisconnected,
+                                    OnDeviceIntelligenceService.this));
+                }
+            };
+        }
+        Slog.w(TAG, "Incorrect service interface, returning null.");
+        return null;
+    }
+
+    /**
+     * Using this signal to assertively a signal each time service binds successfully, used only in
+     * tests to get a signal that service instance is ready. This is needed because we cannot rely
+     * on {@link #onCreate} or {@link #onBind} to be invoke on each binding.
+     */
+    public void onReady() {
+    }
+
+
+    /**
+     * Invoked when a new instance of the remote inference service is created.
+     * This method should be used as a signal to perform any initialization operations, for e.g. by
+     * invoking the {@link #updateProcessingState} method to initialize the remote processing
+     * service.
+     */
+    public abstract void onInferenceServiceConnected();
+
+
+    /**
+     * Invoked when an instance of the remote inference service is disconnected.
+     */
+    public abstract void onInferenceServiceDisconnected();
+
+
+    /**
+     * Invoked by the {@link OnDeviceIntelligenceService} inorder to send updates to the inference
+     * service if there is a state change to be performed. State change could be config updates,
+     * performing initialization or cleanup tasks in the remote inference service.
+     * The Bundle passed in here is expected to be read-only and will be rejected if it has any
+     * writable fields as detailed under {@link StateParams}.
+     *
+     * @param processingState  the updated state to be applied.
+     * @param callbackExecutor executor to the run status callback on.
+     * @param statusReceiver   receiver to get status of the update state operation.
+     */
+    public final void updateProcessingState(@NonNull @StateParams Bundle processingState,
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull OutcomeReceiver<PersistableBundle, OnDeviceIntelligenceException> statusReceiver) {
+        Objects.requireNonNull(callbackExecutor);
+        if (mRemoteProcessingService == null) {
+            throw new IllegalStateException("Remote processing service is unavailable.");
+        }
+        try {
+            mRemoteProcessingService.updateProcessingState(processingState,
+                    new IProcessingUpdateStatusCallback.Stub() {
+                        @Override
+                        public void onSuccess(PersistableBundle result) {
+                            Binder.withCleanCallingIdentity(() -> {
+                                callbackExecutor.execute(
+                                        () -> statusReceiver.onResult(result));
+                            });
+                        }
+
+                        @Override
+                        public void onFailure(int errorCode, String errorMessage) {
+                            Binder.withCleanCallingIdentity(() -> callbackExecutor.execute(
+                                    () -> statusReceiver.onError(
+                                            new OnDeviceIntelligenceException(
+                                                    errorCode, errorMessage))));
+                        }
+                    });
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error in updateProcessingState: " + e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    private OutcomeReceiver<Feature,
+            OnDeviceIntelligenceException> wrapFeatureCallback(
+            IFeatureCallback featureCallback) {
+        return new OutcomeReceiver<>() {
+            @Override
+            public void onResult(@NonNull Feature feature) {
+                try {
+                    featureCallback.onSuccess(feature);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending feature: " + e);
+                }
+            }
+
+            @Override
+            public void onError(
+                    @NonNull OnDeviceIntelligenceException exception) {
+                try {
+                    featureCallback.onFailure(exception.getErrorCode(), exception.getMessage(),
+                            exception.getErrorParams());
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending download feature: " + e);
+                }
+            }
+        };
+    }
+
+    private OutcomeReceiver<List<Feature>,
+            OnDeviceIntelligenceException> wrapListFeaturesCallback(
+            IListFeaturesCallback listFeaturesCallback) {
+        return new OutcomeReceiver<>() {
+            @Override
+            public void onResult(@NonNull List<Feature> features) {
+                try {
+                    listFeaturesCallback.onSuccess(features);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending feature: " + e);
+                }
+            }
+
+            @Override
+            public void onError(
+                    @NonNull OnDeviceIntelligenceException exception) {
+                try {
+                    listFeaturesCallback.onFailure(exception.getErrorCode(), exception.getMessage(),
+                            exception.getErrorParams());
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending download feature: " + e);
+                }
+            }
+        };
+    }
+
+    private OutcomeReceiver<FeatureDetails,
+            OnDeviceIntelligenceException> wrapFeatureDetailsCallback(
+            IFeatureDetailsCallback featureStatusCallback) {
+        return new OutcomeReceiver<>() {
+            @Override
+            public void onResult(FeatureDetails result) {
+                try {
+                    featureStatusCallback.onSuccess(result);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending feature status: " + e);
+                }
+            }
+
+            @Override
+            public void onError(
+                    @NonNull OnDeviceIntelligenceException exception) {
+                try {
+                    featureStatusCallback.onFailure(exception.getErrorCode(),
+                            exception.getMessage(), exception.getErrorParams());
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending feature status: " + e);
+                }
+            }
+        };
+    }
+
+
+    private DownloadCallback wrapDownloadCallback(IDownloadCallback downloadCallback) {
+        return new DownloadCallback() {
+            @Override
+            public void onDownloadStarted(long bytesToDownload) {
+                try {
+                    downloadCallback.onDownloadStarted(bytesToDownload);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending download status: " + e);
+                }
+            }
+
+            @Override
+            public void onDownloadFailed(int failureStatus,
+                    String errorMessage, @NonNull PersistableBundle errorParams) {
+                try {
+                    downloadCallback.onDownloadFailed(failureStatus, errorMessage, errorParams);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending download status: " + e);
+                }
+            }
+
+            @Override
+            public void onDownloadProgress(long totalBytesDownloaded) {
+                try {
+                    downloadCallback.onDownloadProgress(totalBytesDownloaded);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending download status: " + e);
+                }
+            }
+
+            @Override
+            public void onDownloadCompleted(@NonNull PersistableBundle persistableBundle) {
+                try {
+                    downloadCallback.onDownloadCompleted(persistableBundle);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending download status: " + e);
+                }
+            }
+        };
+    }
+
+    private static void tryClosePfds(Collection<ParcelFileDescriptor> pfds) {
+        pfds.forEach(pfd -> {
+            try {
+                pfd.close();
+            } catch (Exception e) {
+                Log.w(TAG, "Error closing FD", e);
+            }
+        });
+    }
+
+    private void onGetReadOnlyFileDescriptor(@NonNull String fileName,
+            @NonNull AndroidFuture<ParcelFileDescriptor> future) {
+        Slog.v(TAG, "onGetReadOnlyFileDescriptor " + fileName);
+        Binder.withCleanCallingIdentity(() -> {
+            Slog.v(TAG,
+                    "onGetReadOnlyFileDescriptor: " + fileName + " under internal app storage.");
+            File f = new File(getBaseContext().getFilesDir(), fileName);
+            if (!f.exists()) {
+                f = new File(fileName);
+            }
+            ParcelFileDescriptor pfd = null;
+            try {
+                pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
+                Slog.d(TAG, "Successfully opened a file with ParcelFileDescriptor.");
+            } catch (FileNotFoundException e) {
+                Slog.e(TAG, "Cannot open file. No ParcelFileDescriptor returned.");
+                future.completeExceptionally(e);
+            } finally {
+                future.complete(pfd);
+                if (pfd != null) {
+                    pfd.close();
+                }
+            }
+        });
+    }
+
+    /**
+     * Provide implementation for a scenario when caller wants to get all feature related
+     * file-descriptors that might be required for processing a request for the corresponding the
+     * feature.
+     *
+     * @param feature                   the feature for which files need to be opened.
+     * @param fileDescriptorMapConsumer callback to be populated with a map of file-path and
+     *                                  corresponding ParcelDescriptor to be used in a remote
+     *                                  service.
+     */
+    public abstract void onGetReadOnlyFeatureFileDescriptorMap(
+            @NonNull Feature feature,
+            @NonNull Consumer<Map<String, ParcelFileDescriptor>> fileDescriptorMapConsumer);
+
+    /**
+     * Request download for feature that is requested and listen to download progress updates. If
+     * the download completes successfully, success callback should be populated.
+     *
+     * @param callerUid          UID of the caller that initiated this call chain.
+     * @param feature            the feature for which files need to be downlaoded.
+     *                           process.
+     * @param cancellationSignal signal to attach a listener to, and receive cancellation signals
+     *                           from thw client.
+     * @param downloadCallback   callback to populate download updates for clients to listen on..
+     */
+    public abstract void onDownloadFeature(
+            int callerUid, @NonNull Feature feature,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull DownloadCallback downloadCallback);
+
+    /**
+     * Provide feature details for the passed in feature. Usually the client and remote
+     * implementation use the {@link Feature#getFeatureParams()} as a hint to communicate what
+     * details the client is looking for.
+     *
+     * @param callerUid              UID of the caller that initiated this call chain.
+     * @param feature                the feature for which status needs to be known.
+     * @param featureDetailsCallback callback to populate the resulting feature status.
+     */
+    public abstract void onGetFeatureDetails(int callerUid, @NonNull Feature feature,
+            @NonNull OutcomeReceiver<FeatureDetails,
+                    OnDeviceIntelligenceException> featureDetailsCallback);
+
+
+    /**
+     * Get feature using the provided identifier to the remote implementation.
+     *
+     * @param callerUid       UID of the caller that initiated this call chain.
+     * @param featureCallback callback to populate the features list.
+     */
+    public abstract void onGetFeature(int callerUid, int featureId,
+            @NonNull OutcomeReceiver<Feature,
+                    OnDeviceIntelligenceException> featureCallback);
+
+    /**
+     * List all features which are available in the remote implementation. The implementation might
+     * choose to provide only a certain list of features based on the caller.
+     *
+     * @param callerUid            UID of the caller that initiated this call chain.
+     * @param listFeaturesCallback callback to populate the features list.
+     */
+    public abstract void onListFeatures(int callerUid, @NonNull OutcomeReceiver<List<Feature>,
+            OnDeviceIntelligenceException> listFeaturesCallback);
+
+    /**
+     * Provides a long value representing the version of the remote implementation processing
+     * requests.
+     *
+     * @param versionConsumer consumer to populate the version.
+     */
+    public abstract void onGetVersion(@NonNull LongConsumer versionConsumer);
+}
diff --git a/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
new file mode 100644
index 0000000..949fb8d
--- /dev/null
+++ b/packages/NeuralNetworks/framework/platform/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
@@ -0,0 +1,617 @@
+/*
+ * 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 android.service.ondeviceintelligence;
+
+import static android.app.ondeviceintelligence.OnDeviceIntelligenceManager.AUGMENT_REQUEST_CONTENT_BUNDLE_KEY;
+import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.app.ondeviceintelligence.Feature;
+import android.app.ondeviceintelligence.IProcessingSignal;
+import android.app.ondeviceintelligence.IResponseCallback;
+import android.app.ondeviceintelligence.IStreamingResponseCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceException;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceManager;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.InferenceParams;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.StateParams;
+import android.app.ondeviceintelligence.ProcessingCallback;
+import android.app.ondeviceintelligence.ProcessingSignal;
+import android.app.ondeviceintelligence.StreamingProcessingCallback;
+import android.app.ondeviceintelligence.TokenInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Abstract base class for performing inference in a isolated process. This service exposes its
+ * methods via {@link OnDeviceIntelligenceManager}.
+ *
+ * <p> A service that provides methods to perform on-device inference both in streaming and
+ * non-streaming fashion. Also, provides a way to register a storage service that will be used to
+ * read-only access files from the {@link OnDeviceIntelligenceService} counterpart. </p>
+ *
+ * <p> Similar to {@link OnDeviceIntelligenceManager} class, the contracts in this service are
+ * defined to be open-ended in general, to allow interoperability. Therefore, it is recommended
+ * that implementations of this system-service expose this API to the clients via a library which
+ * has more defined contract.</p>
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".SampleSandboxedInferenceService"
+ *          android:permission="android.permission.BIND_ONDEVICE_SANDBOXED_INFERENCE_SERVICE"
+ *          android:isolatedProcess="true">
+ * </service>}
+ * </pre>
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(FLAG_ENABLE_ON_DEVICE_INTELLIGENCE)
+public abstract class OnDeviceSandboxedInferenceService extends Service {
+    private static final String TAG = OnDeviceSandboxedInferenceService.class.getSimpleName();
+
+    /**
+     * @hide
+     */
+    public static final String INFERENCE_INFO_BUNDLE_KEY = "inference_info";
+
+    /**
+     * The {@link Intent} that must be declared as handled by the service. To be supported, the
+     * service must also require the
+     * {@link android.Manifest.permission#BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE}
+     * permission so that other applications can not abuse it.
+     */
+    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+    public static final String SERVICE_INTERFACE =
+            "android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService";
+
+    // TODO(339594686): make API
+    /**
+     * @hide
+     */
+    public static final String REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY =
+            "register_model_update_callback";
+    /**
+     * @hide
+     */
+    public static final String MODEL_LOADED_BUNDLE_KEY = "model_loaded";
+    /**
+     * @hide
+     */
+    public static final String MODEL_UNLOADED_BUNDLE_KEY = "model_unloaded";
+    /**
+     * @hide
+     */
+    public static final String MODEL_LOADED_BROADCAST_INTENT =
+        "android.service.ondeviceintelligence.MODEL_LOADED";
+    /**
+     * @hide
+     */
+    public static final String MODEL_UNLOADED_BROADCAST_INTENT =
+        "android.service.ondeviceintelligence.MODEL_UNLOADED";
+
+    /**
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_UPDATE_BUNDLE_KEY = "device_config_update";
+
+    private IRemoteStorageService mRemoteStorageService;
+    private Handler mHandler;
+
+    @CallSuper
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mHandler = new Handler(Looper.getMainLooper(), null /* callback */, true /* async */);
+    }
+
+    /**
+     * @hide
+     */
+    @Nullable
+    @Override
+    public final IBinder onBind(@NonNull Intent intent) {
+        if (SERVICE_INTERFACE.equals(intent.getAction())) {
+            return new IOnDeviceSandboxedInferenceService.Stub() {
+                @Override
+                public void registerRemoteStorageService(IRemoteStorageService storageService,
+                        IRemoteCallback remoteCallback) throws RemoteException {
+                    Objects.requireNonNull(storageService);
+                    mRemoteStorageService = storageService;
+                    remoteCallback.sendResult(
+                            Bundle.EMPTY); //to notify caller uid to system-server.
+                }
+
+                @Override
+                public void requestTokenInfo(int callerUid, Feature feature, Bundle request,
+                        AndroidFuture cancellationSignalFuture,
+                        ITokenInfoCallback tokenInfoCallback) {
+                    Objects.requireNonNull(feature);
+                    Objects.requireNonNull(tokenInfoCallback);
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
+
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceSandboxedInferenceService::onTokenInfoRequest,
+                                    OnDeviceSandboxedInferenceService.this,
+                                    callerUid, feature,
+                                    request,
+                                    CancellationSignal.fromTransport(transport),
+                                    wrapTokenInfoCallback(tokenInfoCallback)));
+                }
+
+                @Override
+                public void processRequestStreaming(int callerUid, Feature feature, Bundle request,
+                        int requestType,
+                        AndroidFuture cancellationSignalFuture,
+                        AndroidFuture processingSignalFuture,
+                        IStreamingResponseCallback callback) {
+                    Objects.requireNonNull(feature);
+                    Objects.requireNonNull(callback);
+
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
+                    IProcessingSignal processingSignalTransport = null;
+                    if (processingSignalFuture != null) {
+                        processingSignalTransport = ProcessingSignal.createTransport();
+                        processingSignalFuture.complete(processingSignalTransport);
+                    }
+
+
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceSandboxedInferenceService::onProcessRequestStreaming,
+                                    OnDeviceSandboxedInferenceService.this, callerUid,
+                                    feature,
+                                    request,
+                                    requestType,
+                                    CancellationSignal.fromTransport(transport),
+                                    ProcessingSignal.fromTransport(processingSignalTransport),
+                                    wrapStreamingResponseCallback(callback)));
+                }
+
+                @Override
+                public void processRequest(int callerUid, Feature feature, Bundle request,
+                        int requestType,
+                        AndroidFuture cancellationSignalFuture,
+                        AndroidFuture processingSignalFuture,
+                        IResponseCallback callback) {
+                    Objects.requireNonNull(feature);
+                    Objects.requireNonNull(callback);
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
+                    IProcessingSignal processingSignalTransport = null;
+                    if (processingSignalFuture != null) {
+                        processingSignalTransport = ProcessingSignal.createTransport();
+                        processingSignalFuture.complete(processingSignalTransport);
+                    }
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceSandboxedInferenceService::onProcessRequest,
+                                    OnDeviceSandboxedInferenceService.this, callerUid, feature,
+                                    request, requestType,
+                                    CancellationSignal.fromTransport(transport),
+                                    ProcessingSignal.fromTransport(processingSignalTransport),
+                                    wrapResponseCallback(callback)));
+                }
+
+                @Override
+                public void updateProcessingState(Bundle processingState,
+                        IProcessingUpdateStatusCallback callback) {
+                    Objects.requireNonNull(processingState);
+                    Objects.requireNonNull(callback);
+                    mHandler.executeOrSendMessage(
+                            obtainMessage(
+                                    OnDeviceSandboxedInferenceService::onUpdateProcessingState,
+                                    OnDeviceSandboxedInferenceService.this, processingState,
+                                    wrapOutcomeReceiver(callback)));
+                }
+            };
+        }
+        Slog.w(TAG, "Incorrect service interface, returning null.");
+        return null;
+    }
+
+    /**
+     * Invoked when caller  wants to obtain token info related to the payload in the passed
+     * content, associated with the provided feature.
+     * The expectation from the implementation is that when processing is complete, it
+     * should provide the token info in the {@link OutcomeReceiver#onResult}.
+     *
+     * @param callerUid          UID of the caller that initiated this call chain.
+     * @param feature            feature which is associated with the request.
+     * @param request            request that requires processing.
+     * @param cancellationSignal Cancellation Signal to receive cancellation events from client and
+     *                           configure a listener to.
+     * @param callback           callback to populate failure or the token info for the provided
+     *                           request.
+     */
+    @NonNull
+    public abstract void onTokenInfoRequest(
+            int callerUid, @NonNull Feature feature,
+            @NonNull @InferenceParams Bundle request,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull OutcomeReceiver<TokenInfo, OnDeviceIntelligenceException> callback);
+
+    /**
+     * Invoked when caller provides a request for a particular feature to be processed in a
+     * streaming manner. The expectation from the implementation is that when processing the
+     * request,
+     * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to
+     * continuously
+     * provide partial Bundle results for the caller to utilize. Optionally the implementation can
+     * provide the complete response in the {@link StreamingProcessingCallback#onResult} upon
+     * processing completion.
+     *
+     * @param callerUid          UID of the caller that initiated this call chain.
+     * @param feature            feature which is associated with the request.
+     * @param request            request that requires processing.
+     * @param requestType        identifier representing the type of request.
+     * @param cancellationSignal Cancellation Signal to receive cancellation events from client and
+     *                           configure a listener to.
+     * @param processingSignal   Signal to receive custom action instructions from client.
+     * @param callback           callback to populate the partial responses, failure and optionally
+     *                           full response for the provided request.
+     */
+    @NonNull
+    public abstract void onProcessRequestStreaming(
+            int callerUid, @NonNull Feature feature,
+            @NonNull @InferenceParams Bundle request,
+            @OnDeviceIntelligenceManager.RequestType int requestType,
+            @Nullable CancellationSignal cancellationSignal,
+            @Nullable ProcessingSignal processingSignal,
+            @NonNull StreamingProcessingCallback callback);
+
+    /**
+     * Invoked when caller provides a request for a particular feature to be processed in one shot
+     * completely.
+     * The expectation from the implementation is that when processing the request is complete, it
+     * should
+     * provide the complete response in the {@link OutcomeReceiver#onResult}.
+     *
+     * @param callerUid          UID of the caller that initiated this call chain.
+     * @param feature            feature which is associated with the request.
+     * @param request            request that requires processing.
+     * @param requestType        identifier representing the type of request.
+     * @param cancellationSignal Cancellation Signal to receive cancellation events from client and
+     *                           configure a listener to.
+     * @param processingSignal   Signal to receive custom action instructions from client.
+     * @param callback           callback to populate failure and full response for the provided
+     *                           request.
+     */
+    @NonNull
+    public abstract void onProcessRequest(
+            int callerUid, @NonNull Feature feature,
+            @NonNull @InferenceParams Bundle request,
+            @OnDeviceIntelligenceManager.RequestType int requestType,
+            @Nullable CancellationSignal cancellationSignal,
+            @Nullable ProcessingSignal processingSignal,
+            @NonNull ProcessingCallback callback);
+
+
+    /**
+     * Invoked when processing environment needs to be updated or refreshed with fresh
+     * configuration, files or state.
+     *
+     * @param processingState contains updated state and params that are to be applied to the
+     *                        processing environmment,
+     * @param callback        callback to populate the update status and if there are params
+     *                        associated with the status.
+     */
+    public abstract void onUpdateProcessingState(@NonNull @StateParams Bundle processingState,
+            @NonNull OutcomeReceiver<PersistableBundle,
+                    OnDeviceIntelligenceException> callback);
+
+
+    /**
+     * Overrides {@link Context#openFileInput} to read files with the given file names under the
+     * internal app storage of the {@link OnDeviceIntelligenceService}, i.e., only files stored in
+     * {@link Context#getFilesDir()} can be opened.
+     */
+    @Override
+    public final FileInputStream openFileInput(@NonNull String filename) throws
+            FileNotFoundException {
+        try {
+            AndroidFuture<ParcelFileDescriptor> future = new AndroidFuture<>();
+            mRemoteStorageService.getReadOnlyFileDescriptor(filename, future);
+            ParcelFileDescriptor pfd = future.get();
+            return new FileInputStream(pfd.getFileDescriptor());
+        } catch (RemoteException | ExecutionException | InterruptedException e) {
+            Log.w(TAG, "Cannot open file due to remote service failure");
+            throw new FileNotFoundException(e.getMessage());
+        }
+    }
+
+    /**
+     * Provides read-only access to the internal app storage via the
+     * {@link OnDeviceIntelligenceService}. This is an asynchronous alternative for
+     * {@link #openFileInput(String)}.
+     *
+     * @param fileName       File name relative to the {@link Context#getFilesDir()}.
+     * @param resultConsumer Consumer to populate the corresponding file descriptor in.
+     */
+    public final void getReadOnlyFileDescriptor(@NonNull String fileName,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<ParcelFileDescriptor> resultConsumer) throws FileNotFoundException {
+        AndroidFuture<ParcelFileDescriptor> future = new AndroidFuture<>();
+        try {
+            mRemoteStorageService.getReadOnlyFileDescriptor(fileName, future);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Cannot open file due to remote service failure");
+            throw new FileNotFoundException(e.getMessage());
+        }
+        future.whenCompleteAsync((pfd, err) -> {
+            if (err != null) {
+                Log.e(TAG, "Failure when reading file: " + fileName + err);
+                executor.execute(() -> resultConsumer.accept(null));
+            } else {
+                executor.execute(
+                        () -> resultConsumer.accept(pfd));
+            }
+        }, executor);
+    }
+
+    /**
+     * Provides access to all file streams required for feature via the
+     * {@link OnDeviceIntelligenceService}.
+     *
+     * @param feature        Feature for which the associated files should be fetched.
+     * @param executor       Executor to run the consumer callback on.
+     * @param resultConsumer Consumer to receive a map of filePath to the corresponding file input
+     *                       stream.
+     */
+    public final void fetchFeatureFileDescriptorMap(@NonNull Feature feature,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<Map<String, ParcelFileDescriptor>> resultConsumer) {
+        try {
+            mRemoteStorageService.getReadOnlyFeatureFileDescriptorMap(feature,
+                    wrapAsRemoteCallback(resultConsumer, executor));
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    /**
+     * Returns the {@link Executor} to use for incoming IPC from request sender into your service
+     * implementation. For e.g. see
+     * {@link ProcessingCallback#onDataAugmentRequest(Bundle,
+     * Consumer)} where we use the executor to populate the consumer.
+     * <p>
+     * Override this method in your {@link OnDeviceSandboxedInferenceService} implementation to
+     * provide the executor you want to use for incoming IPC.
+     *
+     * @return the {@link Executor} to use for incoming IPC from {@link OnDeviceIntelligenceManager}
+     * to {@link OnDeviceSandboxedInferenceService}.
+     */
+    @SuppressLint("OnNameExpected")
+    @NonNull
+    public Executor getCallbackExecutor() {
+        return new HandlerExecutor(Handler.createAsync(getMainLooper()));
+    }
+
+
+    private RemoteCallback wrapAsRemoteCallback(
+            @NonNull Consumer<Map<String, ParcelFileDescriptor>> resultConsumer,
+            @NonNull Executor executor) {
+        return new RemoteCallback(result -> {
+            if (result == null) {
+                executor.execute(() -> resultConsumer.accept(new HashMap<>()));
+            } else {
+                Map<String, ParcelFileDescriptor> pfdMap = new HashMap<>();
+                result.keySet().forEach(key ->
+                        pfdMap.put(key, result.getParcelable(key,
+                                ParcelFileDescriptor.class)));
+                executor.execute(() -> resultConsumer.accept(pfdMap));
+            }
+        });
+    }
+
+    private ProcessingCallback wrapResponseCallback(
+            IResponseCallback callback) {
+        return new ProcessingCallback() {
+            @Override
+            public void onResult(@NonNull Bundle result) {
+                try {
+                    callback.onSuccess(result);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending result: " + e);
+                }
+            }
+
+            @Override
+            public void onError(
+                    OnDeviceIntelligenceException exception) {
+                try {
+                    callback.onFailure(exception.getErrorCode(), exception.getMessage(),
+                            exception.getErrorParams());
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending result: " + e);
+                }
+            }
+
+            @Override
+            public void onDataAugmentRequest(@NonNull Bundle content,
+                    @NonNull Consumer<Bundle> contentCallback) {
+                try {
+                    callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback));
+
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending augment request: " + e);
+                }
+            }
+        };
+    }
+
+    private StreamingProcessingCallback wrapStreamingResponseCallback(
+            IStreamingResponseCallback callback) {
+        return new StreamingProcessingCallback() {
+            @Override
+            public void onPartialResult(@NonNull Bundle partialResult) {
+                try {
+                    callback.onNewContent(partialResult);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending result: " + e);
+                }
+            }
+
+            @Override
+            public void onResult(@NonNull Bundle result) {
+                try {
+                    callback.onSuccess(result);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending result: " + e);
+                }
+            }
+
+            @Override
+            public void onError(
+                    OnDeviceIntelligenceException exception) {
+                try {
+                    callback.onFailure(exception.getErrorCode(), exception.getMessage(),
+                            exception.getErrorParams());
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending result: " + e);
+                }
+            }
+
+            @Override
+            public void onDataAugmentRequest(@NonNull Bundle content,
+                    @NonNull Consumer<Bundle> contentCallback) {
+                try {
+                    callback.onDataAugmentRequest(content, wrapRemoteCallback(contentCallback));
+
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending augment request: " + e);
+                }
+            }
+        };
+    }
+
+    private RemoteCallback wrapRemoteCallback(
+            @NonNull Consumer<Bundle> contentCallback) {
+        return new RemoteCallback(
+                result -> {
+                    if (result != null) {
+                        getCallbackExecutor().execute(() -> contentCallback.accept(
+                                result.getParcelable(AUGMENT_REQUEST_CONTENT_BUNDLE_KEY,
+                                        Bundle.class)));
+                    } else {
+                        getCallbackExecutor().execute(
+                                () -> contentCallback.accept(null));
+                    }
+                });
+    }
+
+    private OutcomeReceiver<TokenInfo, OnDeviceIntelligenceException> wrapTokenInfoCallback(
+            ITokenInfoCallback tokenInfoCallback) {
+        return new OutcomeReceiver<>() {
+            @Override
+            public void onResult(TokenInfo tokenInfo) {
+                try {
+                    tokenInfoCallback.onSuccess(tokenInfo);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending result: " + e);
+                }
+            }
+
+            @Override
+            public void onError(
+                    OnDeviceIntelligenceException exception) {
+                try {
+                    tokenInfoCallback.onFailure(exception.getErrorCode(), exception.getMessage(),
+                            exception.getErrorParams());
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending failure: " + e);
+                }
+            }
+        };
+    }
+
+    @NonNull
+    private static OutcomeReceiver<PersistableBundle, OnDeviceIntelligenceException> wrapOutcomeReceiver(
+            IProcessingUpdateStatusCallback callback) {
+        return new OutcomeReceiver<>() {
+            @Override
+            public void onResult(@NonNull PersistableBundle result) {
+                try {
+                    callback.onSuccess(result);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending result: " + e);
+
+                }
+            }
+
+            @Override
+            public void onError(
+                    @NonNull OnDeviceIntelligenceException error) {
+                try {
+                    callback.onFailure(error.getErrorCode(), error.getMessage());
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending exception details: " + e);
+                }
+            }
+        };
+    }
+
+}
diff --git a/packages/NeuralNetworks/service/Android.bp b/packages/NeuralNetworks/service/Android.bp
index 05c603f..cfdc1af 100644
--- a/packages/NeuralNetworks/service/Android.bp
+++ b/packages/NeuralNetworks/service/Android.bp
@@ -19,11 +19,20 @@
 filegroup {
     name: "service-ondeviceintelligence-sources",
     srcs: [
-        "java/**/*.java",
+        "module/java/**/*.java",
     ],
-    path: "java",
     visibility: [
         "//frameworks/base:__subpackages__",
         "//packages/modules/NeuralNetworks:__subpackages__",
     ],
 }
+
+filegroup {
+    name: "service-ondeviceintelligence-sources-platform",
+    srcs: [
+        "platform/java/**/*.java",
+    ],
+    visibility: [
+        "//frameworks/base:__subpackages__",
+    ],
+}
diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/BundleUtil.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/BundleUtil.java
similarity index 100%
rename from packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/BundleUtil.java
rename to packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/BundleUtil.java
diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java
similarity index 100%
rename from packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java
rename to packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java
diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java
similarity index 100%
rename from packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java
rename to packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java
diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
similarity index 100%
rename from packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
rename to packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java
similarity index 100%
rename from packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java
rename to packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java
diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java
similarity index 100%
rename from packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java
rename to packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java
diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
similarity index 100%
rename from packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
rename to packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java b/packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java
similarity index 100%
rename from packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java
rename to packages/NeuralNetworks/service/module/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java
diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/BundleUtil.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/BundleUtil.java
new file mode 100644
index 0000000..7dd8f2f
--- /dev/null
+++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/BundleUtil.java
@@ -0,0 +1,407 @@
+/*
+ * 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.ondeviceintelligence;
+
+import static android.system.OsConstants.F_GETFL;
+import static android.system.OsConstants.O_ACCMODE;
+import static android.system.OsConstants.O_RDONLY;
+import static android.system.OsConstants.PROT_READ;
+
+import android.app.ondeviceintelligence.IResponseCallback;
+import android.app.ondeviceintelligence.IStreamingResponseCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.InferenceParams;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.ResponseParams;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceManager.StateParams;
+import android.app.ondeviceintelligence.TokenInfo;
+import android.database.CursorWindow;
+import android.graphics.Bitmap;
+import android.os.BadParcelableException;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.SharedMemory;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Util methods for ensuring the Bundle passed in various methods are read-only and restricted to
+ * some known types.
+ */
+public class BundleUtil {
+    private static final String TAG = "BundleUtil";
+
+    /**
+     * Validation of the inference request payload as described in {@link InferenceParams}
+     * description.
+     *
+     * @throws BadParcelableException when the bundle does not meet the read-only requirements.
+     */
+    public static void sanitizeInferenceParams(
+            @InferenceParams Bundle bundle) {
+        ensureValidBundle(bundle);
+
+        if (!bundle.hasFileDescriptors()) {
+            return; //safe to exit if there are no FDs and Binders
+        }
+
+        for (String key : bundle.keySet()) {
+            Object obj = bundle.get(key);
+            if (obj == null) {
+                /* Null value here could also mean deserializing a custom parcelable has failed,
+                 *  and since {@link Bundle} is marked as defusable in system-server - the
+                 * {@link ClassNotFoundException} exception is swallowed and `null` is returned
+                 * instead. We want to ensure cleanup of null entries in such case.
+                 */
+                bundle.putObject(key, null);
+                continue;
+            }
+            if (canMarshall(obj) || obj instanceof CursorWindow) {
+                continue;
+            }
+            if (obj instanceof Bundle) {
+              sanitizeInferenceParams((Bundle) obj);
+            } else if (obj instanceof ParcelFileDescriptor) {
+                validatePfdReadOnly((ParcelFileDescriptor) obj);
+            } else if (obj instanceof SharedMemory) {
+                ((SharedMemory) obj).setProtect(PROT_READ);
+            } else if (obj instanceof Bitmap) {
+                validateBitmap((Bitmap) obj);
+            } else if (obj instanceof Parcelable[]) {
+                validateParcelableArray((Parcelable[]) obj);
+            } else {
+                throw new BadParcelableException(
+                        "Unsupported Parcelable type encountered in the Bundle: "
+                                + obj.getClass().getSimpleName());
+            }
+        }
+    }
+
+    /**
+     * Validation of the inference request payload as described in {@link ResponseParams}
+     * description.
+     *
+     * @throws BadParcelableException when the bundle does not meet the read-only requirements.
+     */
+    public static void sanitizeResponseParams(
+            @ResponseParams Bundle bundle) {
+        ensureValidBundle(bundle);
+
+        if (!bundle.hasFileDescriptors()) {
+            return; //safe to exit if there are no FDs and Binders
+        }
+
+        for (String key : bundle.keySet()) {
+            Object obj = bundle.get(key);
+            if (obj == null) {
+                /* Null value here could also mean deserializing a custom parcelable has failed,
+                 *  and since {@link Bundle} is marked as defusable in system-server - the
+                 * {@link ClassNotFoundException} exception is swallowed and `null` is returned
+                 * instead. We want to ensure cleanup of null entries in such case.
+                 */
+                bundle.putObject(key, null);
+                continue;
+            }
+            if (canMarshall(obj)) {
+                continue;
+            }
+
+            if (obj instanceof Bundle) {
+                sanitizeResponseParams((Bundle) obj);
+            } else if (obj instanceof ParcelFileDescriptor) {
+                validatePfdReadOnly((ParcelFileDescriptor) obj);
+            } else if (obj instanceof Bitmap) {
+                validateBitmap((Bitmap) obj);
+            } else if (obj instanceof Parcelable[]) {
+                validateParcelableArray((Parcelable[]) obj);
+            } else {
+                throw new BadParcelableException(
+                        "Unsupported Parcelable type encountered in the Bundle: "
+                                + obj.getClass().getSimpleName());
+            }
+        }
+    }
+
+    /**
+     * Validation of the inference request payload as described in {@link StateParams}
+     * description.
+     *
+     * @throws BadParcelableException when the bundle does not meet the read-only requirements.
+     */
+    public static void sanitizeStateParams(
+            @StateParams Bundle bundle) {
+        ensureValidBundle(bundle);
+
+        if (!bundle.hasFileDescriptors()) {
+            return; //safe to exit if there are no FDs and Binders
+        }
+
+        for (String key : bundle.keySet()) {
+            Object obj = bundle.get(key);
+            if (obj == null) {
+                /* Null value here could also mean deserializing a custom parcelable has failed,
+                 *  and since {@link Bundle} is marked as defusable in system-server - the
+                 * {@link ClassNotFoundException} exception is swallowed and `null` is returned
+                 * instead. We want to ensure cleanup of null entries in such case.
+                 */
+                bundle.putObject(key, null);
+                continue;
+            }
+            if (canMarshall(obj)) {
+                continue;
+            }
+
+            if (obj instanceof ParcelFileDescriptor) {
+                validatePfdReadOnly((ParcelFileDescriptor) obj);
+            } else {
+                throw new BadParcelableException(
+                        "Unsupported Parcelable type encountered in the Bundle: "
+                                + obj.getClass().getSimpleName());
+            }
+        }
+    }
+
+
+    public static IStreamingResponseCallback wrapWithValidation(
+            IStreamingResponseCallback streamingResponseCallback,
+            Executor resourceClosingExecutor,
+            AndroidFuture future,
+            InferenceInfoStore inferenceInfoStore) {
+        return new IStreamingResponseCallback.Stub() {
+            @Override
+            public void onNewContent(Bundle processedResult) throws RemoteException {
+                try {
+                    sanitizeResponseParams(processedResult);
+                    streamingResponseCallback.onNewContent(processedResult);
+                } finally {
+                    resourceClosingExecutor.execute(() -> tryCloseResource(processedResult));
+                }
+            }
+
+            @Override
+            public void onSuccess(Bundle resultBundle)
+                    throws RemoteException {
+                try {
+                    sanitizeResponseParams(resultBundle);
+                    streamingResponseCallback.onSuccess(resultBundle);
+                } finally {
+                    inferenceInfoStore.addInferenceInfoFromBundle(resultBundle);
+                    resourceClosingExecutor.execute(() -> tryCloseResource(resultBundle));
+                    future.complete(null);
+                }
+            }
+
+            @Override
+            public void onFailure(int errorCode, String errorMessage,
+                    PersistableBundle errorParams) throws RemoteException {
+                streamingResponseCallback.onFailure(errorCode, errorMessage, errorParams);
+                inferenceInfoStore.addInferenceInfoFromBundle(errorParams);
+                future.completeExceptionally(new TimeoutException());
+            }
+
+            @Override
+            public void onDataAugmentRequest(Bundle processedContent,
+                    RemoteCallback remoteCallback)
+                    throws RemoteException {
+                try {
+                    sanitizeResponseParams(processedContent);
+                    streamingResponseCallback.onDataAugmentRequest(processedContent,
+                            new RemoteCallback(
+                                    augmentedData -> {
+                                        try {
+                                            sanitizeInferenceParams(augmentedData);
+                                            remoteCallback.sendResult(augmentedData);
+                                        } finally {
+                                            resourceClosingExecutor.execute(
+                                                    () -> tryCloseResource(augmentedData));
+                                        }
+                                    }));
+                } finally {
+                    resourceClosingExecutor.execute(() -> tryCloseResource(processedContent));
+                }
+            }
+        };
+    }
+
+    public static IResponseCallback wrapWithValidation(IResponseCallback responseCallback,
+            Executor resourceClosingExecutor,
+            AndroidFuture future,
+            InferenceInfoStore inferenceInfoStore) {
+        return new IResponseCallback.Stub() {
+            @Override
+            public void onSuccess(Bundle resultBundle)
+                    throws RemoteException {
+                try {
+                    sanitizeResponseParams(resultBundle);
+                    responseCallback.onSuccess(resultBundle);
+                } finally {
+                    inferenceInfoStore.addInferenceInfoFromBundle(resultBundle);
+                    resourceClosingExecutor.execute(() -> tryCloseResource(resultBundle));
+                    future.complete(null);
+                }
+            }
+
+            @Override
+            public void onFailure(int errorCode, String errorMessage,
+                    PersistableBundle errorParams) throws RemoteException {
+                responseCallback.onFailure(errorCode, errorMessage, errorParams);
+                inferenceInfoStore.addInferenceInfoFromBundle(errorParams);
+                future.completeExceptionally(new TimeoutException());
+            }
+
+            @Override
+            public void onDataAugmentRequest(Bundle processedContent,
+                    RemoteCallback remoteCallback)
+                    throws RemoteException {
+                try {
+                    sanitizeResponseParams(processedContent);
+                    responseCallback.onDataAugmentRequest(processedContent, new RemoteCallback(
+                            augmentedData -> {
+                                try {
+                                    sanitizeInferenceParams(augmentedData);
+                                    remoteCallback.sendResult(augmentedData);
+                                } finally {
+                                    resourceClosingExecutor.execute(
+                                            () -> tryCloseResource(augmentedData));
+                                }
+                            }));
+                } finally {
+                    resourceClosingExecutor.execute(() -> tryCloseResource(processedContent));
+                }
+            }
+        };
+    }
+
+
+    public static ITokenInfoCallback wrapWithValidation(ITokenInfoCallback responseCallback,
+            AndroidFuture future,
+            InferenceInfoStore inferenceInfoStore) {
+        return new ITokenInfoCallback.Stub() {
+            @Override
+            public void onSuccess(TokenInfo tokenInfo) throws RemoteException {
+                responseCallback.onSuccess(tokenInfo);
+                inferenceInfoStore.addInferenceInfoFromBundle(tokenInfo.getInfoParams());
+                future.complete(null);
+            }
+
+            @Override
+            public void onFailure(int errorCode, String errorMessage, PersistableBundle errorParams)
+                    throws RemoteException {
+                responseCallback.onFailure(errorCode, errorMessage, errorParams);
+                inferenceInfoStore.addInferenceInfoFromBundle(errorParams);
+                future.completeExceptionally(new TimeoutException());
+            }
+        };
+    }
+
+    private static boolean canMarshall(Object obj) {
+        return obj instanceof byte[] || obj instanceof PersistableBundle
+                || PersistableBundle.isValidType(obj);
+    }
+
+    private static void ensureValidBundle(Bundle bundle) {
+        if (bundle == null) {
+            throw new IllegalArgumentException("Request passed is expected to be non-null");
+        }
+
+        if (bundle.hasBinders() != Bundle.STATUS_BINDERS_NOT_PRESENT) {
+            throw new BadParcelableException("Bundle should not contain IBinder objects.");
+        }
+    }
+
+    private static void validateParcelableArray(Parcelable[] parcelables) {
+        if (parcelables.length > 0
+                && parcelables[0] instanceof ParcelFileDescriptor) {
+            // Safe to cast
+            validatePfdsReadOnly(parcelables);
+        } else if (parcelables.length > 0
+                && parcelables[0] instanceof Bitmap) {
+            validateBitmapsImmutable(parcelables);
+        } else {
+            throw new BadParcelableException(
+                    "Could not cast to any known parcelable array");
+        }
+    }
+
+    public static void validatePfdsReadOnly(Parcelable[] pfds) {
+        for (Parcelable pfd : pfds) {
+            validatePfdReadOnly((ParcelFileDescriptor) pfd);
+        }
+    }
+
+    public static void validatePfdReadOnly(ParcelFileDescriptor pfd) {
+        if (pfd == null) {
+            return;
+        }
+        try {
+            int readMode = Os.fcntlInt(pfd.getFileDescriptor(), F_GETFL, 0) & O_ACCMODE;
+            if (readMode != O_RDONLY) {
+                throw new BadParcelableException(
+                        "Bundle contains a parcel file descriptor which is not read-only.");
+            }
+        } catch (ErrnoException e) {
+            throw new BadParcelableException(
+                    "Invalid File descriptor passed in the Bundle.", e);
+        }
+    }
+
+    private static void validateBitmap(Bitmap obj) {
+        if (obj.isMutable()) {
+            throw new BadParcelableException(
+                    "Encountered a mutable Bitmap in the Bundle at key : " + obj);
+        }
+    }
+
+    private static void validateBitmapsImmutable(Parcelable[] bitmaps) {
+        for (Parcelable bitmap : bitmaps) {
+            validateBitmap((Bitmap) bitmap);
+        }
+    }
+
+    public static void tryCloseResource(Bundle bundle) {
+        if (bundle == null || bundle.isEmpty() || !bundle.hasFileDescriptors()) {
+            return;
+        }
+
+        for (String key : bundle.keySet()) {
+            Object obj = bundle.get(key);
+
+            try {
+                // TODO(b/329898589) : This can be cleaned up after the flag passing is fixed.
+                if (obj instanceof ParcelFileDescriptor) {
+                    ((ParcelFileDescriptor) obj).close();
+                } else if (obj instanceof CursorWindow) {
+                    ((CursorWindow) obj).close();
+                } else if (obj instanceof SharedMemory) {
+                    // TODO(b/331796886) : Shared memory should honour parcelable flags.
+                    ((SharedMemory) obj).close();
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Error closing resource with key: " + key, e);
+            }
+        }
+    }
+}
diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java
new file mode 100644
index 0000000..bef3f80
--- /dev/null
+++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java
@@ -0,0 +1,101 @@
+/*
+ * 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.ondeviceintelligence;
+
+import android.app.ondeviceintelligence.InferenceInfo;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService;
+import android.util.Base64;
+import android.util.Slog;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.List;
+import java.util.TreeSet;
+
+public class InferenceInfoStore {
+    private static final String TAG = "InferenceInfoStore";
+    private final TreeSet<InferenceInfo> inferenceInfos;
+    private final long maxAgeMs;
+
+    public InferenceInfoStore(long maxAgeMs) {
+        this.maxAgeMs = maxAgeMs;
+        this.inferenceInfos = new TreeSet<>(
+                Comparator.comparingLong(InferenceInfo::getStartTimeMillis));
+    }
+
+    public List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) {
+        return inferenceInfos.stream().filter(
+                info -> info.getStartTimeMillis() > startTimeEpochMillis).toList();
+    }
+
+    public void addInferenceInfoFromBundle(PersistableBundle pb) {
+        if (!pb.containsKey(OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY)) {
+            return;
+        }
+
+        try {
+            String infoBytesBase64String = pb.getString(
+                    OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY);
+            if (infoBytesBase64String != null) {
+                byte[] infoBytes = Base64.decode(infoBytesBase64String, Base64.DEFAULT);
+                com.android.server.ondeviceintelligence.nano.InferenceInfo inferenceInfo =
+                        com.android.server.ondeviceintelligence.nano.InferenceInfo.parseFrom(
+                                infoBytes);
+                add(inferenceInfo);
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to parse InferenceInfo from the received bytes.");
+        }
+    }
+
+    public void addInferenceInfoFromBundle(Bundle b) {
+        if (!b.containsKey(OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY)) {
+            return;
+        }
+
+        try {
+            byte[] infoBytes = b.getByteArray(
+                    OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY);
+            if (infoBytes != null) {
+                com.android.server.ondeviceintelligence.nano.InferenceInfo inferenceInfo =
+                        com.android.server.ondeviceintelligence.nano.InferenceInfo.parseFrom(
+                                infoBytes);
+                add(inferenceInfo);
+            }
+        } catch (IOException e) {
+            Slog.e(TAG, "Unable to parse InferenceInfo from the received bytes.");
+        }
+    }
+
+    private synchronized void add(com.android.server.ondeviceintelligence.nano.InferenceInfo info) {
+        while (!inferenceInfos.isEmpty()
+                && System.currentTimeMillis() - inferenceInfos.first().getStartTimeMillis()
+                > maxAgeMs) {
+            inferenceInfos.pollFirst();
+        }
+        inferenceInfos.add(toInferenceInfo(info));
+    }
+
+    private static InferenceInfo toInferenceInfo(
+            com.android.server.ondeviceintelligence.nano.InferenceInfo info) {
+        return new InferenceInfo.Builder(info.uid).setStartTimeMillis(
+                info.startTimeMs).setEndTimeMillis(info.endTimeMs).setSuspendedTimeMillis(
+                info.suspendedTimeMs).build();
+    }
+}
\ No newline at end of file
diff --git a/packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java
similarity index 100%
copy from packages/NeuralNetworks/service/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java
copy to packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerLocal.java
diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
new file mode 100644
index 0000000..0a69af6
--- /dev/null
+++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -0,0 +1,1096 @@
+/*
+ * 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.ondeviceintelligence;
+
+import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.DEVICE_CONFIG_UPDATE_BUNDLE_KEY;
+import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_LOADED_BUNDLE_KEY;
+import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_UNLOADED_BUNDLE_KEY;
+import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_LOADED_BROADCAST_INTENT;
+import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_UNLOADED_BROADCAST_INTENT;
+import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY;
+
+import static com.android.server.ondeviceintelligence.BundleUtil.sanitizeInferenceParams;
+import static com.android.server.ondeviceintelligence.BundleUtil.validatePfdReadOnly;
+import static com.android.server.ondeviceintelligence.BundleUtil.sanitizeStateParams;
+import static com.android.server.ondeviceintelligence.BundleUtil.wrapWithValidation;
+
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.app.AppGlobals;
+import android.app.ondeviceintelligence.DownloadCallback;
+import android.app.ondeviceintelligence.Feature;
+import android.app.ondeviceintelligence.FeatureDetails;
+import android.app.ondeviceintelligence.IDownloadCallback;
+import android.app.ondeviceintelligence.IFeatureCallback;
+import android.app.ondeviceintelligence.IFeatureDetailsCallback;
+import android.app.ondeviceintelligence.IListFeaturesCallback;
+import android.app.ondeviceintelligence.IOnDeviceIntelligenceManager;
+import android.app.ondeviceintelligence.IProcessingSignal;
+import android.app.ondeviceintelligence.IResponseCallback;
+import android.app.ondeviceintelligence.IStreamingResponseCallback;
+import android.app.ondeviceintelligence.ITokenInfoCallback;
+import android.app.ondeviceintelligence.InferenceInfo;
+import android.app.ondeviceintelligence.OnDeviceIntelligenceException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.service.ondeviceintelligence.IOnDeviceIntelligenceService;
+import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService;
+import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback;
+import android.service.ondeviceintelligence.IRemoteProcessingService;
+import android.service.ondeviceintelligence.IRemoteStorageService;
+import android.service.ondeviceintelligence.OnDeviceIntelligenceService;
+import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
+import com.android.internal.os.BackgroundThread;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.SystemService;
+import com.android.server.SystemService.TargetUser;
+import com.android.server.ondeviceintelligence.callbacks.ListenableDownloadCallback;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This is the system service for handling calls on the
+ * {@link android.app.ondeviceintelligence.OnDeviceIntelligenceManager}. This
+ * service holds connection references to the underlying remote services i.e. the isolated service
+ * {@link OnDeviceSandboxedInferenceService} and a regular
+ * service counter part {@link OnDeviceIntelligenceService}.
+ *
+ * Note: Both the remote services run under the SYSTEM user, as we cannot have separate instance of
+ * the Inference service for each user, due to possible high memory footprint.
+ *
+ * @hide
+ */
+public class OnDeviceIntelligenceManagerService extends SystemService {
+
+    private static final String TAG = OnDeviceIntelligenceManagerService.class.getSimpleName();
+    private static final String KEY_SERVICE_ENABLED = "service_enabled";
+
+    /** Handler message to {@link #resetTemporaryServices()} */
+    private static final int MSG_RESET_TEMPORARY_SERVICE = 0;
+    /** Handler message to clean up temporary broadcast keys. */
+    private static final int MSG_RESET_BROADCAST_KEYS = 1;
+    /** Handler message to clean up temporary config namespace. */
+    private static final int MSG_RESET_CONFIG_NAMESPACE = 2;
+
+    /** Default value in absence of {@link DeviceConfig} override. */
+    private static final boolean DEFAULT_SERVICE_ENABLED = true;
+    private static final String NAMESPACE_ON_DEVICE_INTELLIGENCE = "ondeviceintelligence";
+
+    private static final String SYSTEM_PACKAGE = "android";
+    private static final long MAX_AGE_MS = TimeUnit.HOURS.toMillis(3);
+
+
+    private final Executor resourceClosingExecutor = Executors.newCachedThreadPool();
+    private final Executor callbackExecutor = Executors.newCachedThreadPool();
+    private final Executor broadcastExecutor = Executors.newCachedThreadPool();
+    private final Executor mConfigExecutor = Executors.newCachedThreadPool();
+
+
+    private final Context mContext;
+    protected final Object mLock = new Object();
+
+    private final InferenceInfoStore mInferenceInfoStore;
+    private RemoteOnDeviceSandboxedInferenceService mRemoteInferenceService;
+    private RemoteOnDeviceIntelligenceService mRemoteOnDeviceIntelligenceService;
+    volatile boolean mIsServiceEnabled;
+
+    @GuardedBy("mLock")
+    private int remoteInferenceServiceUid = -1;
+
+    @GuardedBy("mLock")
+    private String[] mTemporaryServiceNames;
+    @GuardedBy("mLock")
+    private String[] mTemporaryBroadcastKeys;
+    @GuardedBy("mLock")
+    private String mBroadcastPackageName = SYSTEM_PACKAGE;
+    @GuardedBy("mLock")
+    private String mTemporaryConfigNamespace;
+
+    private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener =
+            this::sendUpdatedConfig;
+
+
+    /**
+     * Handler used to reset the temporary service names.
+     */
+    private Handler mTemporaryHandler;
+    private final @NonNull Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+
+    public OnDeviceIntelligenceManagerService(Context context) {
+        super(context);
+        mContext = context;
+        mTemporaryServiceNames = new String[0];
+        mInferenceInfoStore = new InferenceInfoStore(MAX_AGE_MS);
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(
+                Context.ON_DEVICE_INTELLIGENCE_SERVICE, getOnDeviceIntelligenceManagerService(),
+                /* allowIsolated = */ true);
+        LocalManagerRegistry.addManager(OnDeviceIntelligenceManagerLocal.class,
+                    this::getRemoteInferenceServiceUid);
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+            DeviceConfig.addOnPropertiesChangedListener(
+                    NAMESPACE_ON_DEVICE_INTELLIGENCE,
+                    BackgroundThread.getExecutor(),
+                    (properties) -> onDeviceConfigChange(properties.getKeyset()));
+
+            mIsServiceEnabled = isServiceEnabled();
+        }
+    }
+
+    private void onDeviceConfigChange(@NonNull Set<String> keys) {
+        if (keys.contains(KEY_SERVICE_ENABLED)) {
+            mIsServiceEnabled = isServiceEnabled();
+        }
+    }
+
+    private boolean isServiceEnabled() {
+        return DeviceConfig.getBoolean(
+                NAMESPACE_ON_DEVICE_INTELLIGENCE,
+                KEY_SERVICE_ENABLED, DEFAULT_SERVICE_ENABLED);
+    }
+
+    private IBinder getOnDeviceIntelligenceManagerService() {
+        return new IOnDeviceIntelligenceManager.Stub() {
+            @Override
+            public String getRemoteServicePackageName() {
+                return OnDeviceIntelligenceManagerService.this.getRemoteConfiguredPackageName();
+            }
+
+            @Override
+            public List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) {
+                mContext.enforceCallingPermission(
+                        Manifest.permission.DUMP, TAG);
+                return OnDeviceIntelligenceManagerService.this.getLatestInferenceInfo(
+                        startTimeEpochMillis);
+            }
+
+            @Override
+            public void getVersion(RemoteCallback remoteCallback) {
+                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion");
+                Objects.requireNonNull(remoteCallback);
+                mContext.enforceCallingPermission(
+                        Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+                if (!mIsServiceEnabled) {
+                    Slog.w(TAG, "Service not available");
+                    remoteCallback.sendResult(null);
+                    return;
+                }
+                ensureRemoteIntelligenceServiceInitialized();
+                mRemoteOnDeviceIntelligenceService.postAsync(
+                        service -> {
+                            AndroidFuture future = new AndroidFuture();
+                            service.getVersion(new RemoteCallback(
+                                    result -> {
+                                        remoteCallback.sendResult(result);
+                                        future.complete(null);
+                                    }));
+                            return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
+                        });
+            }
+
+            @Override
+            public void getFeature(int id, IFeatureCallback featureCallback)
+                    throws RemoteException {
+                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatures");
+                Objects.requireNonNull(featureCallback);
+                mContext.enforceCallingPermission(
+                        Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+                if (!mIsServiceEnabled) {
+                    Slog.w(TAG, "Service not available");
+                    featureCallback.onFailure(
+                            OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE,
+                            "OnDeviceIntelligenceManagerService is unavailable",
+                            PersistableBundle.EMPTY);
+                    return;
+                }
+                ensureRemoteIntelligenceServiceInitialized();
+                int callerUid = Binder.getCallingUid();
+                mRemoteOnDeviceIntelligenceService.postAsync(
+                        service -> {
+                            AndroidFuture future = new AndroidFuture();
+                            service.getFeature(callerUid, id, new IFeatureCallback.Stub() {
+                                @Override
+                                public void onSuccess(Feature result) throws RemoteException {
+                                    featureCallback.onSuccess(result);
+                                    future.complete(null);
+                                }
+
+                                @Override
+                                public void onFailure(int errorCode, String errorMessage,
+                                        PersistableBundle errorParams) throws RemoteException {
+                                    featureCallback.onFailure(errorCode, errorMessage, errorParams);
+                                    future.completeExceptionally(new TimeoutException());
+                                }
+                            });
+                            return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
+                        });
+            }
+
+            @Override
+            public void listFeatures(IListFeaturesCallback listFeaturesCallback)
+                    throws RemoteException {
+                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatures");
+                Objects.requireNonNull(listFeaturesCallback);
+                mContext.enforceCallingPermission(
+                        Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+                if (!mIsServiceEnabled) {
+                    Slog.w(TAG, "Service not available");
+                    listFeaturesCallback.onFailure(
+                            OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE,
+                            "OnDeviceIntelligenceManagerService is unavailable",
+                            PersistableBundle.EMPTY);
+                    return;
+                }
+                ensureRemoteIntelligenceServiceInitialized();
+                int callerUid = Binder.getCallingUid();
+                mRemoteOnDeviceIntelligenceService.postAsync(
+                        service -> {
+                            AndroidFuture future = new AndroidFuture();
+                            service.listFeatures(callerUid,
+                                    new IListFeaturesCallback.Stub() {
+                                        @Override
+                                        public void onSuccess(List<Feature> result)
+                                                throws RemoteException {
+                                            listFeaturesCallback.onSuccess(result);
+                                            future.complete(null);
+                                        }
+
+                                        @Override
+                                        public void onFailure(int errorCode, String errorMessage,
+                                                PersistableBundle errorParams)
+                                                throws RemoteException {
+                                            listFeaturesCallback.onFailure(errorCode, errorMessage,
+                                                    errorParams);
+                                            future.completeExceptionally(new TimeoutException());
+                                        }
+                                    });
+                            return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
+                        });
+            }
+
+            @Override
+            public void getFeatureDetails(Feature feature,
+                    IFeatureDetailsCallback featureDetailsCallback)
+                    throws RemoteException {
+                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getFeatureStatus");
+                Objects.requireNonNull(feature);
+                Objects.requireNonNull(featureDetailsCallback);
+                mContext.enforceCallingPermission(
+                        Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+                if (!mIsServiceEnabled) {
+                    Slog.w(TAG, "Service not available");
+                    featureDetailsCallback.onFailure(
+                            OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE,
+                            "OnDeviceIntelligenceManagerService is unavailable",
+                            PersistableBundle.EMPTY);
+                    return;
+                }
+                ensureRemoteIntelligenceServiceInitialized();
+                int callerUid = Binder.getCallingUid();
+                mRemoteOnDeviceIntelligenceService.postAsync(
+                        service -> {
+                            AndroidFuture future = new AndroidFuture();
+                            service.getFeatureDetails(callerUid, feature,
+                                    new IFeatureDetailsCallback.Stub() {
+                                        @Override
+                                        public void onSuccess(FeatureDetails result)
+                                                throws RemoteException {
+                                            future.complete(null);
+                                            featureDetailsCallback.onSuccess(result);
+                                        }
+
+                                        @Override
+                                        public void onFailure(int errorCode, String errorMessage,
+                                                PersistableBundle errorParams)
+                                                throws RemoteException {
+                                            future.completeExceptionally(null);
+                                            featureDetailsCallback.onFailure(errorCode,
+                                                    errorMessage, errorParams);
+                                        }
+                                    });
+                            return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
+                        });
+            }
+
+            @Override
+            public void requestFeatureDownload(Feature feature,
+                    AndroidFuture cancellationSignalFuture,
+                    IDownloadCallback downloadCallback) throws RemoteException {
+                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload");
+                Objects.requireNonNull(feature);
+                Objects.requireNonNull(downloadCallback);
+                mContext.enforceCallingPermission(
+                        Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+                if (!mIsServiceEnabled) {
+                    Slog.w(TAG, "Service not available");
+                    downloadCallback.onDownloadFailed(
+                            DownloadCallback.DOWNLOAD_FAILURE_STATUS_UNAVAILABLE,
+                            "OnDeviceIntelligenceManagerService is unavailable",
+                            PersistableBundle.EMPTY);
+                }
+                ensureRemoteIntelligenceServiceInitialized();
+                int callerUid = Binder.getCallingUid();
+                mRemoteOnDeviceIntelligenceService.postAsync(
+                        service -> {
+                            AndroidFuture future = new AndroidFuture();
+                            ListenableDownloadCallback listenableDownloadCallback =
+                                    new ListenableDownloadCallback(
+                                            downloadCallback,
+                                            mMainHandler, future, getIdleTimeoutMs());
+                            service.requestFeatureDownload(callerUid, feature,
+                                    wrapCancellationFuture(cancellationSignalFuture),
+                                    listenableDownloadCallback);
+                            return future; // this future has no timeout because, actual download
+                            // might take long, but we fail early if there is no progress callbacks.
+                        }
+                );
+            }
+
+
+            @Override
+            public void requestTokenInfo(Feature feature,
+                    Bundle request,
+                    AndroidFuture cancellationSignalFuture,
+                    ITokenInfoCallback tokenInfoCallback) throws RemoteException {
+                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestTokenInfo");
+                AndroidFuture<Void> result = null;
+                try {
+                    Objects.requireNonNull(feature);
+                    sanitizeInferenceParams(request);
+                    Objects.requireNonNull(tokenInfoCallback);
+
+                    mContext.enforceCallingPermission(
+                            Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+                    if (!mIsServiceEnabled) {
+                        Slog.w(TAG, "Service not available");
+                        tokenInfoCallback.onFailure(
+                                OnDeviceIntelligenceException.ON_DEVICE_INTELLIGENCE_SERVICE_UNAVAILABLE,
+                                "OnDeviceIntelligenceManagerService is unavailable",
+                                PersistableBundle.EMPTY);
+                    }
+                    ensureRemoteInferenceServiceInitialized();
+                    int callerUid = Binder.getCallingUid();
+                    result = mRemoteInferenceService.postAsync(
+                            service -> {
+                                AndroidFuture future = new AndroidFuture();
+                                service.requestTokenInfo(callerUid, feature,
+                                        request,
+                                        wrapCancellationFuture(cancellationSignalFuture),
+                                        wrapWithValidation(tokenInfoCallback, future,
+                                                mInferenceInfoStore));
+                                return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
+                            });
+                    result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request),
+                            resourceClosingExecutor);
+                } finally {
+                    if (result == null) {
+                        resourceClosingExecutor.execute(() -> BundleUtil.tryCloseResource(request));
+                    }
+                }
+            }
+
+            @Override
+            public void processRequest(Feature feature,
+                    Bundle request,
+                    int requestType,
+                    AndroidFuture cancellationSignalFuture,
+                    AndroidFuture processingSignalFuture,
+                    IResponseCallback responseCallback)
+                    throws RemoteException {
+                AndroidFuture<Void> result = null;
+                try {
+                    Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest");
+                    Objects.requireNonNull(feature);
+                    sanitizeInferenceParams(request);
+                    Objects.requireNonNull(responseCallback);
+                    mContext.enforceCallingPermission(
+                            Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+                    if (!mIsServiceEnabled) {
+                        Slog.w(TAG, "Service not available");
+                        responseCallback.onFailure(
+                                OnDeviceIntelligenceException.PROCESSING_ERROR_SERVICE_UNAVAILABLE,
+                                "OnDeviceIntelligenceManagerService is unavailable",
+                                PersistableBundle.EMPTY);
+                    }
+                    ensureRemoteInferenceServiceInitialized();
+                    int callerUid = Binder.getCallingUid();
+                    result = mRemoteInferenceService.postAsync(
+                            service -> {
+                                AndroidFuture future = new AndroidFuture();
+                                service.processRequest(callerUid, feature,
+                                        request,
+                                        requestType,
+                                        wrapCancellationFuture(cancellationSignalFuture),
+                                        wrapProcessingFuture(processingSignalFuture),
+                                        wrapWithValidation(responseCallback,
+                                                resourceClosingExecutor, future,
+                                                mInferenceInfoStore));
+                                return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
+                            });
+                    result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request),
+                            resourceClosingExecutor);
+                } finally {
+                    if (result == null) {
+                        resourceClosingExecutor.execute(() -> BundleUtil.tryCloseResource(request));
+                    }
+                }
+            }
+
+            @Override
+            public void processRequestStreaming(Feature feature,
+                    Bundle request,
+                    int requestType,
+                    AndroidFuture cancellationSignalFuture,
+                    AndroidFuture processingSignalFuture,
+                    IStreamingResponseCallback streamingCallback) throws RemoteException {
+                AndroidFuture<Void> result = null;
+                try {
+                    Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming");
+                    Objects.requireNonNull(feature);
+                    sanitizeInferenceParams(request);
+                    Objects.requireNonNull(streamingCallback);
+                    mContext.enforceCallingPermission(
+                            Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+                    if (!mIsServiceEnabled) {
+                        Slog.w(TAG, "Service not available");
+                        streamingCallback.onFailure(
+                                OnDeviceIntelligenceException.PROCESSING_ERROR_SERVICE_UNAVAILABLE,
+                                "OnDeviceIntelligenceManagerService is unavailable",
+                                PersistableBundle.EMPTY);
+                    }
+                    ensureRemoteInferenceServiceInitialized();
+                    int callerUid = Binder.getCallingUid();
+                    result = mRemoteInferenceService.postAsync(
+                            service -> {
+                                AndroidFuture future = new AndroidFuture();
+                                service.processRequestStreaming(callerUid,
+                                        feature,
+                                        request, requestType,
+                                        wrapCancellationFuture(cancellationSignalFuture),
+                                        wrapProcessingFuture(processingSignalFuture),
+                                        wrapWithValidation(streamingCallback,
+                                                resourceClosingExecutor, future,
+                                                mInferenceInfoStore));
+                                return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
+                            });
+                    result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request),
+                            resourceClosingExecutor);
+                } finally {
+                    if (result == null) {
+                        resourceClosingExecutor.execute(() -> BundleUtil.tryCloseResource(request));
+                    }
+                }
+            }
+
+            @Override
+            public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
+                    String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
+                new OnDeviceIntelligenceShellCommand(OnDeviceIntelligenceManagerService.this).exec(
+                        this, in, out, err, args, callback, resultReceiver);
+            }
+        };
+    }
+
+    private void ensureRemoteIntelligenceServiceInitialized() {
+        synchronized (mLock) {
+            if (mRemoteOnDeviceIntelligenceService == null) {
+                String serviceName = getServiceNames()[0];
+                Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, false));
+                mRemoteOnDeviceIntelligenceService = new RemoteOnDeviceIntelligenceService(mContext,
+                        ComponentName.unflattenFromString(serviceName),
+                        UserHandle.SYSTEM.getIdentifier());
+                mRemoteOnDeviceIntelligenceService.setServiceLifecycleCallbacks(
+                        new ServiceConnector.ServiceLifecycleCallbacks<>() {
+                            @Override
+                            public void onConnected(
+                                    @NonNull IOnDeviceIntelligenceService service) {
+                                try {
+                                    service.registerRemoteServices(
+                                            getRemoteProcessingService());
+                                    service.ready();
+                                } catch (RemoteException ex) {
+                                    Slog.w(TAG, "Failed to send connected event", ex);
+                                }
+                            }
+                        });
+            }
+        }
+    }
+
+    @NonNull
+    private IRemoteProcessingService.Stub getRemoteProcessingService() {
+        return new IRemoteProcessingService.Stub() {
+            @Override
+            public void updateProcessingState(
+                    Bundle processingState,
+                    IProcessingUpdateStatusCallback callback) {
+                callbackExecutor.execute(() -> {
+                    AndroidFuture<Void> result = null;
+                    try {
+                        sanitizeStateParams(processingState);
+                        ensureRemoteInferenceServiceInitialized();
+                        result = mRemoteInferenceService.post(
+                                service -> service.updateProcessingState(
+                                        processingState, callback));
+                        result.whenCompleteAsync(
+                                (c, e) -> BundleUtil.tryCloseResource(processingState),
+                                resourceClosingExecutor);
+                    } finally {
+                        if (result == null) {
+                            resourceClosingExecutor.execute(
+                                    () -> BundleUtil.tryCloseResource(processingState));
+                        }
+                    }
+                });
+            }
+        };
+    }
+
+    private void ensureRemoteInferenceServiceInitialized() {
+        synchronized (mLock) {
+            if (mRemoteInferenceService == null) {
+                String serviceName = getServiceNames()[1];
+                Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, true));
+                mRemoteInferenceService = new RemoteOnDeviceSandboxedInferenceService(mContext,
+                        ComponentName.unflattenFromString(serviceName),
+                        UserHandle.SYSTEM.getIdentifier());
+                mRemoteInferenceService.setServiceLifecycleCallbacks(
+                        new ServiceConnector.ServiceLifecycleCallbacks<>() {
+                            @Override
+                            public void onConnected(
+                                    @NonNull IOnDeviceSandboxedInferenceService service) {
+                                try {
+                                    ensureRemoteIntelligenceServiceInitialized();
+                                    service.registerRemoteStorageService(
+                                            getIRemoteStorageService(), new IRemoteCallback.Stub() {
+                                                @Override
+                                                public void sendResult(Bundle bundle) {
+                                                    final int uid = Binder.getCallingUid();
+                                                    setRemoteInferenceServiceUid(uid);
+                                                }
+                                            });
+                                    mRemoteOnDeviceIntelligenceService.run(
+                                            IOnDeviceIntelligenceService::notifyInferenceServiceConnected);
+                                    broadcastExecutor.execute(
+                                            () -> registerModelLoadingBroadcasts(service));
+                                    mConfigExecutor.execute(
+                                            () -> registerDeviceConfigChangeListener());
+                                } catch (RemoteException ex) {
+                                    Slog.w(TAG, "Failed to send connected event", ex);
+                                }
+                            }
+
+                            @Override
+                            public void onDisconnected(
+                                    @NonNull IOnDeviceSandboxedInferenceService service) {
+                                ensureRemoteIntelligenceServiceInitialized();
+                                mRemoteOnDeviceIntelligenceService.run(
+                                        IOnDeviceIntelligenceService::notifyInferenceServiceDisconnected);
+                            }
+
+                            @Override
+                            public void onBinderDied() {
+                                ensureRemoteIntelligenceServiceInitialized();
+                                mRemoteOnDeviceIntelligenceService.run(
+                                        IOnDeviceIntelligenceService::notifyInferenceServiceDisconnected);
+                            }
+                        });
+            }
+        }
+    }
+
+    private void registerModelLoadingBroadcasts(IOnDeviceSandboxedInferenceService service) {
+        String[] modelBroadcastKeys;
+        try {
+            modelBroadcastKeys = getBroadcastKeys();
+        } catch (Resources.NotFoundException e) {
+            Slog.d(TAG, "Skipping model broadcasts as broadcast intents configured.");
+            return;
+        }
+
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY, true);
+        try {
+            service.updateProcessingState(bundle, new IProcessingUpdateStatusCallback.Stub() {
+                @Override
+                public void onSuccess(PersistableBundle statusParams) {
+                    Binder.clearCallingIdentity();
+                    synchronized (mLock) {
+                        if (statusParams.containsKey(MODEL_LOADED_BUNDLE_KEY)) {
+                            String modelLoadedBroadcastKey = modelBroadcastKeys[0];
+                            if (modelLoadedBroadcastKey != null
+                                    && !modelLoadedBroadcastKey.isEmpty()) {
+                                final Intent intent = new Intent(modelLoadedBroadcastKey);
+                                intent.setPackage(mBroadcastPackageName);
+                                mContext.sendBroadcast(intent,
+                                        Manifest.permission.USE_ON_DEVICE_INTELLIGENCE);
+                            }
+                        } else if (statusParams.containsKey(MODEL_UNLOADED_BUNDLE_KEY)) {
+                            String modelUnloadedBroadcastKey = modelBroadcastKeys[1];
+                            if (modelUnloadedBroadcastKey != null
+                                    && !modelUnloadedBroadcastKey.isEmpty()) {
+                                final Intent intent = new Intent(modelUnloadedBroadcastKey);
+                                intent.setPackage(mBroadcastPackageName);
+                                mContext.sendBroadcast(intent,
+                                        Manifest.permission.USE_ON_DEVICE_INTELLIGENCE);
+                            }
+                        }
+                    }
+                }
+
+                @Override
+                public void onFailure(int errorCode, String errorMessage) {
+                    Slog.e(TAG, "Failed to register model loading callback with status code",
+                            new OnDeviceIntelligenceException(errorCode, errorMessage));
+                }
+            });
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to register model loading callback with status code", e);
+        }
+    }
+
+    private void registerDeviceConfigChangeListener() {
+        Log.d(TAG, "registerDeviceConfigChangeListener");
+        String configNamespace = getConfigNamespace();
+        if (configNamespace.isEmpty()) {
+            Slog.e(TAG, "config_defaultOnDeviceIntelligenceDeviceConfigNamespace is empty");
+            return;
+        }
+        DeviceConfig.addOnPropertiesChangedListener(
+                configNamespace,
+                mConfigExecutor,
+                mOnPropertiesChangedListener);
+    }
+
+    private String getConfigNamespace() {
+        synchronized (mLock) {
+            if (mTemporaryConfigNamespace != null) {
+                return mTemporaryConfigNamespace;
+            }
+
+            return mContext.getResources().getString(
+                    R.string.config_defaultOnDeviceIntelligenceDeviceConfigNamespace);
+        }
+    }
+
+    private void sendUpdatedConfig(
+            DeviceConfig.Properties props) {
+        Log.d(TAG, "sendUpdatedConfig");
+
+        PersistableBundle persistableBundle = new PersistableBundle();
+        for (String key : props.getKeyset()) {
+            persistableBundle.putString(key, props.getString(key, ""));
+        }
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(DEVICE_CONFIG_UPDATE_BUNDLE_KEY, persistableBundle);
+        ensureRemoteInferenceServiceInitialized();
+        mRemoteInferenceService.run(service -> service.updateProcessingState(bundle,
+                new IProcessingUpdateStatusCallback.Stub() {
+                    @Override
+                    public void onSuccess(PersistableBundle result) {
+                        Slog.d(TAG, "Config update successful." + result);
+                    }
+
+                    @Override
+                    public void onFailure(int errorCode, String errorMessage) {
+                        Slog.e(TAG, "Config update failed with code ["
+                                + String.valueOf(errorCode) + "] and message = " + errorMessage);
+                    }
+                }));
+    }
+
+    @NonNull
+    private IRemoteStorageService.Stub getIRemoteStorageService() {
+        return new IRemoteStorageService.Stub() {
+            @Override
+            public void getReadOnlyFileDescriptor(
+                    String filePath,
+                    AndroidFuture<ParcelFileDescriptor> future) {
+                ensureRemoteIntelligenceServiceInitialized();
+                AndroidFuture<ParcelFileDescriptor> pfdFuture = new AndroidFuture<>();
+                mRemoteOnDeviceIntelligenceService.run(
+                        service -> service.getReadOnlyFileDescriptor(
+                                filePath, pfdFuture));
+                pfdFuture.whenCompleteAsync((pfd, error) -> {
+                    try {
+                        if (error != null) {
+                            future.completeExceptionally(error);
+                        } else {
+                            validatePfdReadOnly(pfd);
+                            future.complete(pfd);
+                        }
+                    } finally {
+                        tryClosePfd(pfd);
+                    }
+                }, callbackExecutor);
+            }
+
+            @Override
+            public void getReadOnlyFeatureFileDescriptorMap(
+                    Feature feature,
+                    RemoteCallback remoteCallback) {
+                ensureRemoteIntelligenceServiceInitialized();
+                mRemoteOnDeviceIntelligenceService.run(
+                        service -> service.getReadOnlyFeatureFileDescriptorMap(
+                                feature,
+                                new RemoteCallback(result -> callbackExecutor.execute(() -> {
+                                    try {
+                                        if (result == null) {
+                                            remoteCallback.sendResult(null);
+                                        }
+                                        for (String key : result.keySet()) {
+                                            ParcelFileDescriptor pfd = result.getParcelable(key,
+                                                    ParcelFileDescriptor.class);
+                                            validatePfdReadOnly(pfd);
+                                        }
+                                        remoteCallback.sendResult(result);
+                                    } finally {
+                                        resourceClosingExecutor.execute(
+                                                () -> BundleUtil.tryCloseResource(result));
+                                    }
+                                }))));
+            }
+        };
+    }
+
+    private void validateServiceElevated(String serviceName, boolean checkIsolated) {
+        try {
+            if (TextUtils.isEmpty(serviceName)) {
+                throw new IllegalStateException(
+                        "Remote service is not configured to complete the request");
+            }
+            ComponentName serviceComponent = ComponentName.unflattenFromString(
+                    serviceName);
+            ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
+                    serviceComponent,
+                    PackageManager.MATCH_DIRECT_BOOT_AWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+                    UserHandle.SYSTEM.getIdentifier());
+            if (serviceInfo != null) {
+                if (!checkIsolated) {
+                    checkServiceRequiresPermission(serviceInfo,
+                            Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE);
+                    return;
+                }
+
+                checkServiceRequiresPermission(serviceInfo,
+                        Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE);
+                if (!isIsolatedService(serviceInfo)) {
+                    throw new SecurityException(
+                            "Call required an isolated service, but the configured service: "
+                                    + serviceName + ", is not isolated");
+                }
+            } else {
+                throw new IllegalStateException(
+                        "Remote service is not configured to complete the request.");
+            }
+        } catch (RemoteException e) {
+            throw new IllegalStateException("Could not fetch service info for remote services", e);
+        }
+    }
+
+    private static void checkServiceRequiresPermission(ServiceInfo serviceInfo,
+            String requiredPermission) {
+        final String permission = serviceInfo.permission;
+        if (!requiredPermission.equals(permission)) {
+            throw new SecurityException(String.format(
+                    "Service %s requires %s permission. Found %s permission",
+                    serviceInfo.getComponentName(),
+                    requiredPermission,
+                    serviceInfo.permission));
+        }
+    }
+
+    private static boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) {
+        return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
+                && (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0;
+    }
+
+    private List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) {
+        return mInferenceInfoStore.getLatestInferenceInfo(startTimeEpochMillis);
+    }
+
+    @Nullable
+    public String getRemoteConfiguredPackageName() {
+        try {
+            String[] serviceNames = getServiceNames();
+            ComponentName componentName = ComponentName.unflattenFromString(serviceNames[1]);
+            if (componentName != null) {
+                return componentName.getPackageName();
+            }
+        } catch (Resources.NotFoundException e) {
+            Slog.e(TAG, "Could not find resource", e);
+        }
+
+        return null;
+    }
+
+
+    protected String[] getServiceNames() throws Resources.NotFoundException {
+        // TODO 329240495 : Consider a small class with explicit field names for the two services
+        synchronized (mLock) {
+            if (mTemporaryServiceNames != null && mTemporaryServiceNames.length == 2) {
+                return mTemporaryServiceNames;
+            }
+        }
+        return new String[]{mContext.getResources().getString(
+                R.string.config_defaultOnDeviceIntelligenceService),
+                mContext.getResources().getString(
+                        R.string.config_defaultOnDeviceSandboxedInferenceService)};
+    }
+
+    protected String[] getBroadcastKeys() throws Resources.NotFoundException {
+        // TODO 329240495 : Consider a small class with explicit field names for the two services
+        synchronized (mLock) {
+            if (mTemporaryBroadcastKeys != null && mTemporaryBroadcastKeys.length == 2) {
+                return mTemporaryBroadcastKeys;
+            }
+        }
+
+        return new String[]{ MODEL_LOADED_BROADCAST_INTENT, MODEL_UNLOADED_BROADCAST_INTENT };
+    }
+
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void setTemporaryServices(@NonNull String[] componentNames, int durationMs) {
+        Objects.requireNonNull(componentNames);
+        enforceShellOnly(Binder.getCallingUid(), "setTemporaryServices");
+        mContext.enforceCallingPermission(
+                Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+        synchronized (mLock) {
+            mTemporaryServiceNames = componentNames;
+            if (mRemoteInferenceService != null) {
+                mRemoteInferenceService.unbind();
+                mRemoteInferenceService = null;
+            }
+            if (mRemoteOnDeviceIntelligenceService != null) {
+                mRemoteOnDeviceIntelligenceService.unbind();
+                mRemoteOnDeviceIntelligenceService = null;
+            }
+
+            if (durationMs != -1) {
+                getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE,
+                        durationMs);
+            }
+        }
+    }
+
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void setModelBroadcastKeys(@NonNull String[] broadcastKeys, String receiverPackageName,
+            int durationMs) {
+        Objects.requireNonNull(broadcastKeys);
+        enforceShellOnly(Binder.getCallingUid(), "setModelBroadcastKeys");
+        mContext.enforceCallingPermission(
+                Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+        synchronized (mLock) {
+            mTemporaryBroadcastKeys = broadcastKeys;
+            mBroadcastPackageName = receiverPackageName;
+            if (durationMs != -1) {
+                getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_BROADCAST_KEYS, durationMs);
+            }
+        }
+    }
+
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
+    public void setTemporaryDeviceConfigNamespace(@NonNull String configNamespace,
+            int durationMs) {
+        Objects.requireNonNull(configNamespace);
+        enforceShellOnly(Binder.getCallingUid(), "setTemporaryDeviceConfigNamespace");
+        mContext.enforceCallingPermission(
+                Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
+        synchronized (mLock) {
+            mTemporaryConfigNamespace = configNamespace;
+            if (durationMs != -1) {
+                getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_CONFIG_NAMESPACE,
+                        durationMs);
+            }
+        }
+    }
+
+    /**
+     * Reset the temporary services set in CTS tests, this method is primarily used to only revert
+     * the changes caused by CTS tests.
+     */
+    public void resetTemporaryServices() {
+        synchronized (mLock) {
+            if (mTemporaryHandler != null) {
+                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
+                mTemporaryHandler = null;
+            }
+
+            mRemoteInferenceService = null;
+            mRemoteOnDeviceIntelligenceService = null;
+            mTemporaryServiceNames = new String[0];
+        }
+    }
+
+    /**
+     * Throws if the caller is not of a shell (or root) UID.
+     *
+     * @param callingUid pass Binder.callingUid().
+     */
+    public static void enforceShellOnly(int callingUid, String message) {
+        if (callingUid == android.os.Process.SHELL_UID
+                || callingUid == android.os.Process.ROOT_UID) {
+            return; // okay
+        }
+
+        throw new SecurityException(message + ": Only shell user can call it");
+    }
+
+    private AndroidFuture<IBinder> wrapCancellationFuture(
+            AndroidFuture future) {
+        if (future == null) {
+            return null;
+        }
+        AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>();
+        cancellationFuture.whenCompleteAsync((c, e) -> {
+            if (e != null) {
+                Log.e(TAG, "Error forwarding ICancellationSignal to manager layer", e);
+                future.completeExceptionally(e);
+            } else {
+                future.complete(new ICancellationSignal.Stub() {
+                    @Override
+                    public void cancel() throws RemoteException {
+                        ICancellationSignal.Stub.asInterface(c).cancel();
+                    }
+                });
+            }
+        });
+        return cancellationFuture;
+    }
+
+    private AndroidFuture<IBinder> wrapProcessingFuture(
+            AndroidFuture future) {
+        if (future == null) {
+            return null;
+        }
+        AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>();
+        processingSignalFuture.whenCompleteAsync((c, e) -> {
+            if (e != null) {
+                future.completeExceptionally(e);
+            } else {
+                future.complete(new IProcessingSignal.Stub() {
+                    @Override
+                    public void sendSignal(PersistableBundle actionParams) throws RemoteException {
+                        IProcessingSignal.Stub.asInterface(c).sendSignal(actionParams);
+                    }
+                });
+            }
+        });
+        return processingSignalFuture;
+    }
+
+    private static void tryClosePfd(ParcelFileDescriptor pfd) {
+        if (pfd != null) {
+            try {
+                pfd.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to close parcel file descriptor ", e);
+            }
+        }
+    }
+
+    private synchronized Handler getTemporaryHandler() {
+        if (mTemporaryHandler == null) {
+            mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
+                @Override
+                public void handleMessage(Message msg) {
+                    synchronized (mLock) {
+                        if (msg.what == MSG_RESET_TEMPORARY_SERVICE) {
+                            resetTemporaryServices();
+                        } else if (msg.what == MSG_RESET_BROADCAST_KEYS) {
+                            mTemporaryBroadcastKeys = null;
+                            mBroadcastPackageName = SYSTEM_PACKAGE;
+                        } else if (msg.what == MSG_RESET_CONFIG_NAMESPACE) {
+                            mTemporaryConfigNamespace = null;
+                        } else {
+                            Slog.wtf(TAG, "invalid handler msg: " + msg);
+                        }
+                    }
+                }
+            };
+        }
+
+        return mTemporaryHandler;
+    }
+
+    private long getIdleTimeoutMs() {
+        return Settings.Secure.getLongForUser(mContext.getContentResolver(),
+                Settings.Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, TimeUnit.HOURS.toMillis(1),
+                mContext.getUserId());
+    }
+
+    private int getRemoteInferenceServiceUid() {
+        synchronized (mLock) {
+            return remoteInferenceServiceUid;
+        }
+    }
+
+    private void setRemoteInferenceServiceUid(int remoteInferenceServiceUid) {
+        synchronized (mLock) {
+            this.remoteInferenceServiceUid = remoteInferenceServiceUid;
+        }
+    }
+}
diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java
new file mode 100644
index 0000000..d2c84fa
--- /dev/null
+++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ondeviceintelligence;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.ShellCommand;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+
+final class OnDeviceIntelligenceShellCommand extends ShellCommand {
+    private static final String TAG = OnDeviceIntelligenceShellCommand.class.getSimpleName();
+
+    @NonNull
+    private final OnDeviceIntelligenceManagerService mService;
+
+    OnDeviceIntelligenceShellCommand(@NonNull OnDeviceIntelligenceManagerService service) {
+        mService = service;
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        if (cmd == null) {
+            return handleDefaultCommands(cmd);
+        }
+
+        switch (cmd) {
+            case "set-temporary-services":
+                return setTemporaryServices();
+            case "get-services":
+                return getConfiguredServices();
+            case "set-model-broadcasts":
+                return setBroadcastKeys();
+            case "set-deviceconfig-namespace":
+                return setDeviceConfigNamespace();
+            default:
+                return handleDefaultCommands(cmd);
+        }
+    }
+
+    @Override
+    public void onHelp() {
+        PrintWriter pw = getOutPrintWriter();
+        pw.println("OnDeviceIntelligenceShellCommand commands: ");
+        pw.println("  help");
+        pw.println("    Print this help text.");
+        pw.println();
+        pw.println(
+                "  set-temporary-services [IntelligenceServiceComponentName] "
+                        + "[InferenceServiceComponentName] [DURATION]");
+        pw.println("    Temporarily (for DURATION ms) changes the service implementations.");
+        pw.println("    To reset, call without any arguments.");
+
+        pw.println("  get-services To get the names of services that are currently being used.");
+        pw.println(
+                "  set-model-broadcasts [ModelLoadedBroadcastKey] [ModelUnloadedBroadcastKey] "
+                        + "[ReceiverPackageName] "
+                        + "[DURATION] To set the names of broadcast intent keys that are to be "
+                        + "emitted for cts tests.");
+        pw.println(
+                "  set-deviceconfig-namespace [DeviceConfigNamespace] "
+                        + "[DURATION] To set the device config namespace "
+                        + "to use for cts tests.");
+    }
+
+    private int setTemporaryServices() {
+        final PrintWriter out = getOutPrintWriter();
+        final String intelligenceServiceName = getNextArg();
+        final String inferenceServiceName = getNextArg();
+
+        if (getRemainingArgsCount() == 0 && intelligenceServiceName == null
+                && inferenceServiceName == null) {
+            OnDeviceIntelligenceManagerService.enforceShellOnly(Binder.getCallingUid(),
+                    "resetTemporaryServices");
+            mService.resetTemporaryServices();
+            out.println("OnDeviceIntelligenceManagerService temporary reset. ");
+            return 0;
+        }
+
+        Objects.requireNonNull(intelligenceServiceName);
+        Objects.requireNonNull(inferenceServiceName);
+        final int duration = Integer.parseInt(getNextArgRequired());
+        mService.setTemporaryServices(
+                new String[]{intelligenceServiceName, inferenceServiceName},
+                duration);
+        out.println("OnDeviceIntelligenceService temporarily set to " + intelligenceServiceName
+                + " \n and \n OnDeviceTrustedInferenceService set to " + inferenceServiceName
+                + " for " + duration + "ms");
+        return 0;
+    }
+
+    private int getConfiguredServices() {
+        final PrintWriter out = getOutPrintWriter();
+        String[] services = mService.getServiceNames();
+        out.println("OnDeviceIntelligenceService set to :  " + services[0]
+                + " \n and \n OnDeviceTrustedInferenceService set to : " + services[1]);
+        return 0;
+    }
+
+    private int setBroadcastKeys() {
+        final PrintWriter out = getOutPrintWriter();
+        final String modelLoadedKey = getNextArgRequired();
+        final String modelUnloadedKey = getNextArgRequired();
+        final String receiverPackageName = getNextArg();
+
+        final int duration = Integer.parseInt(getNextArgRequired());
+        mService.setModelBroadcastKeys(
+                new String[]{modelLoadedKey, modelUnloadedKey}, receiverPackageName, duration);
+        out.println("OnDeviceIntelligence Model Loading broadcast keys temporarily set to "
+                + modelLoadedKey
+                + " \n and \n OnDeviceTrustedInferenceService set to " + modelUnloadedKey
+                + "\n and Package name set to : " + receiverPackageName
+                + " for " + duration + "ms");
+        return 0;
+    }
+
+    private int setDeviceConfigNamespace() {
+        final PrintWriter out = getOutPrintWriter();
+        final String configNamespace = getNextArg();
+
+        final int duration = Integer.parseInt(getNextArgRequired());
+        mService.setTemporaryDeviceConfigNamespace(configNamespace, duration);
+        out.println("OnDeviceIntelligence DeviceConfig Namespace temporarily set to "
+                + configNamespace
+                + " for " + duration + "ms");
+        return 0;
+    }
+
+}
\ No newline at end of file
diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java
new file mode 100644
index 0000000..ac9747a
--- /dev/null
+++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ondeviceintelligence;
+
+import static android.content.Context.BIND_FOREGROUND_SERVICE;
+import static android.content.Context.BIND_INCLUDE_CAPABILITIES;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.service.ondeviceintelligence.IOnDeviceIntelligenceService;
+import android.service.ondeviceintelligence.OnDeviceIntelligenceService;
+
+import com.android.internal.infra.ServiceConnector;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Manages the connection to the remote on-device intelligence service. Also, handles unbinding
+ * logic set by the service implementation via a Secure Settings flag.
+ */
+public class RemoteOnDeviceIntelligenceService extends
+        ServiceConnector.Impl<IOnDeviceIntelligenceService> {
+    private static final long LONG_TIMEOUT = TimeUnit.HOURS.toMillis(4);
+    private static final String TAG =
+            RemoteOnDeviceIntelligenceService.class.getSimpleName();
+
+    RemoteOnDeviceIntelligenceService(Context context, ComponentName serviceName,
+            int userId) {
+        super(context, new Intent(
+                        OnDeviceIntelligenceService.SERVICE_INTERFACE).setComponent(serviceName),
+                BIND_FOREGROUND_SERVICE | BIND_INCLUDE_CAPABILITIES, userId,
+                IOnDeviceIntelligenceService.Stub::asInterface);
+
+        // Bind right away
+        connect();
+    }
+
+    @Override
+    protected long getRequestTimeoutMs() {
+        return LONG_TIMEOUT;
+    }
+
+    @Override
+    protected long getAutoDisconnectTimeoutMs() {
+        return Settings.Secure.getLongForUser(mContext.getContentResolver(),
+                Settings.Secure.ON_DEVICE_INTELLIGENCE_UNBIND_TIMEOUT_MS,
+                TimeUnit.SECONDS.toMillis(30),
+                mContext.getUserId());
+    }
+}
diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
new file mode 100644
index 0000000..18b1383
--- /dev/null
+++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java
@@ -0,0 +1,76 @@
+/*
+ * 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.ondeviceintelligence;
+
+import static android.content.Context.BIND_FOREGROUND_SERVICE;
+import static android.content.Context.BIND_INCLUDE_CAPABILITIES;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService;
+import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService;
+
+import com.android.internal.infra.ServiceConnector;
+
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Manages the connection to the remote on-device sand boxed inference service. Also, handles
+ * unbinding
+ * logic set by the service implementation via a SecureSettings flag.
+ */
+public class RemoteOnDeviceSandboxedInferenceService extends
+        ServiceConnector.Impl<IOnDeviceSandboxedInferenceService> {
+    private static final long LONG_TIMEOUT = TimeUnit.HOURS.toMillis(1);
+
+    /**
+     * Creates an instance of {@link ServiceConnector}
+     *
+     * See {@code protected} methods for optional parameters you can override.
+     *
+     * @param context to be used for {@link Context#bindServiceAsUser binding} and
+     *                {@link Context#unbindService unbinding}
+     * @param userId  to be used for {@link Context#bindServiceAsUser binding}
+     */
+    RemoteOnDeviceSandboxedInferenceService(Context context, ComponentName serviceName,
+            int userId) {
+        super(context, new Intent(
+                        OnDeviceSandboxedInferenceService.SERVICE_INTERFACE).setComponent(serviceName),
+                BIND_FOREGROUND_SERVICE | BIND_INCLUDE_CAPABILITIES, userId,
+                IOnDeviceSandboxedInferenceService.Stub::asInterface);
+
+        // Bind right away
+        connect();
+    }
+
+    @Override
+    protected long getRequestTimeoutMs() {
+        return LONG_TIMEOUT;
+    }
+
+
+    @Override
+    protected long getAutoDisconnectTimeoutMs() {
+        return Settings.Secure.getLongForUser(mContext.getContentResolver(),
+                Settings.Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS,
+                TimeUnit.SECONDS.toMillis(30),
+                mContext.getUserId());
+    }
+}
diff --git a/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java
new file mode 100644
index 0000000..32f0698
--- /dev/null
+++ b/packages/NeuralNetworks/service/platform/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java
@@ -0,0 +1,97 @@
+/*
+ * 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.ondeviceintelligence.callbacks;
+
+import android.app.ondeviceintelligence.IDownloadCallback;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class extends the {@link IDownloadCallback} and adds a timeout Runnable to the callback
+ * such that, in the case where the callback methods are not invoked, we do not have to wait for
+ * timeout based on {@link #onDownloadCompleted} which might take minutes or hours to complete in
+ * some cases. Instead, in such cases we rely on the remote service sending progress updates and if
+ * there are *no* progress callbacks in the duration of {@link #idleTimeoutMs}, we can assume the
+ * download will not complete and enabling faster cleanup.
+ */
+public class ListenableDownloadCallback extends IDownloadCallback.Stub implements Runnable {
+    private final IDownloadCallback callback;
+    private final Handler handler;
+    private final AndroidFuture future;
+    private final long idleTimeoutMs;
+
+    /**
+     * Constructor to create a ListenableDownloadCallback.
+     *
+     * @param callback      callback to send download updates to caller.
+     * @param handler       handler to schedule timeout runnable.
+     * @param future        future to complete to signal the callback has reached a terminal state.
+     * @param idleTimeoutMs timeout within which download updates should be received.
+     */
+    public ListenableDownloadCallback(IDownloadCallback callback, Handler handler,
+            AndroidFuture future,
+            long idleTimeoutMs) {
+        this.callback = callback;
+        this.handler = handler;
+        this.future = future;
+        this.idleTimeoutMs = idleTimeoutMs;
+        handler.postDelayed(this,
+                idleTimeoutMs); // init the timeout runnable in case no callback is ever invoked
+    }
+
+    @Override
+    public void onDownloadStarted(long bytesToDownload) throws RemoteException {
+        callback.onDownloadStarted(bytesToDownload);
+        handler.removeCallbacks(this);
+        handler.postDelayed(this, idleTimeoutMs);
+    }
+
+    @Override
+    public void onDownloadProgress(long bytesDownloaded) throws RemoteException {
+        callback.onDownloadProgress(bytesDownloaded);
+        handler.removeCallbacks(this); // remove previously queued timeout tasks.
+        handler.postDelayed(this, idleTimeoutMs); // queue fresh timeout task for next update.
+    }
+
+    @Override
+    public void onDownloadFailed(int failureStatus,
+            String errorMessage, PersistableBundle errorParams) throws RemoteException {
+        callback.onDownloadFailed(failureStatus, errorMessage, errorParams);
+        handler.removeCallbacks(this);
+        future.completeExceptionally(new TimeoutException());
+    }
+
+    @Override
+    public void onDownloadCompleted(
+            android.os.PersistableBundle downloadParams) throws RemoteException {
+        callback.onDownloadCompleted(downloadParams);
+        handler.removeCallbacks(this);
+        future.complete(null);
+    }
+
+    @Override
+    public void run() {
+        future.completeExceptionally(
+                new TimeoutException()); // complete the future as we haven't received updates
+        // for download progress.
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt
index 265c065..bfaeb42 100644
--- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt
+++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt
@@ -16,6 +16,9 @@
 
 package com.android.settingslib.widget
 
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.CallSuper
 import androidx.preference.PreferenceFragmentCompat
 import androidx.preference.PreferenceScreen
 import androidx.recyclerview.widget.RecyclerView
@@ -23,9 +26,18 @@
 /** Base class for Settings to use PreferenceFragmentCompat */
 abstract class SettingsBasePreferenceFragment : PreferenceFragmentCompat() {
 
+    @CallSuper
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        if (SettingsThemeHelper.isExpressiveTheme(requireContext())) {
+            // Don't allow any divider in between the preferences in expressive design.
+            setDivider(null)
+        }
+    }
+
     override fun onCreateAdapter(preferenceScreen: PreferenceScreen): RecyclerView.Adapter<*> {
         if (SettingsThemeHelper.isExpressiveTheme(requireContext()))
             return SettingsPreferenceGroupAdapter(preferenceScreen)
         return super.onCreateAdapter(preferenceScreen)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml
index 63c8929..3d3dad3 100644
--- a/packages/SettingsLib/res/values/arrays.xml
+++ b/packages/SettingsLib/res/values/arrays.xml
@@ -683,9 +683,9 @@
 
     <!-- Values for showing shade on external display for developers -->
     <string-array name="shade_display_awareness_values" >
-        <item>device-display</item>
-        <item>external-display</item>
-        <item>focus-based</item>
+        <item>default_display</item>
+        <item>any_external_display</item>
+        <item>status_bar_latest_touch</item>
     </string-array>
 
 </resources>
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 731cb72..18bebd4 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -52,6 +52,7 @@
         Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT,
         Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
         Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
+        Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS,
         Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
         Settings.Secure.CONTRAST_LEVEL,
         Settings.Secure.ACCESSIBILITY_CAPTIONING_PRESET,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 039832c..1d7608d 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -88,6 +88,9 @@
         VALIDATORS.put(Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(
+                Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS,
+                new DiscreteValueValidator(new String[] {"0", "1", "2"}));
         VALIDATORS.put(Secure.CONTRAST_LEVEL, new InclusiveFloatRangeValidator(-1f, 1f));
         VALIDATORS.put(
                 Secure.ACCESSIBILITY_CAPTIONING_PRESET,
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 326bff4..1c4def3 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -35,6 +35,7 @@
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiManager;
 import android.os.Build;
+import android.os.LocaleList;
 import android.os.ParcelFileDescriptor;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -269,7 +270,7 @@
         byte[] secureSettingsData = getSecureSettings();
         byte[] globalSettingsData = getGlobalSettings();
         byte[] lockSettingsData   = getLockSettings(UserHandle.myUserId());
-        byte[] locale = mSettingsHelper.getLocaleData();
+        byte[] locale = getLocaleSettings();
         byte[] softApConfigData = getSoftAPConfiguration();
         byte[] netPoliciesData = getNetworkPolicies();
         byte[] wifiFullConfigData = getNewWifiConfigData();
@@ -408,7 +409,12 @@
                 case KEY_LOCALE :
                     byte[] localeData = new byte[size];
                     data.readEntityData(localeData, 0, size);
-                    mSettingsHelper.setLocaleData(localeData, size);
+                    mSettingsHelper
+                        .setLocaleData(
+                            localeData,
+                            size,
+                            mBackupRestoreEventLogger,
+                            KEY_LOCALE);
                     break;
 
                 case KEY_WIFI_CONFIG :
@@ -545,7 +551,9 @@
             if (DEBUG_BACKUP) Log.d(TAG, nBytes + " bytes of locale data");
             if (nBytes > buffer.length) buffer = new byte[nBytes];
             in.readFully(buffer, 0, nBytes);
-            mSettingsHelper.setLocaleData(buffer, nBytes);
+            mSettingsHelper
+                .setLocaleData(
+                    buffer, nBytes, mBackupRestoreEventLogger, KEY_LOCALE);
 
             // Restore older backups performing the necessary migrations.
             if (version < FULL_BACKUP_ADDED_WIFI_NEW) {
@@ -1410,6 +1418,15 @@
         return mWifiManager.retrieveBackupData();
     }
 
+    private byte[] getLocaleSettings() {
+        if (!areAgentMetricsEnabled) {
+            return mSettingsHelper.getLocaleData();
+        }
+        LocaleList localeList = mSettingsHelper.getLocaleList();
+        numberOfSettingsPerKey.put(KEY_LOCALE, localeList.size());
+        return localeList.toLanguageTags().getBytes();
+    }
+
     private void restoreNewWifiConfigData(byte[] bytes) {
         if (DEBUG_BACKUP) {
             Log.v(TAG, "Applying restored wifi data");
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index ea8ae7b..924c151 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -19,11 +19,14 @@
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.IActivityManager;
+import android.app.backup.BackupRestoreEventLogger;
 import android.app.backup.IBackupManager;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.hardware.display.ColorDisplayManager;
 import android.icu.util.ULocale;
@@ -31,6 +34,7 @@
 import android.media.RingtoneManager;
 import android.media.Utils;
 import android.net.Uri;
+import android.os.Build;
 import android.os.LocaleList;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -43,11 +47,13 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.LocalePicker;
+import com.android.server.backup.Flags;
 import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager;
 
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 
@@ -67,6 +73,13 @@
     private static final int LONG_PRESS_POWER_FOR_ASSISTANT = 5;
     /** See frameworks/base/core/res/res/values/config.xml#config_keyChordPowerVolumeUp **/
     private static final int KEY_CHORD_POWER_VOLUME_UP_GLOBAL_ACTIONS = 2;
+    @VisibleForTesting
+    static final String HIGH_CONTRAST_TEXT_RESTORED_BROADCAST_ACTION =
+            "com.android.settings.accessibility.ACTION_HIGH_CONTRAST_TEXT_RESTORED";
+
+    // Error messages for logging metrics.
+    private static final String ERROR_REMOTE_EXCEPTION_SETTING_LOCALE_DATA =
+        "remote_exception_setting_locale_data";
 
     private Context mContext;
     private AudioManager mAudioManager;
@@ -88,21 +101,26 @@
      */
     private static final ArraySet<String> sBroadcastOnRestore;
     private static final ArraySet<String> sBroadcastOnRestoreSystemUI;
+    private static final ArraySet<String> sBroadcastOnRestoreAccessibility;
     static {
-        sBroadcastOnRestore = new ArraySet<String>(12);
+        sBroadcastOnRestore = new ArraySet<>(7);
         sBroadcastOnRestore.add(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
         sBroadcastOnRestore.add(Settings.Secure.ENABLED_VR_LISTENERS);
-        sBroadcastOnRestore.add(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
         sBroadcastOnRestore.add(Settings.Global.BLUETOOTH_ON);
         sBroadcastOnRestore.add(Settings.Secure.UI_NIGHT_MODE);
         sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_START_TIME);
         sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_END_TIME);
-        sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
-        sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
-        sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_QS_TARGETS);
-        sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
         sBroadcastOnRestore.add(Settings.Secure.SCREEN_RESOLUTION_MODE);
-        sBroadcastOnRestoreSystemUI = new ArraySet<String>(2);
+
+        sBroadcastOnRestoreAccessibility = new ArraySet<>(5);
+        sBroadcastOnRestoreAccessibility.add(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+        sBroadcastOnRestoreAccessibility.add(
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
+        sBroadcastOnRestoreAccessibility.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
+        sBroadcastOnRestoreAccessibility.add(Settings.Secure.ACCESSIBILITY_QS_TARGETS);
+        sBroadcastOnRestoreAccessibility.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
+
+        sBroadcastOnRestoreSystemUI = new ArraySet<>(2);
         sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_TILES);
         sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_AUTO_ADDED_TILES);
     }
@@ -175,6 +193,7 @@
         String oldValue = null;
         boolean sendBroadcast = false;
         boolean sendBroadcastSystemUI = false;
+        boolean sendBroadcastAccessibility = false;
         final SettingsLookup table;
 
         if (destination.equals(Settings.Secure.CONTENT_URI)) {
@@ -187,6 +206,7 @@
 
         sendBroadcast = sBroadcastOnRestore.contains(name);
         sendBroadcastSystemUI = sBroadcastOnRestoreSystemUI.contains(name);
+        sendBroadcastAccessibility = sBroadcastOnRestoreAccessibility.contains(name);
 
         if (sendBroadcast) {
             // TODO: http://b/22388012
@@ -196,6 +216,10 @@
             // It would probably be correct to do it for the ones sent to the system, but consumers
             // may be depending on the current behavior.
             oldValue = table.lookup(cr, name, context.getUserId());
+        } else if (sendBroadcastAccessibility) {
+            int userId = android.view.accessibility.Flags.restoreA11ySecureSettingsOnHsumDevice()
+                    ? context.getUserId() : UserHandle.USER_SYSTEM;
+            oldValue = table.lookup(cr, name, userId);
         }
 
         try {
@@ -238,14 +262,27 @@
             } else if (Settings.System.ACCELEROMETER_ROTATION.equals(name)
                     && shouldSkipAutoRotateRestore()) {
                 return;
-            } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(name)) {
+            } else if (shouldSkipAndLetBroadcastHandlesRestoreLogic(name)) {
                 // Don't write it to setting. Let the broadcast receiver in
                 // AccessibilityManagerService handle restore/merging logic.
                 return;
-            } else if (Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE.equals(name)) {
-                // Don't write it to setting. Let the broadcast receiver in
-                // AccessibilityManagerService handle restore/merging logic.
-                return;
+            } else if (com.android.graphics.hwui.flags.Flags.highContrastTextSmallTextRect()
+                    && Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED.equals(name)) {
+                final boolean currentlyEnabled = Settings.Secure.getInt(
+                        context.getContentResolver(),
+                        Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, 0) == 1;
+                final boolean enabledInRestore = value != null && Integer.parseInt(value) == 1;
+
+                // If restoring from Android 15 or earlier and the user didn't already enable HCT
+                // on this new device, then don't restore and trigger custom migration logic.
+                final boolean needsCustomMigration = !currentlyEnabled
+                        && restoredFromSdkInt < Build.VERSION_CODES.BAKLAVA
+                        && enabledInRestore;
+                if (needsCustomMigration) {
+                    migrateHighContrastText(context);
+                    return;
+                }
+                // fall through to the ordinary write to settings
             }
 
             // Default case: write the restored value to settings
@@ -257,12 +294,13 @@
             // If we fail to apply the setting, by definition nothing happened
             sendBroadcast = false;
             sendBroadcastSystemUI = false;
+            sendBroadcastAccessibility = false;
             Log.e(TAG, "Failed to restore setting name: " + name + " + value: " + value, e);
         } finally {
             // If this was an element of interest, send the "we just restored it"
             // broadcast with the historical value now that the new value has
             // been committed and observers kicked off.
-            if (sendBroadcast || sendBroadcastSystemUI) {
+            if (sendBroadcast || sendBroadcastSystemUI || sendBroadcastAccessibility) {
                 Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
                         .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
                         .putExtra(Intent.EXTRA_SETTING_NAME, name)
@@ -279,6 +317,13 @@
                             context.getString(com.android.internal.R.string.config_systemUi));
                     context.sendBroadcastAsUser(intent, context.getUser(), null);
                 }
+                if (sendBroadcastAccessibility) {
+                    UserHandle userHandle =
+                            android.view.accessibility.Flags.restoreA11ySecureSettingsOnHsumDevice()
+                                    ? context.getUser() : UserHandle.SYSTEM;
+                    intent.setPackage("android");
+                    context.sendBroadcastAsUser(intent, userHandle, null);
+                }
             }
         }
     }
@@ -444,6 +489,19 @@
         }
     }
 
+    private boolean shouldSkipAndLetBroadcastHandlesRestoreLogic(String settingName) {
+        boolean restoreHandledByBroadcast = Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(
+                settingName)
+                || Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE.equals(settingName);
+        if (android.view.accessibility.Flags.restoreA11ySecureSettingsOnHsumDevice()) {
+            restoreHandledByBroadcast |=
+                    Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS.equals(settingName)
+                            || Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES.equals(settingName);
+        }
+
+        return restoreHandledByBroadcast;
+    }
+
     private void setAutoRestore(boolean enabled) {
         try {
             IBackupManager bm = IBackupManager.Stub.asInterface(
@@ -523,11 +581,40 @@
         }
     }
 
+    private static void migrateHighContrastText(Context context) {
+        final Intent intent = new Intent(HIGH_CONTRAST_TEXT_RESTORED_BROADCAST_ACTION)
+                .setPackage(getSettingsAppPackage(context));
+        context.sendBroadcastAsUser(intent, context.getUser(), null);
+    }
+
+    /**
+     * Returns the System Settings application's package name
+     */
+    private static String getSettingsAppPackage(Context context) {
+        String settingsAppPackage = null;
+        PackageManager packageManager = context.getPackageManager();
+        if (packageManager != null) {
+            List<ResolveInfo> results = packageManager.queryIntentActivities(
+                    new Intent(Settings.ACTION_SETTINGS),
+                    PackageManager.MATCH_SYSTEM_ONLY);
+            if (!results.isEmpty()) {
+                settingsAppPackage = results.getFirst().activityInfo.applicationInfo.packageName;
+            }
+        }
+
+        return !TextUtils.isEmpty(settingsAppPackage) ? settingsAppPackage : "com.android.settings";
+    }
+
     /* package */ byte[] getLocaleData() {
         Configuration conf = mContext.getResources().getConfiguration();
         return conf.getLocales().toLanguageTags().getBytes();
     }
 
+    LocaleList getLocaleList() {
+        Configuration conf = mContext.getResources().getConfiguration();
+        return conf.getLocales();
+    }
+
     private static Locale toFullLocale(@NonNull Locale locale) {
         if (locale.getScript().isEmpty() || locale.getCountry().isEmpty()) {
             return ULocale.addLikelySubtags(ULocale.forLocale(locale)).toLocale();
@@ -653,8 +740,12 @@
      * code and {@code CC} is a two letter country code.
      *
      * @param data the comma separated BCP-47 language tags in bytes.
+     * @param size the size of the data in bytes.
+     * @param backupRestoreEventLogger the logger to log the restore event.
+     * @param dataType the data type of the setting for logging purposes.
      */
-    /* package */ void setLocaleData(byte[] data, int size) {
+    /* package */ void setLocaleData(
+        byte[] data, int size, BackupRestoreEventLogger backupRestoreEventLogger, String dataType) {
         final Configuration conf = mContext.getResources().getConfiguration();
 
         // Replace "_" with "-" to deal with older backups.
@@ -681,8 +772,18 @@
 
             am.updatePersistentConfigurationWithAttribution(config, mContext.getOpPackageName(),
                     mContext.getAttributionTag());
+            if (Flags.enableMetricsSettingsBackupAgents()) {
+                backupRestoreEventLogger
+                    .logItemsRestored(dataType, localeList.size());
+            }
         } catch (RemoteException e) {
-            // Intentionally left blank
+            if (Flags.enableMetricsSettingsBackupAgents()) {
+                backupRestoreEventLogger
+                    .logItemsRestoreFailed(
+                        dataType,
+                        localeList.size(),
+                        ERROR_REMOTE_EXCEPTION_SETTING_LOCALE_DATA);
+            }
         }
     }
 
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 37eda3e..f9c6442 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1778,6 +1778,9 @@
                 Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
                 SecureSettingsProto.Accessibility.HIGH_TEXT_CONTRAST_ENABLED);
         dumpSetting(s, p,
+                Settings.Secure.ACCESSIBILITY_HCT_RECT_PROMPT_STATUS,
+                SecureSettingsProto.Accessibility.HCT_RECT_PROMPT_STATUS);
+        dumpSetting(s, p,
                 Settings.Secure.CONTRAST_LEVEL,
                 SecureSettingsProto.Accessibility.CONTRAST_LEVEL);
         dumpSetting(s, p,
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 6128d45..55f48e3 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -2089,7 +2089,33 @@
                 // setting.
                 return false;
             }
-            final String mimeType = getContext().getContentResolver().getType(audioUri);
+
+            // If the audioUri comes from FileProvider, the security check will fail. Currently, it
+            // should not have too many FileProvider Uri usage, using a workaround fix here.
+            // Only allow for caller is privileged apps
+            ApplicationInfo aInfo = null;
+            try {
+                aInfo = getCallingApplicationInfoOrThrow();
+            } catch (IllegalStateException ignored) {
+                Slog.w(LOG_TAG, "isValidMediaUri: cannot get calling app info for setting: "
+                        + name + " URI: " + audioUri);
+                return false;
+            }
+            final boolean isPrivilegedApp = aInfo != null ? aInfo.isPrivilegedApp() : false;
+            String mimeType = null;
+            if (isPrivilegedApp) {
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    mimeType = getContext().getContentResolver().getType(audioUri);
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            } else {
+                mimeType = getContext().getContentResolver().getType(audioUri);
+            }
+            if (DEBUG) {
+                Slog.v(LOG_TAG, "isValidMediaUri mimeType: " + mimeType);
+            }
             if (mimeType == null) {
                 Slog.e(LOG_TAG,
                         "mutateSystemSetting for setting: " + name + " URI: " + audioUri
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
index 048d93b..62c03dd 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
@@ -26,18 +26,22 @@
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Build;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.provider.SettingsStringUtil;
+import android.view.accessibility.Flags;
 
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
 
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mockito;
 
 import java.util.concurrent.ExecutionException;
 
@@ -48,18 +52,100 @@
  */
 @RunWith(AndroidJUnit4.class)
 public class SettingsHelperRestoreTest {
-
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    public final BroadcastInterceptingContext mInterceptingContext =
+            new BroadcastInterceptingContext(
+                    InstrumentationRegistry.getInstrumentation().getContext());
     private static final float FLOAT_TOLERANCE = 0.01f;
-
-    private Context mContext;
     private ContentResolver mContentResolver;
     private SettingsHelper mSettingsHelper;
 
     @Before
     public void setUp() {
-        mContext = InstrumentationRegistry.getContext();
-        mContentResolver = mContext.getContentResolver();
-        mSettingsHelper = new SettingsHelper(mContext);
+        mContentResolver = mInterceptingContext.getContentResolver();
+        mSettingsHelper = new SettingsHelper(mInterceptingContext);
+    }
+
+    @After
+    public void cleanUp() {
+        setDefaultAccessibilityDisplayMagnificationScale();
+        Settings.Secure.putInt(mContentResolver,
+                Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, 0);
+        Settings.Secure.putString(mContentResolver, Settings.Secure.ACCESSIBILITY_QS_TARGETS, null);
+        Settings.Secure.putString(mContentResolver,
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, null);
+        Settings.Secure.putString(mContentResolver,
+                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, null);
+    }
+
+    @Test
+    public void restoreHighTextContrastEnabled_currentlyEnabled_enableInRestoredFromVanilla_dontSendNotification_hctKeepsEnabled()
+            throws ExecutionException, InterruptedException {
+        BroadcastInterceptingContext.FutureIntent futureIntent =
+                mInterceptingContext.nextBroadcastIntent(
+                        SettingsHelper.HIGH_CONTRAST_TEXT_RESTORED_BROADCAST_ACTION);
+        String settingName = Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED;
+        Settings.Secure.putInt(mContentResolver, settingName, 1);
+
+        mSettingsHelper.restoreValue(
+                mInterceptingContext,
+                mContentResolver,
+                new ContentValues(2),
+                Settings.Secure.getUriFor(settingName),
+                settingName,
+                String.valueOf(1),
+                Build.VERSION_CODES.VANILLA_ICE_CREAM);
+
+        futureIntent.assertNotReceived();
+        assertThat(Settings.Secure.getInt(mContentResolver, settingName, 0)).isEqualTo(1);
+    }
+
+    @EnableFlags(com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+    @Test
+    public void restoreHighTextContrastEnabled_currentlyDisabled_enableInRestoredFromVanilla_sendNotification_hctKeepsDisabled()
+            throws ExecutionException, InterruptedException {
+        BroadcastInterceptingContext.FutureIntent futureIntent =
+                mInterceptingContext.nextBroadcastIntent(
+                        SettingsHelper.HIGH_CONTRAST_TEXT_RESTORED_BROADCAST_ACTION);
+        String settingName = Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED;
+        Settings.Secure.putInt(mContentResolver, settingName, 0);
+
+        mSettingsHelper.restoreValue(
+                mInterceptingContext,
+                mContentResolver,
+                new ContentValues(2),
+                Settings.Secure.getUriFor(settingName),
+                settingName,
+                String.valueOf(1),
+                Build.VERSION_CODES.VANILLA_ICE_CREAM);
+
+        Intent intentReceived = futureIntent.get();
+        assertThat(intentReceived).isNotNull();
+        assertThat(intentReceived.getPackage()).isNotNull();
+        assertThat(Settings.Secure.getInt(mContentResolver, settingName, 0)).isEqualTo(0);
+    }
+
+    @Test
+    public void restoreHighTextContrastEnabled_currentlyDisabled_enableInRestoredFromAfterVanilla_dontSendNotification_hctShouldEnabled()
+            throws ExecutionException, InterruptedException {
+        BroadcastInterceptingContext.FutureIntent futureIntent =
+                mInterceptingContext.nextBroadcastIntent(
+                        SettingsHelper.HIGH_CONTRAST_TEXT_RESTORED_BROADCAST_ACTION);
+        String settingName = Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED;
+        Settings.Secure.putInt(mContentResolver, settingName, 0);
+
+        mSettingsHelper.restoreValue(
+                mInterceptingContext,
+                mContentResolver,
+                new ContentValues(2),
+                Settings.Secure.getUriFor(settingName),
+                settingName,
+                String.valueOf(1),
+                Build.VERSION_CODES.BAKLAVA);
+
+        futureIntent.assertNotReceived();
+        assertThat(Settings.Secure.getInt(mContentResolver, settingName, 0)).isEqualTo(1);
     }
 
     /** Tests for {@link Settings.Secure#ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE}. */
@@ -76,7 +162,7 @@
         Settings.Secure.putFloat(mContentResolver, settingName, configuredSettingValue);
 
         mSettingsHelper.restoreValue(
-                mContext,
+                mInterceptingContext,
                 mContentResolver,
                 new ContentValues(2),
                 Settings.Secure.getUriFor(settingName),
@@ -99,7 +185,7 @@
         float restoreSettingValue = defaultSettingValue + 0.5f;
 
         mSettingsHelper.restoreValue(
-                Mockito.mock(Context.class),
+                mInterceptingContext,
                 mContentResolver,
                 new ContentValues(2),
                 Settings.Secure.getUriFor(settingName),
@@ -121,7 +207,7 @@
      */
     private float setDefaultAccessibilityDisplayMagnificationScale() {
         float defaultSettingValue =
-                mContext.getResources()
+                mInterceptingContext.getResources()
                         .getFraction(
                                 R.fraction.def_accessibility_display_magnification_scale, 1, 1);
         Settings.Secure.putFloat(
@@ -142,7 +228,7 @@
         Settings.Secure.putInt(mContentResolver, settingName, configuredSettingValue);
 
         mSettingsHelper.restoreValue(
-                Mockito.mock(Context.class),
+                mInterceptingContext,
                 mContentResolver,
                 new ContentValues(2),
                 Settings.Secure.getUriFor(settingName),
@@ -164,7 +250,7 @@
 
         int restoreSettingValue = 1;
         mSettingsHelper.restoreValue(
-                Mockito.mock(Context.class),
+                mInterceptingContext,
                 mContentResolver,
                 new ContentValues(2),
                 Settings.Secure.getUriFor(settingName),
@@ -178,17 +264,15 @@
     @Test
     public void restoreAccessibilityQsTargets_broadcastSent()
             throws ExecutionException, InterruptedException {
-        BroadcastInterceptingContext interceptingContext = new BroadcastInterceptingContext(
-                mContext);
         final String settingName = Settings.Secure.ACCESSIBILITY_QS_TARGETS;
         final String restoreSettingValue = "com.android.server.accessibility/ColorInversion"
                 + SettingsStringUtil.DELIMITER
                 + "com.android.server.accessibility/ColorCorrectionTile";
         BroadcastInterceptingContext.FutureIntent futureIntent =
-                interceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED);
+                mInterceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED);
 
         mSettingsHelper.restoreValue(
-                interceptingContext,
+                mInterceptingContext,
                 mContentResolver,
                 new ContentValues(2),
                 Settings.Secure.getUriFor(settingName),
@@ -207,15 +291,13 @@
     @Test
     public void restoreAccessibilityShortcutTargetService_broadcastSent()
             throws ExecutionException, InterruptedException {
-        BroadcastInterceptingContext interceptingContext = new BroadcastInterceptingContext(
-                mContext);
         final String settingName = Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
         final String restoredValue = "com.android.a11y/Service";
         BroadcastInterceptingContext.FutureIntent futureIntent =
-                interceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED);
+                mInterceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED);
 
         mSettingsHelper.restoreValue(
-                interceptingContext,
+                mInterceptingContext,
                 mContentResolver,
                 new ContentValues(2),
                 Settings.Secure.getUriFor(settingName),
@@ -230,4 +312,32 @@
                 Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, /* defaultValue= */ 0))
                 .isEqualTo(Build.VERSION.SDK_INT);
     }
+
+    @EnableFlags(Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    @Test
+    public void restoreAccessibilityShortcutTargets_broadcastSent()
+            throws ExecutionException, InterruptedException {
+        final String settingName = Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS;
+        final String restoreSettingValue = "com.android.server.accessibility/ColorInversion"
+                + SettingsStringUtil.DELIMITER
+                + "com.android.server.accessibility/ColorCorrectionTile";
+        BroadcastInterceptingContext.FutureIntent futureIntent =
+                mInterceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED);
+
+        mSettingsHelper.restoreValue(
+                mInterceptingContext,
+                mContentResolver,
+                new ContentValues(2),
+                Settings.Secure.getUriFor(settingName),
+                settingName,
+                restoreSettingValue,
+                Build.VERSION.SDK_INT);
+
+        Intent intentReceived = futureIntent.get();
+        assertThat(intentReceived.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE))
+                .isEqualTo(restoreSettingValue);
+        assertThat(intentReceived.getIntExtra(
+                Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, /* defaultValue= */ 0))
+                .isEqualTo(Build.VERSION.SDK_INT);
+    }
 }
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
index cea2bbc..58200d4 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
@@ -33,6 +33,7 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.res.AssetFileDescriptor;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.database.MatrixCursor;
@@ -83,6 +84,9 @@
             "content://media/internal/audio/media/30?title=DefaultAlarm&canonical=1";
     private static final String VIBRATION_FILE_NAME = "haptics.xml";
 
+    private static final LocaleList LOCALE_LIST =
+        LocaleList.forLanguageTags("en-US,en-UK");
+
     private SettingsHelper mSettingsHelper;
 
     @Rule
@@ -778,6 +782,15 @@
         assertThat(getAutoRotationSettingValue()).isEqualTo(previousValue);
     }
 
+    @Test
+    public void getLocaleList_returnsLocaleList() {
+        Configuration config = new Configuration();
+        config.setLocales(LOCALE_LIST);
+        when(mResources.getConfiguration()).thenReturn(config);
+
+        assertThat(mSettingsHelper.getLocaleList()).isEqualTo(LOCALE_LIST);
+    }
+
     private int getAutoRotationSettingValue() {
         return Settings.System.getInt(mContentResolver,
                 Settings.System.ACCELEROMETER_ROTATION,
diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig
index e3f5378..9692aa5 100644
--- a/packages/SystemUI/aconfig/biometrics_framework.aconfig
+++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig
@@ -4,16 +4,6 @@
 # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
 
 flag {
-  name: "bp_icon_a11y"
-  namespace: "biometrics_framework"
-  description: "Fixes biometric prompt icon not working as button with a11y"
-  bug: "359423579"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
     name: "cont_auth_plugin"
     namespace: "biometrics_framework"
     description: "Plugin and related API hooks for contextual auth plugins"
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 777f6d3..74c8710 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -265,14 +265,6 @@
 }
 
 flag {
-    name: "keyguard_bottom_area_refactor"
-    namespace: "systemui"
-    description: "Bottom area of keyguard refactor move into KeyguardRootView. Includes "
-        "lock icon and others."
-    bug: "290652751"
-}
-
-flag {
     name: "device_entry_udfps_refactor"
     namespace: "systemui"
     description: "Refactoring device entry UDFPS icon to use modern architecture and "
@@ -1871,6 +1863,20 @@
 }
 
 flag {
+    name: "desktop_effects_qs_tile"
+    namespace: "systemui"
+    description: "Enables the QS tile for desktop effects"
+    bug: "376797327"
+}
+
+flag {
+   name: "hub_edit_mode_touch_adjustments"
+   namespace: "systemui"
+   description: "Makes selected widget toggleable in edit mode and modifier buttons mutually exclusive."
+   bug: "383160667"
+}
+
+flag {
    name: "glanceable_hub_direct_edit_mode"
    namespace: "systemui"
    description: "Invokes edit mode directly from long press in glanceable hub"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 315dc34..5dbedc7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -296,9 +296,19 @@
                                     offset.y,
                                 ) - contentOffset
                             val index = firstIndexAtOffset(gridState, adjustedOffset)
-                            val key =
+                            val tappedKey =
                                 index?.let { keyAtIndexIfEditable(contentListState.list, index) }
-                            viewModel.setSelectedKey(key)
+
+                            viewModel.setSelectedKey(
+                                if (
+                                    Flags.hubEditModeTouchAdjustments() &&
+                                        selectedKey.value == tappedKey
+                                ) {
+                                    null
+                                } else {
+                                    tappedKey
+                                }
+                            )
                         }
                     }
                 }
@@ -1010,7 +1020,7 @@
                 text = titleForEmptyStateCTA,
                 style = MaterialTheme.typography.displaySmall,
                 textAlign = TextAlign.Center,
-                color = colors.primary,
+                color = colors.onPrimary,
                 modifier =
                     Modifier.focusable().semantics(mergeDescendants = true) {
                         contentDescription = titleForEmptyStateCTA
@@ -1080,17 +1090,27 @@
                 .onSizeChanged { setToolbarSize(it) }
     ) {
         val addWidgetText = stringResource(R.string.hub_mode_add_widget_button_text)
-        ToolbarButton(
-            isPrimary = !removeEnabled,
-            modifier = Modifier.align(Alignment.CenterStart),
-            onClick = onOpenWidgetPicker,
-        ) {
-            Icon(Icons.Default.Add, null)
-            Text(text = addWidgetText)
+
+        if (!(Flags.hubEditModeTouchAdjustments() && removeEnabled)) {
+            ToolbarButton(
+                isPrimary = !removeEnabled,
+                modifier = Modifier.align(Alignment.CenterStart),
+                onClick = onOpenWidgetPicker,
+            ) {
+                Icon(Icons.Default.Add, null)
+                Text(text = addWidgetText)
+            }
         }
 
         AnimatedVisibility(
-            modifier = Modifier.align(Alignment.Center),
+            modifier =
+                Modifier.align(
+                    if (Flags.hubEditModeTouchAdjustments()) {
+                        Alignment.CenterStart
+                    } else {
+                        Alignment.Center
+                    }
+                ),
             visible = removeEnabled,
             enter = fadeIn(),
             exit = fadeOut(),
@@ -1113,7 +1133,11 @@
                     horizontalArrangement =
                         Arrangement.spacedBy(
                             ButtonDefaults.IconSpacing,
-                            Alignment.CenterHorizontally,
+                            if (Flags.hubEditModeTouchAdjustments()) {
+                                Alignment.Start
+                            } else {
+                                Alignment.CenterHorizontally
+                            },
                         ),
                     verticalAlignment = Alignment.CenterVertically,
                 ) {
@@ -1374,6 +1398,7 @@
     val shrinkWidgetLabel = stringResource(R.string.accessibility_action_label_shrink_widget)
     val expandWidgetLabel = stringResource(R.string.accessibility_action_label_expand_widget)
 
+    val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false)
     val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle()
     val selectedIndex =
         selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
@@ -1487,7 +1512,8 @@
     ) {
         with(widgetSection) {
             Widget(
-                viewModel = viewModel,
+                isFocusable = isFocusable,
+                openWidgetEditor = { viewModel.onOpenWidgetEditor() },
                 model = model,
                 size = size,
                 modifier = Modifier.fillMaxSize().allowGestures(allowed = !viewModel.isEditMode),
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 105e8da..7956d02 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
@@ -159,6 +159,7 @@
                     with(lockSection) { LockIcon() }
 
                     // Aligned to bottom and constrained to below the lock icon.
+                    // TODO("b/383588832") change this away from "keyguard_bottom_area"
                     Column(modifier = Modifier.fillMaxWidth().sysuiResTag("keyguard_bottom_area")) {
                         if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
                             with(ambientIndicationSectionOptional.get()) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index e78862e..5c7ca97 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -29,11 +29,8 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -52,7 +49,6 @@
 import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack
 import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace
 import com.android.systemui.res.R
-import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
@@ -179,16 +175,13 @@
             return
         }
 
-        val splitShadeTopMargin: Dp =
-            LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp
-
         ConstrainedNotificationStack(
             stackScrollView = stackScrollView.get(),
             viewModel = rememberViewModel("Notifications") { viewModelFactory.create() },
             modifier =
                 modifier
                     .fillMaxWidth()
-                    .thenIf(isShadeLayoutWide) { Modifier.padding(top = splitShadeTopMargin) }
+                    .thenIf(isShadeLayoutWide) { Modifier.padding(top = 12.dp) }
                     .let {
                         if (burnInParams == null) {
                             it
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index bb61a13..9744424 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -23,7 +23,7 @@
 import androidx.compose.ui.unit.round
 import androidx.compose.ui.util.fastCoerceIn
 import com.android.compose.animation.scene.content.Content
-import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
+import com.android.compose.animation.scene.content.state.TransitionState.DirectionProperties.Companion.DistanceUnspecified
 import com.android.compose.nestedscroll.OnStopScope
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
 import com.android.compose.nestedscroll.ScrollController
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index e819bfd..07a19d8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -1257,7 +1257,7 @@
     }
 
     val currentContent = currentContentState.content
-    if (transition is TransitionState.HasOverscrollProperties) {
+    if (transition is TransitionState.DirectionProperties) {
         val overscroll = transition.currentOverscrollSpec
         if (overscroll?.content == currentContent) {
             val elementSpec =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 86c5fd8..e8b2b09 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -382,7 +382,7 @@
         // Compute the [TransformationSpec] when the transition starts.
         val fromContent = transition.fromContent
         val toContent = transition.toContent
-        val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation
+        val orientation = (transition as? TransitionState.DirectionProperties)?.orientation
 
         // Update the transition specs.
         transition.transformationSpec =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index 59d0b55..5aaeda8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -25,7 +25,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import com.android.compose.animation.scene.content.state.TransitionState
-import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
+import com.android.compose.animation.scene.content.state.TransitionState.DirectionProperties.Companion.DistanceUnspecified
 import kotlin.math.absoluteValue
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.launch
@@ -197,7 +197,7 @@
     private val distance: (SwipeAnimation<T>) -> Float,
     currentContent: T = fromContent,
     dragOffset: Float = 0f,
-) : TransitionState.HasOverscrollProperties {
+) : TransitionState.DirectionProperties {
     /** The [TransitionState.Transition] whose implementation delegates to this [SwipeAnimation]. */
     lateinit var contentTransition: TransitionState.Transition
 
@@ -513,7 +513,7 @@
         swipeAnimation.toContent,
         replacedTransition,
     ),
-    TransitionState.HasOverscrollProperties by swipeAnimation {
+    TransitionState.DirectionProperties by swipeAnimation {
 
     constructor(
         other: ChangeSceneSwipeTransition
@@ -575,7 +575,7 @@
         swipeAnimation.toContent,
         replacedTransition,
     ),
-    TransitionState.HasOverscrollProperties by swipeAnimation {
+    TransitionState.DirectionProperties by swipeAnimation {
     constructor(
         other: ShowOrHideOverlaySwipeTransition
     ) : this(
@@ -634,7 +634,7 @@
         swipeAnimation.toContent,
         replacedTransition,
     ),
-    TransitionState.HasOverscrollProperties by swipeAnimation {
+    TransitionState.DirectionProperties by swipeAnimation {
     constructor(
         other: ReplaceOverlaySwipeTransition
     ) : this(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index d66fe42..29be445 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -273,7 +273,7 @@
          * every time progress is changed.
          */
         private val _currentOverscrollSpec: State<OverscrollSpecImpl?>? =
-            if (this !is HasOverscrollProperties) {
+            if (this !is DirectionProperties) {
                 null
             } else {
                 derivedStateOf {
@@ -406,7 +406,7 @@
         /** Returns if the [progress] value of this transition can go beyond range `[0; 1]` */
         internal fun isWithinProgressRange(progress: Float): Boolean {
             // If the properties are missing we assume that every [Transition] can overscroll
-            if (this !is HasOverscrollProperties) return true
+            if (this !is DirectionProperties) return true
             // [OverscrollSpec] for the current scene, even if it hasn't started overscrolling yet.
             val specForCurrentScene =
                 when {
@@ -444,7 +444,7 @@
         }
     }
 
-    interface HasOverscrollProperties {
+    interface DirectionProperties {
         /**
          * The position of the [Transition.toContent].
          *
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
index bfb5ca7..944bd85 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt
@@ -157,7 +157,7 @@
         val idleSize = checkNotNull(element.targetSize(content))
         val userActionDistance = idleSize.height
         val progress =
-            when ((transition as? TransitionState.HasOverscrollProperties)?.bouncingContent) {
+            when ((transition as? TransitionState.DirectionProperties)?.bouncingContent) {
                 null -> transition.progressTo(content)
                 content -> 1f
                 else -> 0f
@@ -256,7 +256,7 @@
 
     private fun targetAlpha(transition: TransitionState.Transition, content: ContentKey): Float {
         if (transition.isUserInputOngoing) {
-            if (transition !is TransitionState.HasOverscrollProperties) {
+            if (transition !is TransitionState.DirectionProperties) {
                 error(
                     "Unsupported transition driven by user input but that does not have " +
                         "overscroll properties: $transition"
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index 2f4d5bff..432add3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -60,7 +60,7 @@
         // As this object is created by OverscrollBuilderImpl and we retrieve the current
         // OverscrollSpec only when the transition implements HasOverscrollProperties, we can assume
         // that this method was invoked after performing this check.
-        val overscrollProperties = transition as TransitionState.HasOverscrollProperties
+        val overscrollProperties = transition as TransitionState.DirectionProperties
         val overscrollScope =
             cachedOverscrollScope.getFromCacheOrCompute(density = this, overscrollProperties)
 
@@ -77,17 +77,17 @@
 
 /**
  * A helper class to cache a [OverscrollScope] given a [Density] and
- * [TransitionState.HasOverscrollProperties]. This helps avoid recreating a scope every frame
- * whenever an overscroll transition is computed.
+ * [TransitionState.DirectionProperties]. This helps avoid recreating a scope every frame whenever
+ * an overscroll transition is computed.
  */
 private class CachedOverscrollScope {
     private var previousScope: OverscrollScope? = null
     private var previousDensity: Density? = null
-    private var previousOverscrollProperties: TransitionState.HasOverscrollProperties? = null
+    private var previousOverscrollProperties: TransitionState.DirectionProperties? = null
 
     fun getFromCacheOrCompute(
         density: Density,
-        overscrollProperties: TransitionState.HasOverscrollProperties,
+        overscrollProperties: TransitionState.DirectionProperties,
     ): OverscrollScope {
         if (
             previousScope == null ||
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 1959f59..ffba639 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -2853,7 +2853,7 @@
         // Start an overscrollable transition driven by progress.
         var progress by mutableFloatStateOf(0f)
         val transition = transition(from = SceneA, to = SceneB, progress = { progress })
-        assertThat(transition).isInstanceOf(TransitionState.HasOverscrollProperties::class.java)
+        assertThat(transition).isInstanceOf(TransitionState.DirectionProperties::class.java)
         scope.launch { state.startTransition(transition) }
 
         // Reset the counters after the first animation frame.
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
index 0adb480..9a2af64 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
@@ -168,12 +168,12 @@
 
     fun hasBouncingContent(content: ContentKey) {
         val actual = actual
-        if (actual !is TransitionState.HasOverscrollProperties) {
+        if (actual !is TransitionState.DirectionProperties) {
             failWithActual(simpleFact("expected to be ContentState.HasOverscrollProperties"))
         }
 
         check("bouncingContent")
-            .that((actual as TransitionState.HasOverscrollProperties).bouncingContent)
+            .that((actual as TransitionState.DirectionProperties).bouncingContent)
             .isEqualTo(content)
     }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt
index 646cff8..6015479 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestOverlayTransition.kt
@@ -71,7 +71,7 @@
 ): TestOverlayTransition {
     return object :
         TestOverlayTransition(fromScene, overlay, replacedTransition),
-        TransitionState.HasOverscrollProperties {
+        TransitionState.DirectionProperties {
         override val isEffectivelyShown: Boolean
             get() = isEffectivelyShown()
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt
index c342f48..bd2118d 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestReplaceOverlayTransition.kt
@@ -68,7 +68,7 @@
 ): TestReplaceOverlayTransition {
     return object :
         TestReplaceOverlayTransition(from, to, replacedTransition),
-        TransitionState.HasOverscrollProperties {
+        TransitionState.DirectionProperties {
         override val effectivelyShownOverlay: OverlayKey
             get() = effectivelyShownOverlay()
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt
index d24b895..1d27e3a 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestSceneTransition.kt
@@ -62,7 +62,7 @@
     replacedTransition: Transition? = null,
 ): TestSceneTransition {
     return object :
-        TestSceneTransition(from, to, replacedTransition), TransitionState.HasOverscrollProperties {
+        TestSceneTransition(from, to, replacedTransition), TransitionState.DirectionProperties {
         override val currentScene: SceneKey
             get() = current()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
deleted file mode 100644
index ce57fe2..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.keyguard;
-
-import static android.view.View.INVISIBLE;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.res.Resources;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
-import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.plugins.clocks.ClockAnimations;
-import com.android.systemui.plugins.clocks.ClockController;
-import com.android.systemui.plugins.clocks.ClockEvents;
-import com.android.systemui.plugins.clocks.ClockFaceConfig;
-import com.android.systemui.plugins.clocks.ClockFaceController;
-import com.android.systemui.plugins.clocks.ClockFaceEvents;
-import com.android.systemui.plugins.clocks.ClockTickRate;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.res.R;
-import com.android.systemui.shared.clocks.AnimatableClockView;
-import com.android.systemui.shared.clocks.ClockRegistry;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder;
-import com.android.systemui.statusbar.phone.NotificationIconContainer;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.settings.SecureSettings;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.Before;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-public class KeyguardClockSwitchControllerBaseTest extends SysuiTestCase {
-
-    @Mock
-    protected KeyguardClockSwitch mView;
-    @Mock
-    protected StatusBarStateController mStatusBarStateController;
-    @Mock
-    protected ClockRegistry mClockRegistry;
-    @Mock
-    KeyguardSliceViewController mKeyguardSliceViewController;
-    @Mock
-    LockscreenSmartspaceController mSmartspaceController;
-
-    @Mock
-    Resources mResources;
-    @Mock
-    KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
-    @Mock
-    protected ClockController mClockController;
-    @Mock
-    protected ClockFaceController mLargeClockController;
-    @Mock
-    protected ClockFaceController mSmallClockController;
-    @Mock
-    protected ClockAnimations mClockAnimations;
-    @Mock
-    protected ClockEvents mClockEvents;
-    @Mock
-    protected ClockFaceEvents mClockFaceEvents;
-    @Mock
-    DumpManager mDumpManager;
-    @Mock
-    ClockEventController mClockEventController;
-
-    @Mock
-    protected NotificationIconContainer mNotificationIcons;
-    @Mock
-    protected AnimatableClockView mSmallClockView;
-    @Mock
-    protected AnimatableClockView mLargeClockView;
-    @Mock
-    protected FrameLayout mSmallClockFrame;
-    @Mock
-    protected FrameLayout mLargeClockFrame;
-    @Mock
-    protected SecureSettings mSecureSettings;
-    @Mock
-    protected LogBuffer mLogBuffer;
-
-    @Mock
-    protected KeyguardClockInteractor mKeyguardClockInteractor;
-
-    protected final View mFakeDateView = (View) (new ViewGroup(mContext) {
-        @Override
-        protected void onLayout(boolean changed, int l, int t, int r, int b) {}
-    });
-    protected final View mFakeWeatherView = new View(mContext);
-    protected final View mFakeSmartspaceView = new View(mContext);
-
-    protected KeyguardClockSwitchController mController;
-    protected View mSliceView;
-    protected LinearLayout mStatusArea;
-    protected FakeExecutor mExecutor;
-    protected FakeFeatureFlags mFakeFeatureFlags;
-    @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
-            ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-
-        mFakeDateView.setTag(R.id.tag_smartspace_view, new Object());
-        mFakeWeatherView.setTag(R.id.tag_smartspace_view, new Object());
-        mFakeSmartspaceView.setTag(R.id.tag_smartspace_view, new Object());
-
-        when(mView.findViewById(R.id.left_aligned_notification_icon_container))
-                .thenReturn(mNotificationIcons);
-        when(mNotificationIcons.getLayoutParams()).thenReturn(
-                mock(RelativeLayout.LayoutParams.class));
-        when(mView.getContext()).thenReturn(getContext());
-        when(mView.getResources()).thenReturn(mResources);
-        when(mResources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin))
-                .thenReturn(100);
-        when(mResources.getDimensionPixelSize(com.android.systemui.customization.R.dimen.keyguard_large_clock_top_margin))
-                .thenReturn(-200);
-        when(mResources.getInteger(com.android.internal.R.integer.config_doublelineClockDefault))
-                .thenReturn(1);
-        when(mResources.getInteger(R.integer.keyguard_date_weather_view_invisibility))
-                .thenReturn(INVISIBLE);
-
-        when(mView
-                .findViewById(com.android.systemui.customization.R.id.lockscreen_clock_view_large))
-                .thenReturn(mLargeClockFrame);
-        when(mView
-                .findViewById(com.android.systemui.customization.R.id.lockscreen_clock_view))
-                .thenReturn(mSmallClockFrame);
-        when(mSmallClockView.getContext()).thenReturn(getContext());
-        when(mLargeClockView.getContext()).thenReturn(getContext());
-
-        when(mView.isAttachedToWindow()).thenReturn(true);
-        when(mSmartspaceController.buildAndConnectDateView(any())).thenReturn(mFakeDateView);
-        when(mSmartspaceController.buildAndConnectWeatherView(any())).thenReturn(mFakeWeatherView);
-        when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
-        mExecutor = new FakeExecutor(new FakeSystemClock());
-        mFakeFeatureFlags = new FakeFeatureFlags();
-        mController = new KeyguardClockSwitchController(
-                mView,
-                mStatusBarStateController,
-                mClockRegistry,
-                mKeyguardSliceViewController,
-                mSmartspaceController,
-                mock(NotificationIconContainerAlwaysOnDisplayViewBinder.class),
-                mKeyguardUnlockAnimationController,
-                mSecureSettings,
-                mExecutor,
-                mExecutor,
-                mDumpManager,
-                mClockEventController,
-                mLogBuffer,
-                KeyguardInteractorFactory.create(mFakeFeatureFlags).getKeyguardInteractor(),
-                mKeyguardClockInteractor,
-                mock(InWindowLauncherUnlockAnimationManager.class)
-        );
-
-        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
-        when(mLargeClockController.getView()).thenReturn(mLargeClockView);
-        when(mSmallClockController.getView()).thenReturn(mSmallClockView);
-        when(mClockController.getLargeClock()).thenReturn(mLargeClockController);
-        when(mClockController.getSmallClock()).thenReturn(mSmallClockController);
-        when(mClockController.getEvents()).thenReturn(mClockEvents);
-        when(mSmallClockController.getEvents()).thenReturn(mClockFaceEvents);
-        when(mLargeClockController.getEvents()).thenReturn(mClockFaceEvents);
-        when(mLargeClockController.getAnimations()).thenReturn(mClockAnimations);
-        when(mSmallClockController.getAnimations()).thenReturn(mClockAnimations);
-        when(mClockRegistry.createCurrentClock()).thenReturn(mClockController);
-        when(mClockEventController.getClock()).thenReturn(mClockController);
-        when(mSmallClockController.getConfig())
-                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false, false));
-        when(mLargeClockController.getConfig())
-                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, false, false, false));
-
-        mSliceView = new View(getContext());
-        when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView);
-        mStatusArea = new LinearLayout(getContext());
-        when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(mStatusArea);
-    }
-
-    private void removeView(View v) {
-        ViewGroup group = ((ViewGroup) v.getParent());
-        if (group != null) {
-            group.removeView(v);
-        }
-    }
-
-    protected void init() {
-        mController.init();
-
-        verify(mView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture());
-        mAttachCaptor.getValue().onViewAttachedToWindow(mView);
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
deleted file mode 100644
index 892375d..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Copyright (C) 2020 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.keyguard;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.database.ContentObserver;
-import android.os.UserHandle;
-import android.platform.test.annotations.DisableFlags;
-import android.provider.Settings;
-import android.view.View;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.Flags;
-import com.android.systemui.plugins.clocks.ClockFaceConfig;
-import com.android.systemui.plugins.clocks.ClockTickRate;
-import com.android.systemui.shared.clocks.ClockRegistry;
-import com.android.systemui.statusbar.StatusBarState;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.verification.VerificationMode;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchControllerBaseTest {
-    @Test
-    public void testInit_viewAlreadyAttached() {
-        mController.init();
-
-        verifyAttachment(times(1));
-    }
-
-    @Test
-    public void testInit_viewNotYetAttached() {
-        ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor =
-                ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
-
-        when(mView.isAttachedToWindow()).thenReturn(false);
-        mController.init();
-        verify(mView).addOnAttachStateChangeListener(listenerArgumentCaptor.capture());
-
-        verifyAttachment(never());
-
-        listenerArgumentCaptor.getValue().onViewAttachedToWindow(mView);
-
-        verifyAttachment(times(1));
-    }
-
-    @Test
-    public void testInitSubControllers() {
-        mController.init();
-        verify(mKeyguardSliceViewController).init();
-    }
-
-    @Test
-    public void testInit_viewDetached() {
-        ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor =
-                ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
-        mController.init();
-        verify(mView).addOnAttachStateChangeListener(listenerArgumentCaptor.capture());
-
-        verifyAttachment(times(1));
-
-        listenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView);
-        verify(mClockEventController).unregisterListeners();
-    }
-
-    @Test
-    public void testPluginPassesStatusBarState() {
-        ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor =
-                ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class);
-
-        mController.init();
-        verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture());
-
-        listenerArgumentCaptor.getValue().onCurrentClockChanged();
-        verify(mView, times(2)).setClock(mClockController, StatusBarState.SHADE);
-        verify(mClockEventController, times(2)).setClock(mClockController);
-    }
-
-    @Test
-    public void testSmartspaceEnabledRemovesKeyguardStatusArea() {
-        when(mSmartspaceController.isEnabled()).thenReturn(true);
-        mController.init();
-
-        assertEquals(View.GONE, mSliceView.getVisibility());
-    }
-
-    @Test
-    public void onLocaleListChangedRebuildsSmartspaceView() {
-        when(mSmartspaceController.isEnabled()).thenReturn(true);
-        mController.init();
-
-        mController.onLocaleListChanged();
-        // Should be called once on initial setup, then once again for locale change
-        verify(mSmartspaceController, times(2)).buildAndConnectView(mView);
-    }
-
-    @Test
-    public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() {
-        when(mSmartspaceController.isEnabled()).thenReturn(true);
-        when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true);
-        mController.init();
-
-        mController.onLocaleListChanged();
-        // Should be called once on initial setup, then once again for locale change
-        verify(mSmartspaceController, times(2)).buildAndConnectDateView(mView);
-        verify(mSmartspaceController, times(2)).buildAndConnectWeatherView(mView);
-        verify(mSmartspaceController, times(2)).buildAndConnectView(mView);
-    }
-
-    @Test
-    public void testSmartspaceDisabledShowsKeyguardStatusArea() {
-        when(mSmartspaceController.isEnabled()).thenReturn(false);
-        mController.init();
-
-        assertEquals(View.VISIBLE, mSliceView.getVisibility());
-    }
-
-    @Test
-    public void testRefresh() {
-        mController.refresh();
-
-        verify(mSmartspaceController).requestSmartspaceUpdate();
-    }
-
-    @Test
-    public void testChangeToDoubleLineClockSetsSmallClock() {
-        when(mSecureSettings.getIntForUser(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1,
-                UserHandle.USER_CURRENT))
-                .thenReturn(0);
-        ArgumentCaptor<ContentObserver> observerCaptor =
-                ArgumentCaptor.forClass(ContentObserver.class);
-        mController.init();
-        mExecutor.runAllReady();
-        verify(mSecureSettings).registerContentObserverForUserSync(
-                eq(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
-                    anyBoolean(), observerCaptor.capture(), eq(UserHandle.USER_ALL));
-        ContentObserver observer = observerCaptor.getValue();
-        mExecutor.runAllReady();
-
-        // When a settings change has occurred to the small clock, make sure the view is adjusted
-        reset(mView);
-        when(mView.getResources()).thenReturn(mResources);
-        observer.onChange(true);
-        mExecutor.runAllReady();
-        verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true);
-    }
-
-    @Test
-    public void testGetClock_ForwardsToClock() {
-        assertEquals(mClockController, mController.getClock());
-    }
-
-    @Test
-    public void testGetLargeClockBottom_returnsExpectedValue() {
-        when(mLargeClockFrame.getVisibility()).thenReturn(View.VISIBLE);
-        when(mLargeClockFrame.getHeight()).thenReturn(100);
-        when(mSmallClockFrame.getHeight()).thenReturn(50);
-        when(mLargeClockView.getHeight()).thenReturn(40);
-        when(mSmallClockView.getHeight()).thenReturn(20);
-        mController.init();
-
-        assertEquals(170, mController.getClockBottom(1000));
-    }
-
-    @Test
-    public void testGetSmallLargeClockBottom_returnsExpectedValue() {
-        when(mLargeClockFrame.getVisibility()).thenReturn(View.GONE);
-        when(mLargeClockFrame.getHeight()).thenReturn(100);
-        when(mSmallClockFrame.getHeight()).thenReturn(50);
-        when(mLargeClockView.getHeight()).thenReturn(40);
-        when(mSmallClockView.getHeight()).thenReturn(20);
-        mController.init();
-
-        assertEquals(1120, mController.getClockBottom(1000));
-    }
-
-    @Test
-    public void testGetClockBottom_nullClock_returnsZero() {
-        when(mClockEventController.getClock()).thenReturn(null);
-        assertEquals(0, mController.getClockBottom(10));
-    }
-
-    @Test
-    public void testChangeLockscreenWeatherEnabledSetsWeatherViewVisible() {
-        when(mSmartspaceController.isWeatherEnabled()).thenReturn(true);
-        ArgumentCaptor<ContentObserver> observerCaptor =
-                ArgumentCaptor.forClass(ContentObserver.class);
-        mController.init();
-        mExecutor.runAllReady();
-        verify(mSecureSettings).registerContentObserverForUserSync(
-                eq(Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED), anyBoolean(),
-                    observerCaptor.capture(), eq(UserHandle.USER_ALL));
-        ContentObserver observer = observerCaptor.getValue();
-        mExecutor.runAllReady();
-        // When a settings change has occurred, check that view is visible.
-        observer.onChange(true);
-        mExecutor.runAllReady();
-        assertEquals(View.VISIBLE, mFakeWeatherView.getVisibility());
-    }
-
-    @Test
-    public void testChangeClockDateWeatherEnabled_SetsDateWeatherViewVisibility() {
-        ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor =
-                ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class);
-        when(mSmartspaceController.isEnabled()).thenReturn(true);
-        when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true);
-        when(mSmartspaceController.isWeatherEnabled()).thenReturn(true);
-        mController.init();
-        mExecutor.runAllReady();
-        assertEquals(View.VISIBLE, mFakeDateView.getVisibility());
-
-        when(mSmallClockController.getConfig())
-                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false, true));
-        when(mLargeClockController.getConfig())
-                .thenReturn(new ClockFaceConfig(ClockTickRate.PER_MINUTE, true, false, true));
-        verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture());
-        listenerArgumentCaptor.getValue().onCurrentClockChanged();
-
-        mExecutor.runAllReady();
-        assertEquals(View.INVISIBLE, mFakeDateView.getVisibility());
-    }
-
-    @Test
-    public void testGetClock_nullClock_returnsNull() {
-        when(mClockEventController.getClock()).thenReturn(null);
-        assertNull(mController.getClock());
-    }
-
-    private void verifyAttachment(VerificationMode times) {
-        verify(mClockRegistry, times).registerClockChangeListener(
-                any(ClockRegistry.ClockChangeListener.class));
-        verify(mClockEventController, times).registerListeners(mView);
-    }
-
-    @Test
-    public void testSplitShadeEnabledSetToSmartspaceController() {
-        mController.setSplitShadeEnabled(true);
-        verify(mSmartspaceController, times(1)).setSplitShadeEnabled(true);
-        verify(mSmartspaceController, times(0)).setSplitShadeEnabled(false);
-    }
-
-    @Test
-    public void testSplitShadeDisabledSetToSmartspaceController() {
-        mController.setSplitShadeEnabled(false);
-        verify(mSmartspaceController, times(1)).setSplitShadeEnabled(false);
-        verify(mSmartspaceController, times(0)).setSplitShadeEnabled(true);
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchTest.java
deleted file mode 100644
index 4ed5fd0..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright (C) 2018 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.keyguard;
-
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-
-import static com.android.keyguard.KeyguardClockSwitch.LARGE;
-import static com.android.keyguard.KeyguardClockSwitch.SMALL;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static junit.framework.TestCase.assertEquals;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.platform.test.annotations.DisableFlags;
-import android.testing.TestableLooper.RunWithLooper;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.Flags;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.plugins.clocks.ClockController;
-import com.android.systemui.plugins.clocks.ClockFaceController;
-import com.android.systemui.res.R;
-import com.android.systemui.statusbar.StatusBarState;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-// Need to run on the main thread because KeyguardSliceView$Row init checks for
-// the main thread before acquiring a wake lock. This class is constructed when
-// the keyguard_clock_switch layout is inflated.
-@RunWithLooper(setAsMainLooper = true)
-@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-public class KeyguardClockSwitchTest extends SysuiTestCase {
-    @Mock
-    ViewGroup mMockKeyguardSliceView;
-
-    @Mock
-    ClockController mClock;
-
-    @Mock
-    ClockFaceController mSmallClock;
-
-    @Mock
-    ClockFaceController mLargeClock;
-
-    private FrameLayout mSmallClockFrame;
-    private FrameLayout mLargeClockFrame;
-    private KeyguardStatusAreaView mStatusArea;
-
-    KeyguardClockSwitch mKeyguardClockSwitch;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        when(mMockKeyguardSliceView.getContext()).thenReturn(mContext);
-        when(mMockKeyguardSliceView.findViewById(R.id.keyguard_status_area))
-                .thenReturn(mMockKeyguardSliceView);
-
-        when(mClock.getSmallClock()).thenReturn(mSmallClock);
-        when(mClock.getLargeClock()).thenReturn(mLargeClock);
-
-        when(mSmallClock.getView()).thenReturn(new TextView(getContext()));
-        when(mLargeClock.getView()).thenReturn(new TextView(getContext()));
-
-        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
-        layoutInflater.setPrivateFactory(new LayoutInflater.Factory2() {
-
-            @Override
-            public View onCreateView(View parent, String name, Context context,
-                    AttributeSet attrs) {
-                return onCreateView(name, context, attrs);
-            }
-
-            @Override
-            public View onCreateView(String name, Context context, AttributeSet attrs) {
-                if ("com.android.keyguard.KeyguardSliceView".equals(name)) {
-                    return mMockKeyguardSliceView;
-                }
-                return null;
-            }
-        });
-        mKeyguardClockSwitch =
-                (KeyguardClockSwitch) layoutInflater.inflate(R.layout.keyguard_clock_switch, null);
-        mSmallClockFrame = mKeyguardClockSwitch
-                .findViewById(com.android.systemui.customization.R.id.lockscreen_clock_view);
-        mLargeClockFrame = mKeyguardClockSwitch
-                .findViewById(com.android.systemui.customization.R.id.lockscreen_clock_view_large);
-        mStatusArea = mKeyguardClockSwitch.findViewById(R.id.keyguard_status_area);
-        mKeyguardClockSwitch.mChildrenAreLaidOut = true;
-    }
-
-    @Test
-    public void noPluginConnected_showNothing() {
-        mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD);
-        assertEquals(mLargeClockFrame.getChildCount(), 0);
-        assertEquals(mSmallClockFrame.getChildCount(), 0);
-    }
-
-    @Test
-    public void pluginConnectedThenDisconnected_showNothing() {
-        mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD);
-        assertEquals(mLargeClockFrame.getChildCount(), 1);
-        assertEquals(mSmallClockFrame.getChildCount(), 1);
-
-        mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD);
-        assertEquals(mLargeClockFrame.getChildCount(), 0);
-        assertEquals(mSmallClockFrame.getChildCount(), 0);
-    }
-
-    @Test
-    public void onPluginConnected_showClock() {
-        mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD);
-
-        assertEquals(mClock.getSmallClock().getView().getParent(), mSmallClockFrame);
-        assertEquals(mClock.getLargeClock().getView().getParent(), mLargeClockFrame);
-    }
-
-    @Test
-    public void onPluginConnected_showSecondPluginClock() {
-        // GIVEN a plugin has already connected
-        ClockController otherClock = mock(ClockController.class);
-        ClockFaceController smallClock = mock(ClockFaceController.class);
-        ClockFaceController largeClock = mock(ClockFaceController.class);
-        when(otherClock.getSmallClock()).thenReturn(smallClock);
-        when(otherClock.getLargeClock()).thenReturn(largeClock);
-        when(smallClock.getView()).thenReturn(new TextView(getContext()));
-        when(largeClock.getView()).thenReturn(new TextView(getContext()));
-        mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD);
-        mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD);
-
-        // THEN only the view from the second plugin should be a child of KeyguardClockSwitch.
-        assertThat(otherClock.getSmallClock().getView().getParent()).isEqualTo(mSmallClockFrame);
-        assertThat(otherClock.getLargeClock().getView().getParent()).isEqualTo(mLargeClockFrame);
-        assertThat(mClock.getSmallClock().getView().getParent()).isNull();
-        assertThat(mClock.getLargeClock().getView().getParent()).isNull();
-    }
-
-    @Test
-    public void onPluginDisconnected_secondOfTwoDisconnected() {
-        // GIVEN two plugins are connected
-        ClockController otherClock = mock(ClockController.class);
-        ClockFaceController smallClock = mock(ClockFaceController.class);
-        ClockFaceController largeClock = mock(ClockFaceController.class);
-        when(otherClock.getSmallClock()).thenReturn(smallClock);
-        when(otherClock.getLargeClock()).thenReturn(largeClock);
-        when(smallClock.getView()).thenReturn(new TextView(getContext()));
-        when(largeClock.getView()).thenReturn(new TextView(getContext()));
-        mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD);
-        mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD);
-        // WHEN the second plugin is disconnected
-        mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD);
-        // THEN nothing should be shown
-        assertThat(otherClock.getSmallClock().getView().getParent()).isNull();
-        assertThat(otherClock.getLargeClock().getView().getParent()).isNull();
-        assertThat(mClock.getSmallClock().getView().getParent()).isNull();
-        assertThat(mClock.getLargeClock().getView().getParent()).isNull();
-    }
-
-    @Test
-    public void switchingToBigClockWithAnimation_makesSmallClockDisappear() {
-        mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true);
-
-        mKeyguardClockSwitch.mClockInAnim.end();
-        mKeyguardClockSwitch.mClockOutAnim.end();
-        mKeyguardClockSwitch.mStatusAreaAnim.end();
-
-        assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
-        assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
-        assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
-        assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE);
-    }
-
-    @Test
-    public void switchingToBigClockNoAnimation_makesSmallClockDisappear() {
-        mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ false);
-
-        assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
-        assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
-        assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
-        assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE);
-    }
-
-    @Test
-    public void switchingToSmallClockWithAnimation_makesBigClockDisappear() {
-        mKeyguardClockSwitch.switchToClock(SMALL, /* animate */ true);
-
-        mKeyguardClockSwitch.mClockInAnim.end();
-        mKeyguardClockSwitch.mClockOutAnim.end();
-        mKeyguardClockSwitch.mStatusAreaAnim.end();
-
-        assertThat(mSmallClockFrame.getAlpha()).isEqualTo(1);
-        assertThat(mSmallClockFrame.getVisibility()).isEqualTo(VISIBLE);
-        // only big clock is removed at switch
-        assertThat(mLargeClockFrame.getParent()).isNull();
-        assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
-        assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE);
-    }
-
-    @Test
-    public void switchingToSmallClockNoAnimation_makesBigClockDisappear() {
-        mKeyguardClockSwitch.switchToClock(SMALL, false);
-
-        assertThat(mSmallClockFrame.getAlpha()).isEqualTo(1);
-        assertThat(mSmallClockFrame.getVisibility()).isEqualTo(VISIBLE);
-        // only big clock is removed at switch
-        assertThat(mLargeClockFrame.getParent()).isNull();
-        assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
-        assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE);
-    }
-
-    @Test
-    public void switchingToSmallClockAnimation_resetsStatusArea() {
-        mKeyguardClockSwitch.switchToClock(SMALL, true);
-
-        mKeyguardClockSwitch.mClockInAnim.end();
-        mKeyguardClockSwitch.mClockOutAnim.end();
-        mKeyguardClockSwitch.mStatusAreaAnim.end();
-
-        assertThat(mStatusArea.getTranslationX()).isEqualTo(0);
-        assertThat(mStatusArea.getTranslationY()).isEqualTo(0);
-        assertThat(mStatusArea.getScaleX()).isEqualTo(1);
-        assertThat(mStatusArea.getScaleY()).isEqualTo(1);
-    }
-
-    @Test
-    public void switchingToSmallClockNoAnimation_resetsStatusArea() {
-        mKeyguardClockSwitch.switchToClock(SMALL, false);
-
-        assertThat(mStatusArea.getTranslationX()).isEqualTo(0);
-        assertThat(mStatusArea.getTranslationY()).isEqualTo(0);
-        assertThat(mStatusArea.getScaleX()).isEqualTo(1);
-        assertThat(mStatusArea.getScaleY()).isEqualTo(1);
-    }
-
-
-    @Test
-    public void switchingToBigClock_returnsTrueOnlyWhenItWasNotVisibleBefore() {
-        assertThat(mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true)).isTrue();
-        assertThat(mKeyguardClockSwitch.switchToClock(LARGE, /* animate */ true)).isFalse();
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index ab93659..66f44ba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -158,6 +158,22 @@
     private val mockFingerprintIconWidth = 300
     private val mockFingerprintIconHeight = 300
 
+    private val faceIconAuthingDescription =
+        R.string.biometric_dialog_face_icon_description_authenticating
+    private val faceIconAuthedDescription =
+        R.string.biometric_dialog_face_icon_description_authenticated
+    private val faceIconConfirmedDescription =
+        R.string.biometric_dialog_face_icon_description_confirmed
+    private val faceIconIdleDescription = R.string.biometric_dialog_face_icon_description_idle
+    private val sfpsFindSensorDescription =
+        R.string.security_settings_sfps_enroll_find_sensor_message
+    private val udfpsIconDescription = R.string.accessibility_fingerprint_label
+    private val faceFailedDescription = R.string.keyguard_face_failed
+    private val bpTryAgainDescription = R.string.biometric_dialog_try_again
+    private val bpConfirmDescription = R.string.biometric_dialog_confirm
+    private val fingerprintIconAuthenticatedDescription =
+        R.string.fingerprint_dialog_authenticated_confirmation
+
     /** Mock [UdfpsOverlayParams] for a test. */
     private fun mockUdfpsOverlayParams(isLandscape: Boolean = false): UdfpsOverlayParams =
         UdfpsOverlayParams(
@@ -337,21 +353,18 @@
             if ((testCase.isCoex && !forceExplicitFlow) || testCase.isFaceOnly) {
                 // Face-only or implicit co-ex auth
                 assertThat(iconAsset).isEqualTo(R.raw.face_dialog_authenticating)
-                assertThat(iconContentDescriptionId)
-                    .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating)
+                assertThat(iconContentDescriptionId).isEqualTo(faceIconAuthingDescription)
                 assertThat(shouldAnimateIconView).isEqualTo(true)
             } else if ((testCase.isCoex && forceExplicitFlow) || testCase.isFingerprintOnly) {
                 // Fingerprint-only or explicit co-ex auth
                 if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
                     assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintAuthenticating())
-                    assertThat(iconContentDescriptionId)
-                        .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
+                    assertThat(iconContentDescriptionId).isEqualTo(sfpsFindSensorDescription)
                     assertThat(shouldAnimateIconView).isEqualTo(true)
                 } else {
                     assertThat(iconAsset)
                         .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie)
-                    assertThat(iconContentDescriptionId)
-                        .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                    assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
                     assertThat(shouldAnimateIconView).isEqualTo(false)
                 }
             }
@@ -397,26 +410,25 @@
         if (testCase.isFaceOnly) {
             // Face-only auth
             assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_error)
-            assertThat(iconContentDescriptionId).isEqualTo(R.string.keyguard_face_failed)
+            assertThat(iconContentDescriptionId).isEqualTo(faceFailedDescription)
             assertThat(shouldAnimateIconView).isEqualTo(true)
 
             // Clear error, go to idle
             errorJob.join()
 
             assertThat(iconAsset).isEqualTo(R.raw.face_dialog_error_to_idle)
-            assertThat(iconContentDescriptionId)
-                .isEqualTo(R.string.biometric_dialog_face_icon_description_idle)
+            assertThat(iconContentDescriptionId).isEqualTo(faceIconIdleDescription)
             assertThat(shouldAnimateIconView).isEqualTo(true)
         } else if ((testCase.isCoex && forceExplicitFlow) || testCase.isFingerprintOnly) {
             // Fingerprint-only or explicit co-ex auth
             if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
                 assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintToError())
-                assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again)
+                assertThat(iconContentDescriptionId).isEqualTo(bpTryAgainDescription)
                 assertThat(shouldAnimateIconView).isEqualTo(true)
             } else {
                 assertThat(iconAsset)
                     .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie)
-                assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again)
+                assertThat(iconContentDescriptionId).isEqualTo(bpTryAgainDescription)
                 assertThat(shouldAnimateIconView).isEqualTo(true)
             }
 
@@ -425,14 +437,12 @@
 
             if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
                 assertThat(iconAsset).isEqualTo(getSfpsAsset_errorToFingerprint())
-                assertThat(iconContentDescriptionId)
-                    .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
+                assertThat(iconContentDescriptionId).isEqualTo(sfpsFindSensorDescription)
                 assertThat(shouldAnimateIconView).isEqualTo(true)
             } else {
                 assertThat(iconAsset)
                     .isEqualTo(R.raw.fingerprint_dialogue_error_to_fingerprint_lottie)
-                assertThat(iconContentDescriptionId)
-                    .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
                 assertThat(shouldAnimateIconView).isEqualTo(true)
             }
         }
@@ -472,13 +482,12 @@
                     // Covers (1) fingerprint-only (2) co-ex, authenticated by fingerprint
                     if (testCase.authenticatedByFingerprint) {
                         assertThat(iconAsset).isEqualTo(R.raw.biometricprompt_sfps_error_to_success)
-                        assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
+                        assertThat(iconContentDescriptionId).isEqualTo(sfpsFindSensorDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                     } else { // Covers co-ex, authenticated by face
                         assertThat(iconAsset).isEqualTo(R.raw.biometricprompt_sfps_error_to_unlock)
                         assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation)
+                            .isEqualTo(fingerprintIconAuthenticatedDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
 
                         // Confirm authentication
@@ -486,8 +495,7 @@
 
                         assertThat(iconAsset)
                             .isEqualTo(R.raw.biometricprompt_sfps_unlock_to_success)
-                        assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                     }
                 } else { // Non-SFPS (UDFPS / rear-FPS) test cases
@@ -495,14 +503,12 @@
                     if (testCase.authenticatedByFingerprint) {
                         assertThat(iconAsset)
                             .isEqualTo(R.raw.fingerprint_dialogue_error_to_success_lottie)
-                        assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                     } else { //  co-ex, authenticated by face
                         assertThat(iconAsset)
                             .isEqualTo(R.raw.fingerprint_dialogue_error_to_unlock_lottie)
-                        assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.biometric_dialog_confirm)
+                        assertThat(iconContentDescriptionId).isEqualTo(bpConfirmDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
 
                         // Confirm authentication
@@ -512,8 +518,7 @@
                             .isEqualTo(
                                 R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie
                             )
-                        assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                     }
                 }
@@ -543,22 +548,19 @@
                     // Fingerprint icon asset assertions
                     if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
                         assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintToSuccess())
-                        assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
+                        assertThat(iconContentDescriptionId).isEqualTo(sfpsFindSensorDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                     } else {
                         assertThat(iconAsset)
                             .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie)
-                        assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                     }
                 } else if (testCase.isFaceOnly || testCase.isCoex) {
                     // Face icon asset assertions
                     // If co-ex, use implicit flow (explicit flow always requires confirmation)
                     assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
-                    assertThat(iconContentDescriptionId)
-                        .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
+                    assertThat(iconContentDescriptionId).isEqualTo(faceIconAuthedDescription)
                     assertThat(shouldAnimateIconView).isEqualTo(true)
                     assertThat(message).isEqualTo(PromptMessage.Empty)
                 }
@@ -586,20 +588,18 @@
 
                 if (testCase.isFaceOnly) {
                     assertThat(iconAsset).isEqualTo(R.raw.face_dialog_wink_from_dark)
-                    assertThat(iconContentDescriptionId)
-                        .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
+                    assertThat(iconContentDescriptionId).isEqualTo(faceIconAuthedDescription)
                     assertThat(shouldAnimateIconView).isEqualTo(true)
                 } else if (testCase.isCoex) { // explicit flow, confirmation requested
                     if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
                         assertThat(iconAsset).isEqualTo(getSfpsAsset_fingerprintToUnlock())
                         assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation)
+                            .isEqualTo(fingerprintIconAuthenticatedDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                     } else {
                         assertThat(iconAsset)
                             .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie)
-                        assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.biometric_dialog_confirm)
+                        assertThat(iconContentDescriptionId).isEqualTo(bpConfirmDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                     }
                 }
@@ -628,8 +628,7 @@
 
                 if (testCase.isFaceOnly) {
                     assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
-                    assertThat(iconContentDescriptionId)
-                        .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed)
+                    assertThat(iconContentDescriptionId).isEqualTo(faceIconConfirmedDescription)
                     assertThat(shouldAnimateIconView).isEqualTo(true)
                 }
 
@@ -644,8 +643,7 @@
                             .isEqualTo(
                                 R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie
                             )
-                        assertThat(iconContentDescriptionId)
-                            .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        assertThat(iconContentDescriptionId).isEqualTo(udfpsIconDescription)
                         assertThat(shouldAnimateIconView).isEqualTo(true)
                     }
                 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt
index a308c8e..3f4d3f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt
@@ -98,6 +98,21 @@
         }
 
     @Test
+    fun onMovedToDisplays_updatesOnMovedToDisplay() =
+        testScope.runTest {
+            val lastOnMovedToDisplay by collectLastValue(underTest.onMovedToDisplay)
+            assertThat(lastOnMovedToDisplay).isNull()
+
+            val configurationCallback = withArgCaptor {
+                verify(configurationController).addCallback(capture())
+            }
+
+            configurationCallback.onMovedToDisplay(1, Configuration())
+            runCurrent()
+            assertThat(lastOnMovedToDisplay).isEqualTo(1)
+        }
+
+    @Test
     fun onAnyConfigurationChange_updatesOnConfigChanged() =
         testScope.runTest {
             val lastAnyConfigurationChange by collectLastValue(underTest.onAnyConfigurationChange)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModelTest.kt
new file mode 100644
index 0000000..a8a3873
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModelTest.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import android.appwidget.AppWidgetHost.AppWidgetHostListener
+import android.appwidget.AppWidgetHostView
+import android.platform.test.flag.junit.FlagsParameterization
+import android.util.SizeF
+import android.widget.RemoteViews
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_SECONDARY_USER_WIDGET_HOST
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper
+import com.android.systemui.communal.widgets.AppWidgetHostListenerDelegate
+import com.android.systemui.communal.widgets.CommunalAppWidgetHost
+import com.android.systemui.communal.widgets.GlanceableHubWidgetManager
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.testKosmos
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class CommunalAppWidgetViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+    val kosmos = testKosmos()
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
+    private val Kosmos.listenerDelegateFactory by
+        Kosmos.Fixture {
+            AppWidgetHostListenerDelegate.Factory { listener ->
+                AppWidgetHostListenerDelegate(fakeExecutor, listener)
+            }
+        }
+
+    private val Kosmos.appWidgetHost by
+        Kosmos.Fixture {
+            mock<CommunalAppWidgetHost> {
+                on { setListener(any(), any()) } doAnswer
+                    { invocation ->
+                        val callback = invocation.arguments[1] as AppWidgetHostListener
+                        callback.updateAppWidget(mock<RemoteViews>())
+                    }
+            }
+        }
+
+    private val Kosmos.glanceableHubWidgetManager by
+        Kosmos.Fixture {
+            mock<GlanceableHubWidgetManager> {
+                on { setAppWidgetHostListener(any(), any()) } doAnswer
+                    { invocation ->
+                        val callback = invocation.arguments[1] as AppWidgetHostListener
+                        callback.updateAppWidget(mock<RemoteViews>())
+                    }
+            }
+        }
+
+    private val Kosmos.underTest by
+        Kosmos.Fixture {
+            CommunalAppWidgetViewModel(
+                    backgroundCoroutineContext,
+                    { appWidgetHost },
+                    listenerDelegateFactory,
+                    { glanceableHubWidgetManager },
+                    fakeGlanceableHubMultiUserHelper,
+                )
+                .apply { activateIn(testScope) }
+        }
+
+    @Test
+    fun setListener() =
+        kosmos.runTest {
+            val listener = mock<AppWidgetHostListener>()
+
+            underTest.setListener(123, listener)
+            runAll()
+
+            verify(listener).updateAppWidget(any())
+        }
+
+    @Test
+    fun setListener_HSUM() =
+        kosmos.runTest {
+            fakeGlanceableHubMultiUserHelper.setIsInHeadlessSystemUser(true)
+            val listener = mock<AppWidgetHostListener>()
+
+            underTest.setListener(123, listener)
+            runAll()
+
+            verify(listener).updateAppWidget(any())
+        }
+
+    @Test
+    fun updateSize() =
+        kosmos.runTest {
+            val view = mock<AppWidgetHostView>()
+            val size = SizeF(/* width= */ 100f, /* height= */ 200f)
+
+            underTest.updateSize(size, view)
+            runAll()
+
+            verify(view)
+                .updateAppWidgetSize(
+                    /* newOptions = */ any(),
+                    /* minWidth = */ eq(100),
+                    /* minHeight = */ eq(200),
+                    /* maxWidth = */ eq(100),
+                    /* maxHeight = */ eq(200),
+                    /* ignorePadding = */ eq(true),
+                )
+        }
+
+    private fun Kosmos.runAll() {
+        runCurrent()
+        fakeExecutor.runAllReady()
+    }
+
+    private companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf(FLAG_SECONDARY_USER_WIDGET_HOST)
+        }
+    }
+}
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 9d711ab..d70af28 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
@@ -172,7 +172,6 @@
             kosmos.testDispatcher,
             testScope,
             kosmos.testScope.backgroundScope,
-            context.resources,
             kosmos.keyguardTransitionInteractor,
             kosmos.keyguardInteractor,
             mock<KeyguardIndicationController>(),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepositoryTest.kt
new file mode 100644
index 0000000..477e31e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepositoryTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.data.repository
+
+import android.hardware.input.AppLaunchData
+import android.hardware.input.AppLaunchData.RoleData
+import android.hardware.input.InputGestureData
+import android.hardware.input.InputGestureData.createKeyTrigger
+import android.hardware.input.fakeInputManager
+import android.view.KeyEvent.KEYCODE_A
+import android.view.KeyEvent.META_ALT_ON
+import android.view.KeyEvent.META_CTRL_ON
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyboard.shortcut.appLaunchDataRepository
+import com.android.systemui.keyboard.shortcut.shared.model.shortcutCommand
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AppLaunchDataRepositoryTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val inputManager = kosmos.fakeInputManager.inputManager
+    private val testHelper = kosmos.shortcutHelperTestHelper
+    private val repo = kosmos.appLaunchDataRepository
+
+    @Test
+    fun appLaunchData_returnsDataRetrievedFromApiBasedOnShortcutCommand() =
+        kosmos.runTest {
+            val inputGesture = simpleInputGestureDataForAppLaunchShortcut()
+            setApiAppLaunchBookmarks(listOf(inputGesture))
+
+            testHelper.toggle(TEST_DEVICE_ID)
+
+            val appLaunchData =
+                repo.getAppLaunchDataForShortcutWithCommand(
+                    shortcutCommand =
+                        shortcutCommand {
+                            key("Ctrl")
+                            key("Alt")
+                            key("A")
+                        }
+                )
+
+            assertThat(appLaunchData).isEqualTo(inputGesture.action.appLaunchData())
+        }
+
+    @Test
+    fun appLaunchData_returnsSameDataForAnyOrderOfShortcutCommandKeys() =
+        kosmos.runTest {
+            val inputGesture = simpleInputGestureDataForAppLaunchShortcut()
+            setApiAppLaunchBookmarks(listOf(inputGesture))
+
+            testHelper.toggle(TEST_DEVICE_ID)
+
+            val shortcutCommandCtrlAltA = shortcutCommand {
+                key("Ctrl")
+                key("Alt")
+                key("A")
+            }
+
+            val shortcutCommandCtrlAAlt = shortcutCommand {
+                key("Ctrl")
+                key("A")
+                key("Alt")
+            }
+
+            val shortcutCommandAltCtrlA = shortcutCommand {
+                key("Alt")
+                key("Ctrl")
+                key("A")
+            }
+
+            assertThat(repo.getAppLaunchDataForShortcutWithCommand(shortcutCommandCtrlAltA))
+                .isEqualTo(inputGesture.action.appLaunchData())
+
+            assertThat(repo.getAppLaunchDataForShortcutWithCommand(shortcutCommandCtrlAAlt))
+                .isEqualTo(inputGesture.action.appLaunchData())
+
+            assertThat(repo.getAppLaunchDataForShortcutWithCommand(shortcutCommandAltCtrlA))
+                .isEqualTo(inputGesture.action.appLaunchData())
+        }
+
+    private fun setApiAppLaunchBookmarks(appLaunchBookmarks: List<InputGestureData>) {
+        whenever(inputManager.appLaunchBookmarks).thenReturn(appLaunchBookmarks)
+    }
+
+    private fun simpleInputGestureDataForAppLaunchShortcut(
+        keyCode: Int = KEYCODE_A,
+        modifiers: Int = META_CTRL_ON or META_ALT_ON,
+        appLaunchData: AppLaunchData = RoleData(TEST_ROLE),
+    ): InputGestureData {
+        return InputGestureData.Builder()
+            .setTrigger(createKeyTrigger(keyCode, modifiers))
+            .setAppLaunchData(appLaunchData)
+            .build()
+    }
+
+    private companion object {
+        private const val TEST_ROLE = "Test role"
+        private const val TEST_DEVICE_ID = 123
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
index d12c045..4cfb26e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
@@ -18,14 +18,21 @@
 
 import android.content.Context
 import android.content.Context.INPUT_SERVICE
+import android.hardware.input.AppLaunchData
+import android.hardware.input.AppLaunchData.RoleData
 import android.hardware.input.InputGestureData
+import android.hardware.input.InputGestureData.createKeyTrigger
 import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST
 import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION
 import android.hardware.input.fakeInputManager
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
+import android.view.KeyEvent.KEYCODE_A
 import android.view.KeyEvent.KEYCODE_SLASH
+import android.view.KeyEvent.META_ALT_ON
 import android.view.KeyEvent.META_CAPS_LOCK_ON
+import android.view.KeyEvent.META_CTRL_ON
 import android.view.KeyEvent.META_META_ON
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -44,14 +51,15 @@
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.customizableInputGestureWithUnknownKeyGestureType
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedShortcutCategoriesWithSimpleShortcutCombination
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.launchCalendarShortcutAddRequest
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardKeyCombination
 import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.Add
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.Delete
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.SingleShortcutCustomization
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.userTracker
@@ -72,7 +80,7 @@
 
     private val mockUserContext: Context = mock()
     private val kosmos =
-        testKosmos().also {
+        testKosmos().useUnconfinedTestDispatcher().also {
             it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
         }
 
@@ -242,6 +250,32 @@
     }
 
     @Test
+    fun buildInputGestureDataForAppLaunchShortcut_keyGestureTypeIsTypeLaunchApp() =
+        testScope.runTest {
+            setApiAppLaunchBookmarks(listOf(simpleInputGestureDataForAppLaunchShortcut()))
+            helper.toggle(deviceId = 123)
+            repo.onCustomizationRequested(launchCalendarShortcutAddRequest)
+            repo.updateUserKeyCombination(standardKeyCombination)
+
+            val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized()
+
+            assertThat(inputGestureData?.action?.keyGestureType())
+                .isEqualTo(KEY_GESTURE_TYPE_LAUNCH_APPLICATION)
+        }
+
+    @Test
+    fun buildInputGestureDataForAppLaunchShortcut_appLaunchDataIsAdded() =
+        testScope.runTest {
+            setApiAppLaunchBookmarks(listOf(simpleInputGestureDataForAppLaunchShortcut()))
+            helper.toggle(deviceId = 123)
+            repo.onCustomizationRequested(launchCalendarShortcutAddRequest)
+            repo.updateUserKeyCombination(standardKeyCombination)
+
+            val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized()
+            assertThat(inputGestureData?.action?.appLaunchData()).isNotNull()
+        }
+
+    @Test
     @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     fun deleteShortcut_successfullyRetrievesGestureDataAndDeletesShortcut() {
         testScope.runTest {
@@ -304,17 +338,17 @@
 
     private suspend fun customizeShortcut(
         customizationRequest: ShortcutCustomizationRequestInfo,
-        keyCombination: KeyCombination? = null
-    ): ShortcutCustomizationRequestResult{
+        keyCombination: KeyCombination? = null,
+    ): ShortcutCustomizationRequestResult {
         repo.onCustomizationRequested(customizationRequest)
         repo.updateUserKeyCombination(keyCombination)
 
         return when (customizationRequest) {
-            is Add -> {
+            is SingleShortcutCustomization.Add -> {
                 repo.confirmAndSetShortcutCurrentlyBeingCustomized()
             }
 
-            is Delete -> {
+            is SingleShortcutCustomization.Delete -> {
                 repo.deleteShortcutCurrentlyBeingCustomized()
             }
 
@@ -352,4 +386,19 @@
             assertThat(categories).isEmpty()
         }
     }
+
+    private fun setApiAppLaunchBookmarks(appLaunchBookmarks: List<InputGestureData>) {
+        whenever(inputManager.appLaunchBookmarks).thenReturn(appLaunchBookmarks)
+    }
+
+    private fun simpleInputGestureDataForAppLaunchShortcut(
+        keyCode: Int = KEYCODE_A,
+        modifiers: Int = META_CTRL_ON or META_ALT_ON,
+        appLaunchData: AppLaunchData = RoleData("Test role"),
+    ): InputGestureData {
+        return InputGestureData.Builder()
+            .setTrigger(createKeyTrigger(keyCode, modifiers))
+            .setAppLaunchData(appLaunchData)
+            .build()
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapterTest.kt
index f78c692..9641059 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapterTest.kt
@@ -28,6 +28,7 @@
 import android.hardware.input.AppLaunchData.RoleData
 import android.hardware.input.InputGestureData
 import android.hardware.input.InputGestureData.createKeyTrigger
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION
 import android.view.KeyEvent.KEYCODE_A
 import android.view.KeyEvent.META_ALT_ON
 import android.view.KeyEvent.META_CTRL_ON
@@ -55,14 +56,15 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
-
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class InputGestureDataAdapterTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos().also { kosmos ->
-        kosmos.userTracker = FakeUserTracker(onCreateCurrentUserContext = { kosmos.mockedContext })
-    }
+    private val kosmos =
+        testKosmos().also { kosmos ->
+            kosmos.userTracker =
+                FakeUserTracker(onCreateCurrentUserContext = { kosmos.mockedContext })
+        }
     private val adapter = kosmos.inputGestureDataAdapter
     private val roleManager = kosmos.roleManager
     private val packageManager: PackageManager = kosmos.packageManager
@@ -139,24 +141,40 @@
             val inputGestureData = buildInputGestureDataForAppLaunchShortcut()
             val internalGroups = adapter.toInternalGroupSources(listOf(inputGestureData))
 
-            assertThat(internalGroups).containsExactly(
-                InternalGroupsSource(
-                    type = ShortcutCategoryType.AppCategories,
-                    groups = listOf(
-                        InternalKeyboardShortcutGroup(
-                            label = APPLICATION_SHORTCUT_GROUP_LABEL,
-                            items = listOf(
-                                InternalKeyboardShortcutInfo(
-                                    label = expectedShortcutLabelForFirstAppMatchingIntent,
-                                    keycode = KEYCODE_A,
-                                    modifiers = META_CTRL_ON or META_ALT_ON,
-                                    isCustomShortcut = true
+            assertThat(internalGroups)
+                .containsExactly(
+                    InternalGroupsSource(
+                        type = ShortcutCategoryType.AppCategories,
+                        groups =
+                            listOf(
+                                InternalKeyboardShortcutGroup(
+                                    label = APPLICATION_SHORTCUT_GROUP_LABEL,
+                                    items =
+                                        listOf(
+                                            InternalKeyboardShortcutInfo(
+                                                label =
+                                                    expectedShortcutLabelForFirstAppMatchingIntent,
+                                                keycode = KEYCODE_A,
+                                                modifiers = META_CTRL_ON or META_ALT_ON,
+                                                isCustomShortcut = true,
+                                            )
+                                        ),
                                 )
-                            )
-                        )
+                            ),
                     )
                 )
-            )
+        }
+
+    @Test
+    fun keyGestureType_returnsTypeLaunchApplicationForAppLaunchShortcutCategory() =
+        kosmos.runTest {
+            assertThat(
+                    adapter.getKeyGestureTypeForShortcut(
+                        shortcutLabel = "Test Shortcut label",
+                        shortcutCategoryType = ShortcutCategoryType.AppCategories,
+                    )
+                )
+                .isEqualTo(KEY_GESTURE_TYPE_LAUNCH_APPLICATION)
         }
 
     private fun setApiToRetrieveResolverActivity() {
@@ -169,11 +187,10 @@
             .thenReturn(fakeActivityInfo)
     }
 
-
     private fun buildInputGestureDataForAppLaunchShortcut(
         keyCode: Int = KEYCODE_A,
         modifiers: Int = META_CTRL_ON or META_ALT_ON,
-        appLaunchData: AppLaunchData = RoleData(TEST_ROLE)
+        appLaunchData: AppLaunchData = RoleData(TEST_ROLE),
     ): InputGestureData {
         return InputGestureData.Builder()
             .setTrigger(createKeyTrigger(keyCode, modifiers))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepositoryTest.kt
new file mode 100644
index 0000000..ded2d22
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepositoryTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyboard.shortcut.shortcutHelperInputDeviceRepository
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutHelperInputDeviceRepositoryTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val testHelper = kosmos.shortcutHelperTestHelper
+    private val repo = kosmos.shortcutHelperInputDeviceRepository
+
+    @Test
+    fun activeInputDevice_nullByDefault() =
+        kosmos.runTest {
+            val activeInputDevice by collectLastValue(repo.activeInputDevice)
+
+            assertThat(activeInputDevice).isNull()
+        }
+
+    @Test
+    fun activeInputDevice_nonNullWhenHelperIsShown() =
+        kosmos.runTest {
+            val activeInputDevice by collectLastValue(repo.activeInputDevice)
+
+            testHelper.showFromActivity()
+
+            assertThat(activeInputDevice).isNotNull()
+        }
+
+    @Test
+    fun activeInputDevice_nullWhenHelperIsClosed() =
+        kosmos.runTest {
+            val activeInputDevice by collectLastValue(repo.activeInputDevice)
+
+            testHelper.showFromActivity()
+            testHelper.hideFromActivity()
+
+            assertThat(activeInputDevice).isNull()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
index 7dc7016..7c88d76 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
@@ -43,11 +43,12 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.SingleShortcutCustomization
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
 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.shortcutCommand
 import com.android.systemui.res.R
 
 object TestShortcuts {
@@ -596,14 +597,27 @@
         )
 
     val allAppsShortcutAddRequest =
-        ShortcutCustomizationRequestInfo.Add(
+        SingleShortcutCustomization.Add(
             label = "Open apps list",
             categoryType = System,
             subCategoryLabel = "System controls",
         )
 
+    val launchCalendarShortcutAddRequest =
+        SingleShortcutCustomization.Add(
+            label = "Calendar",
+            categoryType = ShortcutCategoryType.AppCategories,
+            subCategoryLabel = "Applications",
+            shortcutCommand =
+                shortcutCommand {
+                    key("Ctrl")
+                    key("Alt")
+                    key("A")
+                },
+        )
+
     val allAppsShortcutDeleteRequest =
-        ShortcutCustomizationRequestInfo.Delete(
+        SingleShortcutCustomization.Delete(
             label = "Open apps list",
             categoryType = System,
             subCategoryLabel = "System controls",
@@ -698,7 +712,7 @@
         )
 
     val standardAddShortcutRequest =
-        ShortcutCustomizationRequestInfo.Add(
+        SingleShortcutCustomization.Add(
             label = "Standard shortcut",
             categoryType = System,
             subCategoryLabel = "Standard subcategory",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 77f1979..6805a13 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -123,27 +123,6 @@
         }
 
     @Test
-    fun bottomAreaAlpha() =
-        testScope.runTest {
-            assertThat(underTest.bottomAreaAlpha.value).isEqualTo(1f)
-
-            underTest.setBottomAreaAlpha(0.1f)
-            assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.1f)
-
-            underTest.setBottomAreaAlpha(0.2f)
-            assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.2f)
-
-            underTest.setBottomAreaAlpha(0.3f)
-            assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.3f)
-
-            underTest.setBottomAreaAlpha(0.5f)
-            assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.5f)
-
-            underTest.setBottomAreaAlpha(1.0f)
-            assertThat(underTest.bottomAreaAlpha.value).isEqualTo(1f)
-        }
-
-    @Test
     fun panelAlpha() =
         testScope.runTest {
             assertThat(underTest.panelAlpha.value).isEqualTo(1f)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
index 2101987..9d5bf4d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
@@ -55,7 +55,6 @@
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -887,7 +886,6 @@
 
     @Test
     @EnableSceneContainer
-    @Ignore("b/378766637")
     fun lockscreenVisibilityWithScenes() =
         testScope.runTest {
             val isDeviceUnlocked by
@@ -896,6 +894,7 @@
                 )
             assertThat(isDeviceUnlocked).isFalse()
 
+            kosmos.setSceneTransition(Idle(Scenes.Lockscreen))
             val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
index 10f7128..9ceabd7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
@@ -56,21 +56,12 @@
 
     @Test
     fun addViewsConditionally() {
-        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
         assertThat(constraintLayout.childCount).isGreaterThan(0)
     }
 
     @Test
-    fun addViewsConditionally_migrateFlagOff() {
-        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-        val constraintLayout = ConstraintLayout(context, null)
-        underTest.addViews(constraintLayout)
-        assertThat(constraintLayout.childCount).isEqualTo(0)
-    }
-
-    @Test
     fun applyConstraints() {
         val cs = ConstraintSet()
         underTest.applyConstraints(cs)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
index 9fab603..70d7a5f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
@@ -27,7 +27,9 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -47,6 +49,8 @@
     private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
     private val underTest by lazy { kosmos.alternateBouncerToPrimaryBouncerTransitionViewModel }
 
+    private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
     @Test
     fun deviceEntryParentViewDisappear() =
         testScope.runTest {
@@ -67,13 +71,44 @@
             values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
+    @Test
+    fun blurRadiusGoesToMaximumWhenShadeIsExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
+                startValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                checkInterpolatedValues = false,
+                transitionFactory = ::step,
+                actualValuesProvider = { values },
+            )
+        }
+
+    @Test
+    fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(false)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
+                startValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                transitionFactory = ::step,
+                actualValuesProvider = { values },
+            )
+        }
+
     private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep {
         return TransitionStep(
             from = KeyguardState.ALTERNATE_BOUNCER,
             to = KeyguardState.PRIMARY_BOUNCER,
             value = value,
             transitionState = state,
-            ownerName = "AlternateBouncerToPrimaryBouncerTransitionViewModelTest"
+            ownerName = "AlternateBouncerToPrimaryBouncerTransitionViewModelTest",
         )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModelTest.kt
new file mode 100644
index 0000000..0f239e8
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModelTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AodToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest by lazy { kosmos.aodToPrimaryBouncerTransitionViewModel }
+
+    @Test
+    fun aodToPrimaryBouncerChangesBlurToMax() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0.0f, 0.0f, 0.3f, 0.4f, 0.5f, 1.0f),
+                startValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                transitionFactory = { value, state ->
+                    TransitionStep(
+                        from = KeyguardState.AOD,
+                        to = KeyguardState.PRIMARY_BOUNCER,
+                        value = value,
+                        transitionState = state,
+                        ownerName = "AodToPrimaryBouncerTransitionViewModelTest",
+                    )
+                },
+                actualValuesProvider = { values },
+                checkInterpolatedValues = false,
+            )
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerWindowBlurTestUtilKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerWindowBlurTestUtilKosmos.kt
new file mode 100644
index 0000000..c3f0deb
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerWindowBlurTestUtilKosmos.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.util.MathUtils
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.ShadeTestUtil
+import com.android.systemui.shade.shadeTestUtil
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+
+val Kosmos.bouncerWindowBlurTestUtil by
+    Kosmos.Fixture {
+        BouncerWindowBlurTestUtil(
+            shadeTestUtil = shadeTestUtil,
+            fakeKeyguardTransitionRepository = fakeKeyguardTransitionRepository,
+            fakeKeyguardRepository = fakeKeyguardRepository,
+            testScope = testScope,
+        )
+    }
+
+class BouncerWindowBlurTestUtil(
+    private val shadeTestUtil: ShadeTestUtil,
+    private val fakeKeyguardTransitionRepository: FakeKeyguardTransitionRepository,
+    private val fakeKeyguardRepository: FakeKeyguardRepository,
+    private val testScope: TestScope,
+) {
+
+    suspend fun assertTransitionToBlurRadius(
+        transitionProgress: List<Float>,
+        startValue: Float,
+        endValue: Float,
+        actualValuesProvider: () -> List<Float>,
+        transitionFactory: (value: Float, state: TransitionState) -> TransitionStep,
+        checkInterpolatedValues: Boolean = true,
+    ) {
+        val transitionSteps =
+            listOf(
+                transitionFactory(transitionProgress.first(), STARTED),
+                *transitionProgress.drop(1).map { transitionFactory(it, RUNNING) }.toTypedArray(),
+            )
+        fakeKeyguardTransitionRepository.sendTransitionSteps(transitionSteps, testScope)
+
+        val interpolationFunction = { step: Float -> MathUtils.lerp(startValue, endValue, step) }
+
+        if (checkInterpolatedValues) {
+            assertThat(actualValuesProvider.invoke())
+                .containsExactly(*transitionProgress.map(interpolationFunction).toTypedArray())
+        } else {
+            assertThat(actualValuesProvider.invoke()).contains(endValue)
+        }
+    }
+
+    fun shadeExpanded(expanded: Boolean) {
+        if (expanded) {
+            shadeTestUtil.setQsExpansion(1f)
+        } else {
+            fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            shadeTestUtil.setQsExpansion(0f)
+            shadeTestUtil.setLockscreenShadeExpansion(0f)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModelTest.kt
index bf71bec..7a68d4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModelTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -70,13 +71,27 @@
             values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
+    @Test
+    fun windowBlurRadiusGoesFromMinToMax() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f),
+                startValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                actualValuesProvider = { values },
+                transitionFactory = ::step,
+            )
+        }
+
     private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep {
         return TransitionStep(
             from = KeyguardState.DOZING,
             to = KeyguardState.PRIMARY_BOUNCER,
             value = value,
             transitionState = state,
-            ownerName = "DozingToPrimaryBouncerTransitionViewModelTest"
+            ownerName = "DozingToPrimaryBouncerTransitionViewModelTest",
         )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
index 6f74ed3..242ee3a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
@@ -20,7 +20,6 @@
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
 import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
@@ -31,7 +30,6 @@
 import com.android.systemui.doze.util.BurnInHelperWrapper
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.BurnInModel
@@ -61,8 +59,6 @@
 class KeyguardIndicationAreaViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-
-    private val bottomAreaInteractor = kosmos.keyguardBottomAreaInteractor
     private lateinit var underTest: KeyguardIndicationAreaViewModel
     private val keyguardRepository = kosmos.fakeKeyguardRepository
     private val communalSceneRepository = kosmos.fakeCommunalSceneRepository
@@ -87,12 +83,6 @@
 
     @Before
     fun setUp() {
-        val bottomAreaViewModel =
-            mock<KeyguardBottomAreaViewModel> {
-                on { startButton } doReturn startButtonFlow
-                on { endButton } doReturn endButtonFlow
-                on { alpha } doReturn alphaFlow
-            }
         val burnInInteractor =
             mock<BurnInInteractor> {
                 on { burnIn(anyInt(), anyInt()) } doReturn flowOf(BurnInModel())
@@ -109,8 +99,6 @@
         underTest =
             KeyguardIndicationAreaViewModel(
                 keyguardInteractor = kosmos.keyguardInteractor,
-                bottomAreaInteractor = bottomAreaInteractor,
-                keyguardBottomAreaViewModel = bottomAreaViewModel,
                 burnInHelperWrapper = burnInHelperWrapper,
                 burnInInteractor = burnInInteractor,
                 shortcutsCombinedViewModel = shortcutsCombinedViewModel,
@@ -123,23 +111,6 @@
     }
 
     @Test
-    fun alpha() =
-        testScope.runTest {
-            val alpha by collectLastValue(underTest.alpha)
-
-            assertThat(alpha).isEqualTo(1f)
-            alphaFlow.value = 0.1f
-            assertThat(alpha).isEqualTo(0.1f)
-            alphaFlow.value = 0.5f
-            assertThat(alpha).isEqualTo(0.5f)
-            alphaFlow.value = 0.2f
-            assertThat(alpha).isEqualTo(0.2f)
-            alphaFlow.value = 0f
-            assertThat(alpha).isEqualTo(0f)
-        }
-
-    @Test
-    @DisableFlags(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
     fun isIndicationAreaPadded() =
         testScope.runTest {
             keyguardRepository.setKeyguardShowing(true)
@@ -157,23 +128,6 @@
         }
 
     @Test
-    @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-    fun indicationAreaTranslationX() =
-        testScope.runTest {
-            val translationX by collectLastValue(underTest.indicationAreaTranslationX)
-
-            assertThat(translationX).isEqualTo(0f)
-            bottomAreaInteractor.setClockPosition(100, 100)
-            assertThat(translationX).isEqualTo(100f)
-            bottomAreaInteractor.setClockPosition(200, 100)
-            assertThat(translationX).isEqualTo(200f)
-            bottomAreaInteractor.setClockPosition(200, 200)
-            assertThat(translationX).isEqualTo(200f)
-            bottomAreaInteractor.setClockPosition(300, 100)
-            assertThat(translationX).isEqualTo(300f)
-        }
-
-    @Test
     @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun indicationAreaTranslationY() =
         testScope.runTest {
@@ -236,7 +190,6 @@
         @Parameters(name = "{0}")
         fun getParams(): List<FlagsParameterization> {
             return FlagsParameterization.allCombinationsOf(
-                FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
                 FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
             )
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index b5e670c..95ffc96 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -24,7 +24,6 @@
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
 import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.communalSceneRepository
@@ -74,7 +73,7 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-@EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
+@EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
 class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
index 1c1fcc4..fba3997 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
@@ -21,6 +21,7 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.BrokenWithSceneContainer
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.andSceneContainer
@@ -31,6 +32,7 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.data.repository.sceneContainerRepository
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -128,7 +130,7 @@
                     emptyFlow(),
                     emptyFlow(),
                     false,
-                    emptyFlow()
+                    emptyFlow(),
                 )
             runCurrent()
             // fade out
@@ -150,6 +152,39 @@
             Truth.assertThat(actual).isEqualTo(0f)
         }
 
+    @Test
+    @BrokenWithSceneContainer(330311871)
+    fun blurRadiusIsMaxWhenShadeIsExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f),
+                startValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                actualValuesProvider = { values },
+                transitionFactory = ::step,
+                checkInterpolatedValues = false,
+            )
+        }
+
+    @Test
+    @BrokenWithSceneContainer(330311871)
+    fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(false)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f),
+                startValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                actualValuesProvider = { values },
+                transitionFactory = ::step,
+            )
+        }
+
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING,
@@ -161,7 +196,7 @@
                 else KeyguardState.PRIMARY_BOUNCER,
             value = value,
             transitionState = state,
-            ownerName = "LockscreenToPrimaryBouncerTransitionViewModelTest"
+            ownerName = "LockscreenToPrimaryBouncerTransitionViewModelTest",
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
index c55c27c..b406e6c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModelTest.kt
@@ -21,11 +21,13 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -138,16 +140,31 @@
             assertThat(deviceEntryParentViewAlpha).isNull()
         }
 
+    @Test
+    fun blurRadiusGoesToMinImmediately() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f),
+                startValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                actualValuesProvider = { values },
+                transitionFactory = ::step,
+                checkInterpolatedValues = false,
+            )
+        }
+
     private fun step(
         value: Float,
-        state: TransitionState = TransitionState.RUNNING
+        state: TransitionState = TransitionState.RUNNING,
     ): TransitionStep {
         return TransitionStep(
             from = KeyguardState.PRIMARY_BOUNCER,
             to = KeyguardState.AOD,
             value = value,
             transitionState = state,
-            ownerName = "PrimaryBouncerToAodTransitionViewModelTest"
+            ownerName = "PrimaryBouncerToAodTransitionViewModelTest",
         )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModelTest.kt
index 28473b2..a8f0f2f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -122,13 +123,28 @@
             values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
+    @Test
+    fun blurRadiusGoesToMinImmediately() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f),
+                startValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                actualValuesProvider = { values },
+                transitionFactory = ::step,
+                checkInterpolatedValues = false,
+            )
+        }
+
     private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep {
         return TransitionStep(
             from = KeyguardState.PRIMARY_BOUNCER,
             to = KeyguardState.DOZING,
             value = value,
             transitionState = state,
-            ownerName = "PrimaryBouncerToDozingTransitionViewModelTest"
+            ownerName = "PrimaryBouncerToDozingTransitionViewModelTest",
         )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModelTest.kt
new file mode 100644
index 0000000..2c6e553
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModelTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PrimaryBouncerToGlanceableHubTransitionViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest by lazy { kosmos.primaryBouncerToGlanceableHubTransitionViewModel }
+
+    @Test
+    @DisableSceneContainer
+    fun blurBecomesMinValueImmediately() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f),
+                startValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                actualValuesProvider = { values },
+                transitionFactory = { step, transitionState ->
+                    TransitionStep(
+                        from = KeyguardState.PRIMARY_BOUNCER,
+                        to = KeyguardState.GLANCEABLE_HUB,
+                        value = step,
+                        transitionState = transitionState,
+                        ownerName = "PrimaryBouncerToGlanceableHubTransitionViewModelTest",
+                    )
+                },
+                checkInterpolatedValues = false,
+            )
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
index 5ec566b..9e5976f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
@@ -21,11 +21,13 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.collect.Range
@@ -110,16 +112,47 @@
             assertThat(bgViewAlpha).isEqualTo(1f)
         }
 
+    @Test
+    fun blurRadiusGoesFromMaxToMinWhenShadeIsNotExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(false)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f),
+                startValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS,
+                actualValuesProvider = { values },
+                transitionFactory = ::step,
+            )
+        }
+
+    @Test
+    fun blurRadiusRemainsAtMaxWhenShadeIsExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.windowBlurRadius)
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f),
+                startValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                endValue = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS,
+                actualValuesProvider = { values },
+                transitionFactory = ::step,
+                checkInterpolatedValues = false,
+            )
+        }
+
     private fun step(
         value: Float,
-        state: TransitionState = TransitionState.RUNNING
+        state: TransitionState = TransitionState.RUNNING,
     ): TransitionStep {
         return TransitionStep(
             from = KeyguardState.PRIMARY_BOUNCER,
             to = KeyguardState.LOCKSCREEN,
             value = value,
             transitionState = state,
-            ownerName = "PrimaryBouncerToLockscreenTransitionViewModelTest"
+            ownerName = "PrimaryBouncerToLockscreenTransitionViewModelTest",
         )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt
index 9edd62a..6a2aae1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt
@@ -16,21 +16,23 @@
 
 package com.android.systemui.media.controls.ui.viewmodel
 
-import android.R
 import android.content.packageManager
 import android.content.pm.ApplicationInfo
 import android.media.MediaMetadata
 import android.media.session.MediaSession
 import android.media.session.PlaybackState
+import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
+import com.android.systemui.media.controls.shared.model.MediaButton
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDeviceData
 import com.android.systemui.media.controls.util.mediaInstanceId
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.notificationLockscreenUserManager
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -132,6 +134,31 @@
             assertThat(underTest.setPlayer(playerModel!!)).isTrue()
         }
 
+    @Test
+    fun reservedButtons_showScrubbingTimes() =
+        testScope.runTest {
+            val playerModel by collectLastValue(underTest.player)
+            val mediaData =
+                initMediaData(ARTIST, TITLE)
+                    .copy(semanticActions = MediaButton(reserveNext = true, reservePrev = true))
+
+            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
+
+            assertThat(playerModel?.actionButtons).isNotNull()
+            assertThat(playerModel!!.useSemanticActions).isTrue()
+            assertThat(playerModel!!.canShowTime).isTrue()
+
+            val buttons = playerModel!!.actionButtons
+
+            val prevButton = buttons.find { it.buttonId == R.id.actionPrev }!!
+            assertThat(prevButton.notVisibleValue).isEqualTo(ConstraintSet.GONE)
+            assertThat(prevButton.isVisibleWhenScrubbing).isEqualTo(false)
+
+            val nextButton = buttons.find { it.buttonId == R.id.actionNext }!!
+            assertThat(nextButton.notVisibleValue).isEqualTo(ConstraintSet.GONE)
+            assertThat(nextButton.isVisibleWhenScrubbing).isEqualTo(false)
+        }
+
     private fun initMediaData(artist: String, title: String): MediaData {
         val device = MediaDeviceData(true, null, DEVICE_NAME, null, showBroadcastButton = true)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
index c5a2370..6546a50 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
@@ -35,9 +35,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.truth.correspondence.FakeUiEvent
 import com.android.systemui.truth.correspondence.LogMaker
-import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.nullable
 import com.google.common.truth.Truth.assertThat
@@ -45,8 +43,11 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -92,9 +93,10 @@
         // Dialog is shown.
         verify(globalActionsDialogLite)
             .showOrHideDialog(
-                /* keyguardShowing= */ false,
-                /* isDeviceProvisioned= */ true,
-                expandable,
+                /* keyguardShowing= */ eq(false),
+                /* isDeviceProvisioned= */ eq(true),
+                eq(expandable),
+                anyInt(),
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
index f86337e..396f531 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
@@ -20,7 +20,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags.FLAG_EXAMPLE_FLAG
-import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+import com.android.systemui.Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN
 import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.andSceneContainer
@@ -66,7 +66,7 @@
 
     @Test
     fun oneDependencyAndSceneContainer() {
-        val dependentFlag = FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+        val dependentFlag = FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN
         val result = FlagsParameterization.allCombinationsOf(dependentFlag).andSceneContainer()
         Truth.assertThat(result).hasSize(3)
         Truth.assertThat(result[0].mOverrides[dependentFlag]).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 0d8d57e..d3b5828 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -68,7 +68,6 @@
 import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.LatencyTracker;
-import com.android.keyguard.EmptyLockIconViewController;
 import com.android.keyguard.KeyguardClockSwitch;
 import com.android.keyguard.KeyguardClockSwitchController;
 import com.android.keyguard.KeyguardSliceViewController;
@@ -99,7 +98,6 @@
 import com.android.systemui.keyguard.KeyguardViewConfigurator;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
@@ -108,7 +106,6 @@
 import com.android.systemui.keyguard.ui.view.KeyguardRootView;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
@@ -167,8 +164,6 @@
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardClockPositionAlgorithm;
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
@@ -228,10 +223,7 @@
 
     @Mock protected CentralSurfaces mCentralSurfaces;
     @Mock protected NotificationStackScrollLayout mNotificationStackScrollLayout;
-    @Mock protected KeyguardBottomAreaView mKeyguardBottomArea;
-    @Mock protected KeyguardBottomAreaViewController mKeyguardBottomAreaViewController;
     @Mock protected ViewPropertyAnimator mViewPropertyAnimator;
-    @Mock protected KeyguardBottomAreaView mQsFrame;
     @Mock protected HeadsUpManager mHeadsUpManager;
     @Mock protected NotificationGutsManager mGutsManager;
     @Mock protected KeyguardStatusBarView mKeyguardStatusBar;
@@ -270,7 +262,6 @@
     @Mock protected KeyguardUserSwitcherController mKeyguardUserSwitcherController;
     @Mock protected KeyguardStatusViewComponent mKeyguardStatusViewComponent;
     @Mock protected KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory;
-    @Mock protected EmptyLockIconViewController mLockIconViewController;
     @Mock protected KeyguardStatusBarViewComponent mKeyguardStatusBarViewComponent;
     @Mock protected KeyguardClockSwitchController mKeyguardClockSwitchController;
     @Mock protected KeyguardStatusBarViewController mKeyguardStatusBarViewController;
@@ -317,7 +308,6 @@
     @Mock protected ViewGroup mQsHeader;
     @Mock protected ViewParent mViewParent;
     @Mock protected ViewTreeObserver mViewTreeObserver;
-    @Mock protected KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     @Mock protected DreamingToLockscreenTransitionViewModel
             mDreamingToLockscreenTransitionViewModel;
     @Mock protected OccludedToLockscreenTransitionViewModel
@@ -352,7 +342,6 @@
     @Mock private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector;
     protected final int mMaxUdfpsBurnInOffsetY = 5;
     protected FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic();
-    protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
     protected KeyguardClockInteractor mKeyguardClockInteractor;
     protected FakeKeyguardRepository mFakeKeyguardRepository;
     protected FakeKeyguardClockRepository mFakeKeyguardClockRepository;
@@ -397,13 +386,10 @@
         mFeatureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false);
         mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false);
 
-        mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR);
-
         mMainDispatcher = getMainDispatcher();
         KeyguardInteractorFactory.WithDependencies keyguardInteractorDeps =
                 KeyguardInteractorFactory.create();
         mFakeKeyguardRepository = keyguardInteractorDeps.getRepository();
-        mKeyguardBottomAreaInteractor = new KeyguardBottomAreaInteractor(mFakeKeyguardRepository);
         mFakeKeyguardClockRepository = new FakeKeyguardClockRepository();
         mKeyguardClockInteractor = mKosmos.getKeyguardClockInteractor();
         mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor();
@@ -500,9 +486,6 @@
         when(mNotificationStackScrollLayoutController.getHeight()).thenReturn(1000);
         when(mNotificationStackScrollLayoutController.getHeadsUpCallback())
                 .thenReturn(mHeadsUpCallback);
-        when(mKeyguardBottomAreaViewController.getView()).thenReturn(mKeyguardBottomArea);
-        when(mView.findViewById(R.id.keyguard_bottom_area)).thenReturn(mKeyguardBottomArea);
-        when(mKeyguardBottomArea.animate()).thenReturn(mViewPropertyAnimator);
         when(mView.animate()).thenReturn(mViewPropertyAnimator);
         when(mKeyguardStatusView.animate()).thenReturn(mViewPropertyAnimator);
         when(mViewPropertyAnimator.translationX(anyFloat())).thenReturn(mViewPropertyAnimator);
@@ -513,7 +496,6 @@
         when(mViewPropertyAnimator.setListener(any())).thenReturn(mViewPropertyAnimator);
         when(mViewPropertyAnimator.setUpdateListener(any())).thenReturn(mViewPropertyAnimator);
         when(mViewPropertyAnimator.withEndAction(any())).thenReturn(mViewPropertyAnimator);
-        when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame);
         when(mView.findViewById(R.id.keyguard_status_view))
                 .thenReturn(mock(KeyguardStatusView.class));
         ViewGroup rootView = mock(ViewGroup.class);
@@ -647,8 +629,6 @@
                 .thenReturn(keyguardStatusView);
         when(mLayoutInflater.inflate(eq(R.layout.keyguard_user_switcher), any(), anyBoolean()))
                 .thenReturn(mUserSwitcherView);
-        when(mLayoutInflater.inflate(eq(R.layout.keyguard_bottom_area), any(), anyBoolean()))
-                .thenReturn(mKeyguardBottomArea);
         when(mNotificationRemoteInputManager.isRemoteInputActive())
                 .thenReturn(false);
         doAnswer(invocation -> {
@@ -720,7 +700,6 @@
                 mMediaDataManager,
                 mNotificationShadeDepthController,
                 mAmbientState,
-                mLockIconViewController,
                 mKeyguardMediaController,
                 mTapAgainViewController,
                 mNavigationModeController,
@@ -736,15 +715,12 @@
                 mShadeRepository,
                 mSysUIUnfoldComponent,
                 mSysUiState,
-                () -> mKeyguardBottomAreaViewController,
                 mKeyguardUnlockAnimationController,
                 mKeyguardIndicationController,
                 mNotificationListContainer,
                 mNotificationStackSizeCalculator,
                 mUnlockedScreenOffAnimationController,
                 systemClock,
-                mKeyguardBottomAreaViewModel,
-                mKeyguardBottomAreaInteractor,
                 mKeyguardClockInteractor,
                 mAlternateBouncerInteractor,
                 mDreamingToLockscreenTransitionViewModel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 97441f0..5289554 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -28,7 +28,6 @@
 import com.android.internal.util.CollectionUtils
 import com.android.keyguard.KeyguardClockSwitch.LARGE
 import com.android.systemui.Flags
-import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
 import com.android.systemui.statusbar.StatusBarState.SHADE
@@ -215,31 +214,4 @@
         }
         advanceUntilIdle()
     }
-
-    @Test
-    fun onLayoutChange_shadeCollapsed_bottomAreaAlphaIsZero() = runTest {
-        // GIVEN bottomAreaShadeAlpha was updated before
-        mNotificationPanelViewController.maybeAnimateBottomAreaAlpha()
-
-        // WHEN a layout change is triggered with the shade being closed
-        triggerLayoutChange()
-
-        // THEN the bottomAreaAlpha is zero
-        val bottomAreaAlpha by collectLastValue(mFakeKeyguardRepository.bottomAreaAlpha)
-        assertThat(bottomAreaAlpha).isEqualTo(0f)
-    }
-
-    @Test
-    fun onShadeExpanded_bottomAreaAlphaIsFullyOpaque() = runTest {
-        // GIVEN bottomAreaShadeAlpha was updated before
-        mNotificationPanelViewController.maybeAnimateBottomAreaAlpha()
-
-        // WHEN the shade expanded
-        val transitionDistance = mNotificationPanelViewController.maxPanelTransitionDistance
-        mNotificationPanelViewController.expandedHeight = transitionDistance.toFloat()
-
-        // THEN the bottomAreaAlpha is fully opaque
-        val bottomAreaAlpha by collectLastValue(mFakeKeyguardRepository.bottomAreaAlpha)
-        assertThat(bottomAreaAlpha).isEqualTo(1f)
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 5d1ce7c5..929537dc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -253,6 +253,16 @@
         verify(configurationForwarder).onConfigurationChanged(eq(config))
     }
 
+    @Test
+    @EnableFlags(AConfigFlags.FLAG_SHADE_WINDOW_GOES_AROUND)
+    fun onMovedToDisplay_configForwarderSet_propagatesConfig() {
+        val config = Configuration()
+
+        underTest.onMovedToDisplay(1, config)
+
+        verify(configurationForwarder).dispatchOnMovedToDisplay(eq(1), eq(config))
+    }
+
     private fun captureInteractionEventHandler() {
         verify(underTest).setInteractionEventHandler(interactionEventHandlerCaptor.capture())
         interactionEventHandler = interactionEventHandlerCaptor.value
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
index 0966759..007a0fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
@@ -16,17 +16,20 @@
 
 package com.android.systemui.shade.data.repository
 
+import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS
 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.coroutines.collectValues
+import com.android.systemui.display.data.repository.displayRepository
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.display.ShadeDisplayPolicy
-import com.android.systemui.shade.display.SpecificDisplayIdPolicy
+import com.android.systemui.shade.display.AnyExternalShadeDisplayPolicy
+import com.android.systemui.shade.display.DefaultDisplayShadePolicy
+import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy
 import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.fakeGlobalSettings
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,54 +39,72 @@
 class ShadeDisplaysRepositoryTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val defaultPolicy = SpecificDisplayIdPolicy(0)
+    private val globalSettings = kosmos.fakeGlobalSettings
+    private val displayRepository = kosmos.displayRepository
+    private val defaultPolicy = DefaultDisplayShadePolicy()
+    private val policies = kosmos.shadeDisplayPolicies
 
-    private val shadeDisplaysRepository =
-        ShadeDisplaysRepositoryImpl(defaultPolicy, testScope.backgroundScope)
+    private val underTest =
+        ShadeDisplaysRepositoryImpl(
+            globalSettings,
+            defaultPolicy,
+            testScope.backgroundScope,
+            policies,
+        )
 
     @Test
     fun policy_changing_propagatedFromTheLatestPolicy() =
         testScope.runTest {
-            val displayIds by collectValues(shadeDisplaysRepository.displayId)
-            val policy1 = MutablePolicy()
-            val policy2 = MutablePolicy()
+            val displayIds by collectValues(underTest.displayId)
 
             assertThat(displayIds).containsExactly(0)
 
-            shadeDisplaysRepository.policy.value = policy1
+            globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "any_external_display")
 
-            policy1.sendDisplayId(1)
+            displayRepository.addDisplay(displayId = 1)
 
             assertThat(displayIds).containsExactly(0, 1)
 
-            policy1.sendDisplayId(2)
+            displayRepository.addDisplay(displayId = 2)
+
+            assertThat(displayIds).containsExactly(0, 1)
+
+            displayRepository.removeDisplay(displayId = 1)
 
             assertThat(displayIds).containsExactly(0, 1, 2)
 
-            shadeDisplaysRepository.policy.value = policy2
+            globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "default_display")
 
             assertThat(displayIds).containsExactly(0, 1, 2, 0)
-
-            policy1.sendDisplayId(4)
-
-            // Changes to the first policy don't affect the output now
-            assertThat(displayIds).containsExactly(0, 1, 2, 0)
-
-            policy2.sendDisplayId(5)
-
-            assertThat(displayIds).containsExactly(0, 1, 2, 0, 5)
         }
 
-    private class MutablePolicy : ShadeDisplayPolicy {
-        fun sendDisplayId(id: Int) {
-            _displayId.value = id
+    @Test
+    fun policy_updatesBasedOnSettingValue_defaultDisplay() =
+        testScope.runTest {
+            val policy by collectLastValue(underTest.policy)
+
+            globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "default_display")
+
+            assertThat(policy).isInstanceOf(DefaultDisplayShadePolicy::class.java)
         }
 
-        private val _displayId = MutableStateFlow(0)
-        override val name: String
-            get() = "mutable_policy"
+    @Test
+    fun policy_updatesBasedOnSettingValue_anyExternal() =
+        testScope.runTest {
+            val policy by collectLastValue(underTest.policy)
 
-        override val displayId: StateFlow<Int>
-            get() = _displayId
-    }
+            globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "any_external_display")
+
+            assertThat(policy).isInstanceOf(AnyExternalShadeDisplayPolicy::class.java)
+        }
+
+    @Test
+    fun policy_updatesBasedOnSettingValue_focusBased() =
+        testScope.runTest {
+            val policy by collectLastValue(underTest.policy)
+
+            globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "status_bar_latest_touch")
+
+            assertThat(policy).isInstanceOf(StatusBarTouchShadeDisplayPolicy::class.java)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
index d584dc9..eeb3e6b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.shade.display.ShadeDisplayPolicy
 import com.android.systemui.statusbar.commandline.commandRegistry
 import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.fakeGlobalSettings
 import com.google.common.truth.StringSubject
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
@@ -44,18 +45,17 @@
 class ShadePrimaryDisplayCommandTest : SysuiTestCase() {
     private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
+    private val globalSettings = kosmos.fakeGlobalSettings
     private val commandRegistry = kosmos.commandRegistry
     private val displayRepository = kosmos.displayRepository
     private val defaultPolicy = kosmos.defaultShadeDisplayPolicy
-    private val policy1 = makePolicy("policy_1")
     private val shadeDisplaysRepository = kosmos.shadeDisplaysRepository
+    private val policies = kosmos.shadeDisplayPolicies
     private val pw = PrintWriter(StringWriter())
 
-    private val policies =
-        setOf(defaultPolicy, policy1, makePolicy("policy_2"), makePolicy("policy_3"))
-
     private val underTest =
         ShadePrimaryDisplayCommand(
+            globalSettings,
             commandRegistry,
             displayRepository,
             shadeDisplaysRepository,
@@ -69,30 +69,16 @@
     }
 
     @Test
-    fun commandDisplayOverride_updatesDisplayId() =
-        testScope.runTest {
-            val displayId by collectLastValue(shadeDisplaysRepository.displayId)
-            assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
-
-            val newDisplayId = 2
-            commandRegistry.onShellCommand(
-                pw,
-                arrayOf("shade_display_override", newDisplayId.toString()),
-            )
-
-            assertThat(displayId).isEqualTo(newDisplayId)
-        }
-
-    @Test
     fun commandShadeDisplayOverride_resetsDisplayId() =
         testScope.runTest {
             val displayId by collectLastValue(shadeDisplaysRepository.displayId)
             assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
 
             val newDisplayId = 2
+            displayRepository.addDisplay(displayId = newDisplayId)
             commandRegistry.onShellCommand(
                 pw,
-                arrayOf("shade_display_override", newDisplayId.toString()),
+                arrayOf("shade_display_override", "any_external_display"),
             )
             assertThat(displayId).isEqualTo(newDisplayId)
 
@@ -108,7 +94,10 @@
             val newDisplayId = 2
             displayRepository.addDisplay(displayId = newDisplayId)
 
-            commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", "any_external"))
+            commandRegistry.onShellCommand(
+                pw,
+                arrayOf("shade_display_override", "any_external_display"),
+            )
 
             assertThat(displayId).isEqualTo(newDisplayId)
         }
@@ -127,13 +116,14 @@
         }
 
     @Test
-    fun policies_setsSpecificPolicy() =
+    fun policies_setsNewPolicy() =
         testScope.runTest {
             val policy by collectLastValue(shadeDisplaysRepository.policy)
+            val newPolicy = policies.last().name
 
-            commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", policy1.name))
+            commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", newPolicy))
 
-            assertThat(policy!!.name).isEqualTo(policy1.name)
+            assertThat(policy!!.name).isEqualTo(newPolicy)
         }
 
     private fun makePolicy(policyName: String): ShadeDisplayPolicy {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 5b0b59d..4a3be44 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -31,6 +31,10 @@
 import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS;
 import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS;
 
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC;
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
@@ -280,8 +284,10 @@
 
         mBackgroundExecutor.runAllReady();
 
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mCurrentUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mSecondaryUserNotif));
     }
 
     @Test
@@ -357,7 +363,8 @@
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
 
         // THEN current user's notification is redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mCurrentUserNotif));
     }
 
     @Test
@@ -368,7 +375,8 @@
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
 
         // THEN current user's notification isn't redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mCurrentUserNotif));
     }
 
     @Test
@@ -385,7 +393,8 @@
                 .setChannel(channel)
                 .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build());
         // THEN the notification is redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mCurrentUserNotif));
     }
 
     @Test
@@ -399,7 +408,8 @@
                 .setChannel(null)
                 .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build());
         // THEN the notification is not redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mCurrentUserNotif));
     }
 
     @Test
@@ -410,7 +420,8 @@
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
 
         // THEN work profile notification is redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mWorkProfileNotif));
         assertFalse(mLockscreenUserManager.allowsManagedPrivateNotificationsInPublic());
     }
 
@@ -422,7 +433,8 @@
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
 
         // THEN work profile notification isn't redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mWorkProfileNotif));
         assertTrue(mLockscreenUserManager.allowsManagedPrivateNotificationsInPublic());
     }
 
@@ -440,11 +452,14 @@
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
 
         // THEN the work profile notification doesn't need to be redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mWorkProfileNotif));
 
         // THEN the current user and secondary user notifications do need to be redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mCurrentUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mSecondaryUserNotif));
     }
 
     @Test
@@ -461,11 +476,14 @@
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
 
         // THEN the work profile notification needs to be redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mWorkProfileNotif));
 
         // THEN the current user and secondary user notifications don't need to be redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-        assertFalse(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mCurrentUserNotif));
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mSecondaryUserNotif));
     }
 
     @Test
@@ -481,18 +499,20 @@
 
         // THEN the secondary profile notification still needs to be redacted because the current
         // user's setting takes precedence
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mSecondaryUserNotif));
     }
 
     @Test
     public void testHasSensitiveContent_redacted() {
         // Allow private notifications for this user
-        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
+        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
                 mCurrentUser.id);
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
 
         // Sensitive Content notifications are always redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mSensitiveContentNotif));
+        assertEquals(REDACTION_TYPE_SENSITIVE_CONTENT,
+                mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
     }
 
     @Test
@@ -707,9 +727,11 @@
                 new Intent(ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED)
                         .putExtra(EXTRA_KM_PRIVATE_NOTIFS_ALLOWED, true));
 
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mCurrentUserNotif));
         // it's a global field, confirm secondary too
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mSecondaryUserNotif));
         assertFalse(mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(mCurrentUser.id));
         assertFalse(mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(
                 mSecondaryUser.id));
@@ -732,7 +754,8 @@
         mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext,
                 new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
 
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mCurrentUserNotif));
 
         verify(mDevicePolicyManager, atMost(1)).getKeyguardDisabledFeatures(any(), anyInt());
     }
@@ -763,7 +786,7 @@
         mLockscreenUserManager.mAllUsersReceiver.onReceive(mContext,
                 new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
 
-        assertTrue(mLockscreenUserManager.needsRedaction(notifEntry));
+        assertEquals(REDACTION_TYPE_PUBLIC, mLockscreenUserManager.getRedactionType(notifEntry));
     }
 
     @Test
@@ -784,7 +807,8 @@
                 new Intent(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
 
         mLockscreenUserManager.mUserChangedCallback.onUserChanging(mSecondaryUser.id, mContext);
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
+        assertEquals(REDACTION_TYPE_PUBLIC,
+                mLockscreenUserManager.getRedactionType(mSecondaryUserNotif));
 
         verify(mDevicePolicyManager, atMost(1)).getKeyguardDisabledFeatures(any(), anyInt());
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index 63efc55..9ad1f40 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
@@ -50,7 +49,6 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = icon,
-                    whenTime = 5432,
                     promotedContent = PROMOTED_CONTENT,
                 )
 
@@ -60,7 +58,7 @@
 
             assertThat(latest!!.key).isEqualTo("notif1")
             assertThat(latest!!.statusBarChipIconView).isEqualTo(icon)
-            assertThat(latest!!.whenTime).isEqualTo(5432)
+            assertThat(latest!!.promotedContent).isEqualTo(PROMOTED_CONTENT)
         }
 
     @Test
@@ -83,14 +81,12 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = newIconView,
-                    whenTime = 6543,
                     promotedContent = PROMOTED_CONTENT,
                 )
             )
 
             assertThat(latest!!.key).isEqualTo("notif1")
             assertThat(latest!!.statusBarChipIconView).isEqualTo(newIconView)
-            assertThat(latest!!.whenTime).isEqualTo(6543)
         }
 
     @Test
@@ -174,22 +170,14 @@
                     activeNotificationModel(
                         key = "notif1",
                         statusBarChipIcon = null,
-                        whenTime = 123L,
                         promotedContent = PROMOTED_CONTENT,
                     )
                 )
 
             val latest by collectLastValue(underTest.notificationChip)
 
-            assertThat(latest)
-                .isEqualTo(
-                    NotificationChipModel(
-                        "notif1",
-                        statusBarChipIconView = null,
-                        whenTime = 123L,
-                        promotedContent = PROMOTED_CONTENT,
-                    )
-                )
+            assertThat(latest).isNotNull()
+            assertThat(latest!!.key).isEqualTo("notif1")
         }
 
     @Test
@@ -234,20 +222,12 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = null,
-                    whenTime = 123L,
                     promotedContent = PROMOTED_CONTENT,
                 )
             )
 
-            assertThat(latest)
-                .isEqualTo(
-                    NotificationChipModel(
-                        key = "notif1",
-                        statusBarChipIconView = null,
-                        whenTime = 123L,
-                        promotedContent = PROMOTED_CONTENT,
-                    )
-                )
+            assertThat(latest).isNotNull()
+            assertThat(latest!!.key).isEqualTo("notif1")
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index d4910ce..165e943 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -116,7 +116,7 @@
 
             assertThat(latest).hasSize(1)
             val chip = latest!![0]
-            assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java)
+            assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             assertThat(chip.icon).isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(icon))
         }
 
@@ -139,7 +139,7 @@
 
             assertThat(latest).hasSize(1)
             val chip = latest!![0]
-            assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java)
+            assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             assertThat(chip.icon)
                 .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(notifKey))
         }
@@ -242,15 +242,158 @@
         }
 
     @Test
-    fun chips_noHeadsUp_showsTime() =
+    fun chips_hasShortCriticalText_usesTextInsteadOfTime() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.shortCriticalText = "Arrived"
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = 6543L,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
             setNotifs(
                 listOf(
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = mock<StatusBarIconView>(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Shown.Text::class.java)
+            assertThat((latest!![0] as OngoingActivityChipModel.Shown.Text).text)
+                .isEqualTo("Arrived")
+        }
+
+    @Test
+    fun chips_noTime_isIconOnly() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply { this.time = null }
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0])
+                .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
+        }
+
+    @Test
+    fun chips_basicTime_isShortTimeDelta() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = 6543L,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0])
+                .isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java)
+        }
+
+    @Test
+    fun chips_countUpTime_isTimer() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = 6543L,
+                            mode = PromotedNotificationContentModel.When.Mode.CountUp,
+                        )
+                }
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
+        }
+
+    @Test
+    fun chips_countDownTime_isTimer() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = 6543L,
+                            mode = PromotedNotificationContentModel.When.Mode.CountDown,
+                        )
+                }
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
+        }
+
+    @Test
+    fun chips_noHeadsUp_showsTime() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = 6543L,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        promotedContent = promotedContentBuilder.build(),
                     )
                 )
             )
@@ -267,12 +410,21 @@
     fun chips_hasHeadsUpByUser_onlyShowsIcon() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = 6543L,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
             setNotifs(
                 listOf(
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = mock<StatusBarIconView>(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = promotedContentBuilder.build(),
                     )
                 )
             )
@@ -324,15 +476,11 @@
 
     companion object {
         fun assertIsNotifChip(latest: OngoingActivityChipModel?, expectedIcon: StatusBarIconView) {
-            assertThat(latest)
-                .isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java)
             assertThat((latest as OngoingActivityChipModel.Shown).icon)
                 .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon))
         }
 
         fun assertIsNotifKey(latest: OngoingActivityChipModel?, expectedKey: String) {
-            assertThat(latest)
-                .isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java)
             assertThat((latest as OngoingActivityChipModel.Shown).icon)
                 .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(expectedKey))
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
index 9e7befd..8f21ddff 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
@@ -50,6 +50,8 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC
 import com.android.systemui.statusbar.RankingBuilder
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -840,7 +842,13 @@
                 whenever(sbn).thenReturn(mockSbn)
                 whenever(row).thenReturn(mockRow)
             }
-        whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction)
+        val redactionType =
+            if (needsRedaction) {
+                REDACTION_TYPE_PUBLIC
+            } else {
+                REDACTION_TYPE_NONE
+            }
+        whenever(lockscreenUserManager.getRedactionType(mockEntry)).thenReturn(redactionType)
         whenever(mockEntry.rowExists()).thenReturn(true)
         return object : ListEntry("key", 0) {
             override fun getRepresentativeEntry(): NotificationEntry = mockEntry
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
index 2f77b33..3c772fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
@@ -27,6 +27,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
@@ -42,6 +44,7 @@
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 import org.junit.Before
@@ -212,12 +215,12 @@
         whenever(sensitiveNotifProtectionController.shouldProtectNotification(entry))
             .thenReturn(false)
         val oldAdjustment: NotifUiAdjustment = adjustmentProvider.calculateAdjustment(entry)
-        assertFalse(oldAdjustment.needsRedaction)
+        assertEquals(REDACTION_TYPE_NONE, oldAdjustment.redactionType)
 
         whenever(sensitiveNotifProtectionController.shouldProtectNotification(entry))
             .thenReturn(true)
         val newAdjustment = adjustmentProvider.calculateAdjustment(entry)
-        assertTrue(newAdjustment.needsRedaction)
+        assertEquals(REDACTION_TYPE_PUBLIC, newAdjustment.redactionType)
 
         // Then: need re-inflation
         assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment))
@@ -229,12 +232,12 @@
         whenever(sensitiveNotifProtectionController.shouldProtectNotification(entry))
             .thenReturn(false)
         val oldAdjustment = adjustmentProvider.calculateAdjustment(entry)
-        assertFalse(oldAdjustment.needsRedaction)
+        assertEquals(REDACTION_TYPE_NONE, oldAdjustment.redactionType)
 
         whenever(sensitiveNotifProtectionController.shouldProtectNotification(entry))
             .thenReturn(true)
         val newAdjustment = adjustmentProvider.calculateAdjustment(entry)
-        assertFalse(newAdjustment.needsRedaction)
+        assertEquals(REDACTION_TYPE_NONE, newAdjustment.redactionType)
 
         // Then: need no re-inflation
         assertFalse(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index 98bf0e6..739a9c9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -45,7 +45,6 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorTest.kt
index 6736ccf..abd0a28 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorTest.kt
@@ -101,6 +101,7 @@
                     setSubText(TEST_SUB_TEXT)
                     setContentTitle(TEST_CONTENT_TITLE)
                     setContentText(TEST_CONTENT_TEXT)
+                    setShortCriticalText(TEST_SHORT_CRITICAL_TEXT)
                 }
                 .also { provider.promotedEntries.add(it) }
 
@@ -114,6 +115,52 @@
 
     @Test
     @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
+    @DisableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
+    fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() {
+        val entry =
+            createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
+                .also { provider.promotedEntries.add(it) }
+
+        val content = extractContent(entry)
+
+        assertThat(content).isNotNull()
+        assertThat(content?.text).isNull()
+    }
+
+    @Test
+    @EnableFlags(
+        PromotedNotificationUi.FLAG_NAME,
+        StatusBarNotifChips.FLAG_NAME,
+        android.app.Flags.FLAG_API_RICH_ONGOING,
+    )
+    fun extractContent_apiFlagOn_shortCriticalTextExtracted() {
+        val entry =
+            createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
+                .also { provider.promotedEntries.add(it) }
+
+        val content = extractContent(entry)
+
+        assertThat(content).isNotNull()
+        assertThat(content?.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT)
+    }
+
+    @Test
+    @EnableFlags(
+        PromotedNotificationUi.FLAG_NAME,
+        StatusBarNotifChips.FLAG_NAME,
+        android.app.Flags.FLAG_API_RICH_ONGOING,
+    )
+    fun extractContent_noShortCriticalTextSet_textIsNull() {
+        val entry = createEntry {}.also { provider.promotedEntries.add(it) }
+
+        val content = extractContent(entry)
+
+        assertThat(content).isNotNull()
+        assertThat(content?.shortCriticalText).isNull()
+    }
+
+    @Test
+    @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
     fun extractContent_fromBigPictureStyle() {
         val entry =
             createEntry { setStyle(BigPictureStyle()) }.also { provider.promotedEntries.add(it) }
@@ -201,6 +248,7 @@
         private const val TEST_SUB_TEXT = "sub text"
         private const val TEST_CONTENT_TITLE = "content title"
         private const val TEST_CONTENT_TEXT = "content text"
+        private const val TEST_SHORT_CRITICAL_TEXT = "short"
 
         private const val TEST_PERSON_NAME = "person name"
         private const val TEST_PERSON_KEY = "person key"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
index 942ea65..e87077d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
@@ -21,11 +21,13 @@
 import android.content.res.Configuration.UI_MODE_NIGHT_YES
 import android.content.res.Configuration.UI_MODE_TYPE_CAR
 import android.os.LocaleList
+import android.view.Display
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import com.google.common.truth.Truth.assertThat
+import java.util.Locale
 import org.junit.Before
 import org.junit.Ignore
 import org.junit.Test
@@ -34,7 +36,6 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import java.util.Locale
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -64,9 +65,11 @@
         mConfigurationController.addCallback(listener2)
 
         doAnswer {
-            mConfigurationController.removeCallback(listener2)
-            null
-        }.`when`(listener).onThemeChanged()
+                mConfigurationController.removeCallback(listener2)
+                null
+            }
+            .`when`(listener)
+            .onThemeChanged()
 
         mConfigurationController.notifyThemeChanged()
         verify(listener).onThemeChanged()
@@ -208,7 +211,6 @@
         assertThat(listener.maxBoundsChanged).isTrue()
     }
 
-
     @Test
     fun localeListChanged_listenerNotified() {
         val config = mContext.resources.configuration
@@ -289,7 +291,6 @@
         assertThat(listener.orientationChanged).isTrue()
     }
 
-
     @Test
     fun multipleUpdates_listenerNotifiedOfAll() {
         val config = mContext.resources.configuration
@@ -313,6 +314,17 @@
     }
 
     @Test
+    fun onMovedToDisplay_dispatchedToChildren() {
+        val config = mContext.resources.configuration
+        val listener = createAndAddListener()
+
+        mConfigurationController.dispatchOnMovedToDisplay(newDisplayId = 1, config)
+
+        assertThat(listener.display).isEqualTo(1)
+        assertThat(listener.changedConfig).isEqualTo(config)
+    }
+
+    @Test
     @Ignore("b/261408895")
     fun equivalentConfigObject_listenerNotNotified() {
         val config = mContext.resources.configuration
@@ -343,35 +355,49 @@
         var localeListChanged = false
         var layoutDirectionChanged = false
         var orientationChanged = false
+        var display = Display.DEFAULT_DISPLAY
 
         override fun onConfigChanged(newConfig: Configuration?) {
             changedConfig = newConfig
         }
+
         override fun onDensityOrFontScaleChanged() {
             densityOrFontScaleChanged = true
         }
+
         override fun onSmallestScreenWidthChanged() {
             smallestScreenWidthChanged = true
         }
+
         override fun onMaxBoundsChanged() {
             maxBoundsChanged = true
         }
+
         override fun onUiModeChanged() {
             uiModeChanged = true
         }
+
         override fun onThemeChanged() {
             themeChanged = true
         }
+
         override fun onLocaleListChanged() {
             localeListChanged = true
         }
+
         override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
             layoutDirectionChanged = true
         }
+
         override fun onOrientationChanged(orientation: Int) {
             orientationChanged = true
         }
 
+        override fun onMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration?) {
+            display = newDisplayId
+            changedConfig = newConfiguration
+        }
+
         fun assertNoMethodsCalled() {
             assertThat(densityOrFontScaleChanged).isFalse()
             assertThat(smallestScreenWidthChanged).isFalse()
@@ -391,6 +417,7 @@
             themeChanged = false
             localeListChanged = false
             layoutDirectionChanged = false
+            display = Display.DEFAULT_DISPLAY
         }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index 79b5cc3..0652a83 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.BroadcastReceiver;
+import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Configuration;
@@ -120,6 +121,22 @@
     }
 
     @Test
+    public void testRegisterReceiverWithoutAcsd() {
+        SystemUIDialog dialog = createDialogWithDelegate(mContext, mDelegate,
+                false /* shouldAcsdDismissDialog */);
+        final ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        final ArgumentCaptor<IntentFilter> intentFilterCaptor =
+                ArgumentCaptor.forClass(IntentFilter.class);
+
+        dialog.show();
+        verify(mBroadcastDispatcher).registerReceiver(broadcastReceiverCaptor.capture(),
+                intentFilterCaptor.capture(), ArgumentMatchers.eq(null), ArgumentMatchers.any());
+        assertTrue(intentFilterCaptor.getValue().hasAction(Intent.ACTION_SCREEN_OFF));
+        assertFalse(intentFilterCaptor.getValue().hasAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+    }
+
+    @Test
     @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS)
     public void usePredictiveBackAnimFlag() {
         final SystemUIDialog dialog = new SystemUIDialog(mContext);
@@ -163,7 +180,8 @@
     public void delegateIsCalled_inCorrectOrder() {
         Configuration configuration = new Configuration();
         InOrder inOrder = Mockito.inOrder(mDelegate);
-        SystemUIDialog dialog = createDialogWithDelegate();
+        SystemUIDialog dialog = createDialogWithDelegate(mContext, mDelegate,
+                true /* shouldAcsdDismissDialog */);
 
         dialog.show();
         dialog.onWindowFocusChanged(/* hasFocus= */ true);
@@ -178,7 +196,8 @@
         inOrder.verify(mDelegate).onStop(dialog);
     }
 
-    private SystemUIDialog createDialogWithDelegate() {
+    private SystemUIDialog createDialogWithDelegate(Context context,
+            SystemUIDialog.Delegate delegate, boolean shouldAcsdDismissDialog) {
         SystemUIDialog.Factory factory = new SystemUIDialog.Factory(
                 getContext(),
                 Dependency.get(SystemUIDialogManager.class),
@@ -186,6 +205,6 @@
                 Dependency.get(BroadcastDispatcher.class),
                 Dependency.get(DialogTransitionAnimator.class)
         );
-        return factory.create(mDelegate);
+        return factory.create(delegate, context, shouldAcsdDismissDialog);
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index b19645f..8fb95e8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -40,9 +41,14 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.clearInvocations
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -51,6 +57,11 @@
     private val repository = kosmos.activeNotificationListRepository
     private val underTest = kosmos.ongoingCallInteractor
 
+    @Before
+    fun setUp() {
+        underTest.start()
+    }
+
     @Test
     fun noNotification_emitsNoCall() = runTest {
         val state by collectLastValue(underTest.ongoingCallState)
@@ -210,8 +221,7 @@
     @Test
     fun ongoingCallNotification_setsRequiresStatusBarVisibleTrue() =
         kosmos.runTest {
-            val ongoingCallState by collectLastValue(underTest.ongoingCallState)
-
+            val isStatusBarRequired by collectLastValue(underTest.isStatusBarRequiredForOngoingCall)
             val requiresStatusBarVisibleInRepository by
                 collectLastValue(
                     kosmos.fakeStatusBarModeRepository.defaultDisplay
@@ -222,21 +232,9 @@
                     kosmos.fakeStatusBarWindowControllerStore.defaultDisplay
                         .ongoingProcessRequiresStatusBarVisible
                 )
-            repository.activeNotifications.value =
-                ActiveNotificationsStore.Builder()
-                    .apply {
-                        addIndividualNotif(
-                            activeNotificationModel(
-                                key = "notif1",
-                                whenTime = 1000L,
-                                callType = CallType.Ongoing,
-                                uid = UID,
-                            )
-                        )
-                    }
-                    .build()
+            postOngoingCallNotification()
 
-            assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
+            assertThat(isStatusBarRequired).isTrue()
             assertThat(requiresStatusBarVisibleInRepository).isTrue()
             assertThat(requiresStatusBarVisibleInWindowController).isTrue()
         }
@@ -244,8 +242,7 @@
     @Test
     fun notificationRemoved_setsRequiresStatusBarVisibleFalse() =
         kosmos.runTest {
-            val ongoingCallState by collectLastValue(underTest.ongoingCallState)
-
+            val isStatusBarRequired by collectLastValue(underTest.isStatusBarRequiredForOngoingCall)
             val requiresStatusBarVisibleInRepository by
                 collectLastValue(
                     kosmos.fakeStatusBarModeRepository.defaultDisplay
@@ -257,23 +254,11 @@
                         .ongoingProcessRequiresStatusBarVisible
                 )
 
-            repository.activeNotifications.value =
-                ActiveNotificationsStore.Builder()
-                    .apply {
-                        addIndividualNotif(
-                            activeNotificationModel(
-                                key = "notif1",
-                                whenTime = 1000L,
-                                callType = CallType.Ongoing,
-                                uid = UID,
-                            )
-                        )
-                    }
-                    .build()
+            postOngoingCallNotification()
 
             repository.activeNotifications.value = ActiveNotificationsStore()
 
-            assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.NoCall::class.java)
+            assertThat(isStatusBarRequired).isFalse()
             assertThat(requiresStatusBarVisibleInRepository).isFalse()
             assertThat(requiresStatusBarVisibleInWindowController).isFalse()
         }
@@ -295,19 +280,8 @@
                 )
 
             kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
-            repository.activeNotifications.value =
-                ActiveNotificationsStore.Builder()
-                    .apply {
-                        addIndividualNotif(
-                            activeNotificationModel(
-                                key = "notif1",
-                                whenTime = 1000L,
-                                callType = CallType.Ongoing,
-                                uid = UID,
-                            )
-                        )
-                    }
-                    .build()
+
+            postOngoingCallNotification()
 
             assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
             assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -321,6 +295,99 @@
             assertThat(requiresStatusBarVisibleInWindowController).isFalse()
         }
 
+    @Test
+    fun gestureHandler_inCall_notFullscreen_doesNotListen() =
+        kosmos.runTest {
+            val ongoingCallState by collectLastValue(underTest.ongoingCallState)
+
+            clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
+            // Set up notification but not in fullscreen
+            kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
+            postOngoingCallNotification()
+
+            assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
+            verify(kosmos.swipeStatusBarAwayGestureHandler, never())
+                .addOnGestureDetectedCallback(any(), any())
+        }
+
+    @Test
+    fun gestureHandler_inCall_fullscreen_addsListener() =
+        kosmos.runTest {
+            val isGestureListeningEnabled by collectLastValue(underTest.isGestureListeningEnabled)
+
+            // Set up notification and fullscreen mode
+            kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
+            postOngoingCallNotification()
+
+            assertThat(isGestureListeningEnabled).isTrue()
+            verify(kosmos.swipeStatusBarAwayGestureHandler)
+                .addOnGestureDetectedCallback(any(), any())
+        }
+
+    @Test
+    fun gestureHandler_inCall_fullscreen_chipSwiped_removesListener() =
+        kosmos.runTest {
+            val swipeAwayState by collectLastValue(underTest.isChipSwipedAway)
+
+            // Set up notification and fullscreen mode
+            kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
+            postOngoingCallNotification()
+
+            clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
+
+            underTest.onStatusBarSwiped()
+
+            assertThat(swipeAwayState).isTrue()
+            verify(kosmos.swipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(any())
+        }
+
+    @Test
+    fun chipSwipedAway_setsRequiresStatusBarVisibleFalse() =
+        kosmos.runTest {
+            val isStatusBarRequiredForOngoingCall by
+                collectLastValue(underTest.isStatusBarRequiredForOngoingCall)
+            val requiresStatusBarVisibleInRepository by
+                collectLastValue(
+                    kosmos.fakeStatusBarModeRepository.defaultDisplay
+                        .ongoingProcessRequiresStatusBarVisible
+                )
+            val requiresStatusBarVisibleInWindowController by
+                collectLastValue(
+                    kosmos.fakeStatusBarWindowControllerStore.defaultDisplay
+                        .ongoingProcessRequiresStatusBarVisible
+                )
+
+            // Start with an ongoing call (which should set status bar required)
+            postOngoingCallNotification()
+
+            assertThat(isStatusBarRequiredForOngoingCall).isTrue()
+            assertThat(requiresStatusBarVisibleInRepository).isTrue()
+            assertThat(requiresStatusBarVisibleInWindowController).isTrue()
+
+            // Swipe away the chip
+            underTest.onStatusBarSwiped()
+
+            // Verify status bar is no longer required
+            assertThat(requiresStatusBarVisibleInRepository).isFalse()
+            assertThat(requiresStatusBarVisibleInWindowController).isFalse()
+        }
+
+    private fun postOngoingCallNotification() {
+        repository.activeNotifications.value =
+            ActiveNotificationsStore.Builder()
+                .apply {
+                    addIndividualNotif(
+                        activeNotificationModel(
+                            key = "notif1",
+                            whenTime = 1000L,
+                            callType = CallType.Ongoing,
+                            uid = UID,
+                        )
+                    )
+                }
+                .build()
+    }
+
     companion object {
         private const val UID = 885
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt
new file mode 100644
index 0000000..2ad1124
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted
+import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState
+import com.android.systemui.touchpad.tutorial.ui.composable.toTutorialActionState
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class StateTransitionsTest : SysuiTestCase() {
+
+    companion object {
+        private const val START_MARKER = "startMarker"
+        private const val END_MARKER = "endMarker"
+        private const val SUCCESS_ANIMATION = 0
+    }
+
+    // needed to simulate caching last state as it's used to create new state
+    private var lastState: TutorialActionState = NotStarted
+
+    private fun GestureState.toTutorialActionState(): TutorialActionState {
+        val newState =
+            this.toGestureUiState(
+                    progressStartMarker = START_MARKER,
+                    progressEndMarker = END_MARKER,
+                    successAnimation = SUCCESS_ANIMATION,
+                )
+                .toTutorialActionState(lastState)
+        lastState = newState
+        return lastState
+    }
+
+    @Test
+    fun gestureStateProducesEquivalentTutorialActionStateInHappyPath() {
+        val happyPath =
+            listOf(
+                GestureState.NotStarted,
+                GestureState.InProgress(0f),
+                GestureState.InProgress(0.5f),
+                GestureState.InProgress(1f),
+                GestureState.Finished,
+            )
+
+        val resultingStates = mutableListOf<TutorialActionState>()
+        happyPath.forEach { resultingStates.add(it.toTutorialActionState()) }
+
+        assertThat(resultingStates)
+            .containsExactly(
+                NotStarted,
+                InProgress(0f, START_MARKER, END_MARKER),
+                InProgress(0.5f, START_MARKER, END_MARKER),
+                InProgress(1f, START_MARKER, END_MARKER),
+                Finished(SUCCESS_ANIMATION),
+            )
+            .inOrder()
+    }
+
+    @Test
+    fun gestureStateProducesEquivalentTutorialActionStateInErrorPath() {
+        val errorPath =
+            listOf(
+                GestureState.NotStarted,
+                GestureState.InProgress(0f),
+                GestureState.Error,
+                GestureState.InProgress(0.5f),
+                GestureState.InProgress(1f),
+                GestureState.Finished,
+            )
+
+        val resultingStates = mutableListOf<TutorialActionState>()
+        errorPath.forEach { resultingStates.add(it.toTutorialActionState()) }
+
+        assertThat(resultingStates)
+            .containsExactly(
+                NotStarted,
+                InProgress(0f, START_MARKER, END_MARKER),
+                Error,
+                InProgressAfterError(InProgress(0.5f, START_MARKER, END_MARKER)),
+                InProgressAfterError(InProgress(1f, START_MARKER, END_MARKER)),
+                Finished(SUCCESS_ANIMATION),
+            )
+            .inOrder()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt
index 8d0d172..e23348b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt
@@ -21,6 +21,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.testKosmos
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Error
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
@@ -56,9 +57,9 @@
     }
 
     @Test
-    fun doesntTriggerGestureFinished_onGestureSpeedTooSlow() {
+    fun triggersError_onGestureSpeedTooSlow() {
         velocityTracker.setVelocity(Velocity(SLOW))
-        assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = NotStarted)
+        assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = Error)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt
index 7a77b63..2fe37ae 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt
@@ -21,6 +21,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.testKosmos
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Error
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
@@ -57,9 +58,9 @@
     }
 
     @Test
-    fun doesntTriggerGestureFinished_onGestureSpeedTooHigh() {
+    fun triggersError_onGestureSpeedTooHigh() {
         velocityTracker.setVelocity(Velocity(FAST))
-        assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = NotStarted)
+        assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = Error)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureRecognizerTest.kt
index de41089..533665e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureRecognizerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerGestureRecognizerTest.kt
@@ -19,6 +19,7 @@
 import android.view.MotionEvent
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Error
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Finished
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
@@ -64,12 +65,12 @@
     }
 
     @Test
-    fun doesntTriggerGestureFinished_onGestureDistanceTooShort() {
-        assertStateAfterEvents(events = tooShortGesture, expectedState = NotStarted)
+    fun triggersGestureError_onGestureDistanceTooShort() {
+        assertStateAfterEvents(events = tooShortGesture, expectedState = Error)
     }
 
     @Test
-    fun doesntTriggerGestureFinished_onThreeFingersSwipeInOtherDirections() {
+    fun triggersGestureError_onThreeFingersSwipeInOtherDirections() {
         val allThreeFingerGestures =
             listOf(
                 ThreeFingerGesture.swipeUp(),
@@ -78,7 +79,7 @@
                 ThreeFingerGesture.swipeRight(),
             )
         val invalidGestures = allThreeFingerGestures.filter { it.differentFromAnyOf(validGestures) }
-        invalidGestures.forEach { assertStateAfterEvents(events = it, expectedState = NotStarted) }
+        invalidGestures.forEach { assertStateAfterEvents(events = it, expectedState = Error) }
     }
 
     @Test
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt
index fb5ef02..843afb8 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt
@@ -115,14 +115,25 @@
                 // and we're not planning to add this vide in clockHostView
                 // so we only need position of device entry icon to constrain clock
                 // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection
-                val bottomPaddingPx = getDimen(context, "lock_icon_margin_bottom")
-                val defaultDensity =
-                    DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
-                        DisplayMetrics.DENSITY_DEFAULT.toFloat()
-                val lockIconRadiusPx = (defaultDensity * 36).toInt()
-                val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx
+                clockPreviewConfig.lockId?.let { lockId ->
+                    connect(lockscreenClockViewLargeId, BOTTOM, lockId, TOP)
+                }
+                    ?: run {
+                        val bottomPaddingPx = getDimen(context, "lock_icon_margin_bottom")
+                        val defaultDensity =
+                            DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
+                                DisplayMetrics.DENSITY_DEFAULT.toFloat()
+                        val lockIconRadiusPx = (defaultDensity * 36).toInt()
+                        val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx
+                        connect(
+                            lockscreenClockViewLargeId,
+                            BOTTOM,
+                            PARENT_ID,
+                            BOTTOM,
+                            clockBottomMargin,
+                        )
+                    }
 
-                connect(lockscreenClockViewLargeId, BOTTOM, PARENT_ID, BOTTOM, clockBottomMargin)
                 val smallClockViewId = getId(context, "lockscreen_clock_view")
                 constrainWidth(smallClockViewId, WRAP_CONTENT)
                 constrainHeight(smallClockViewId, getDimen(context, "small_clock_height"))
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPreviewConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPreviewConfig.kt
index 544b705..94e669f 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPreviewConfig.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPreviewConfig.kt
@@ -22,4 +22,5 @@
     val previewContext: Context,
     val isShadeLayoutWide: Boolean,
     val isSceneContainerFlagEnabled: Boolean = false,
+    val lockId: Int? = null,
 )
diff --git a/packages/SystemUI/res/layout/ongoing_activity_chip.xml b/packages/SystemUI/res/layout/ongoing_activity_chip.xml
index 215e4e4..7745af9 100644
--- a/packages/SystemUI/res/layout/ongoing_activity_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_activity_chip.xml
@@ -49,27 +49,25 @@
              ongoing_activity_chip_short_time_delta] will ever be shown at one time. -->
 
         <!-- Shows a timer, like 00:01. -->
+        <!-- Don't use the LimitedWidth style for the timer because the end of the timer is often
+             the most important value. ChipChronometer has the correct logic for when the timer is
+             too large for the space allowed. -->
         <com.android.systemui.statusbar.chips.ui.view.ChipChronometer
             android:id="@+id/ongoing_activity_chip_time"
             style="@style/StatusBar.Chip.Text"
         />
 
         <!-- Shows generic text. -->
-        <!-- Since there's so little room in the status bar chip area, don't ellipsize the text and
-             instead just fade it out a bit at the end. -->
         <TextView
             android:id="@+id/ongoing_activity_chip_text"
-            style="@style/StatusBar.Chip.Text"
-            android:ellipsize="none"
-            android:requiresFadingEdge="horizontal"
-            android:fadingEdgeLength="@dimen/ongoing_activity_chip_text_fading_edge_length"
+            style="@style/StatusBar.Chip.Text.LimitedWidth"
             android:visibility="gone"
             />
 
         <!-- Shows a time delta in short form, like "15min" or "1hr". -->
         <android.widget.DateTimeView
             android:id="@+id/ongoing_activity_chip_short_time_delta"
-            style="@style/StatusBar.Chip.Text"
+            style="@style/StatusBar.Chip.Text.LimitedWidth"
             android:visibility="gone"
             />
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 5894f29..35cfd08 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1760,6 +1760,7 @@
     <dimen name="wallet_button_vertical_padding">8dp</dimen>
 
     <!-- Ongoing activity chip -->
+    <dimen name="ongoing_activity_chip_max_text_width">74dp</dimen>
     <!-- The activity chip side padding, used with the default phone icon. -->
     <dimen name="ongoing_activity_chip_side_padding">12dp</dimen>
     <!-- The activity chip side padding, used with an icon that has embedded padding (e.g. if the icon comes from the notification's smallIcon field). If the icon has padding, the chip itself can have less padding. -->
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 88ed4e3..c06b078 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -215,27 +215,30 @@
     <item type="id" name="backlight_icon" />
 
     <!-- IDs for use in the keyguard/lockscreen scene -->
-    <item type="id" name="keyguard_root_view" />
-    <item type="id" name="keyguard_indication_area" />
-    <item type="id" name="keyguard_indication_text" />
-    <item type="id" name="keyguard_indication_text_bottom" />
-    <item type="id" name="nssl_guideline" />
-    <item type="id" name="nssl_placeholder" />
+    <item type="id" name="accessibility_actions_view" />
+    <item type="id" name="ambient_indication_container" />
     <item type="id" name="aod_notification_icon_container" />
-    <item type="id" name="split_shade_guideline" />
-    <item type="id" name="lock_icon" />
-    <item type="id" name="lock_icon_bg" />
     <item type="id" name="burn_in_layer" />
     <item type="id" name="burn_in_layer_empty_view" />
     <item type="id" name="communal_tutorial_indicator" />
+    <item type="id" name="end_button" />
+    <item type="id" name="lock_icon" />
+    <item type="id" name="lock_icon_bg" />
+    <item type="id" name="keyguard_indication_area" />
+    <item type="id" name="keyguard_indication_text" />
+    <item type="id" name="keyguard_indication_text_bottom" />
+    <item type="id" name="keyguard_root_view" />
+    <item type="id" name="keyguard_settings_button" />
+    <item type="id" name="nssl_guideline" />
+    <item type="id" name="nssl_placeholder" />
     <item type="id" name="nssl_placeholder_barrier_bottom" />
-    <item type="id" name="ambient_indication_container" />
-    <item type="id" name="status_view_media_container" />
-    <item type="id" name="smart_space_barrier_bottom" />
     <item type="id" name="small_clock_guideline_top" />
+    <item type="id" name="smart_space_barrier_bottom" />
+    <item type="id" name="split_shade_guideline" />
+    <item type="id" name="start_button" />
+    <item type="id" name="status_view_media_container" />
     <item type="id" name="weather_clock_date_and_icons_barrier_bottom" />
     <item type="id" name="weather_clock_bc_smartspace_bottom" />
-    <item type="id" name="accessibility_actions_view" />
 
     <!-- Privacy dialog -->
     <item type="id" name="privacy_dialog_close_app_button" />
@@ -280,7 +283,7 @@
     <item type="id" name="udfps_accessibility_overlay_top_guideline" />
 
     <!-- Ids for communal hub widgets -->
-    <item type="id" name="communal_widget_disposable_tag"/>
+    <item type="id" name="communal_widget_listener_tag"/>
 
     <!-- snapshot view-binding IDs -->
     <item type="id" name="snapshot_view_binding" />
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f927e26..56aaf4c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3932,6 +3932,8 @@
     <string name="touchpad_tutorial_recent_apps_gesture_button">View recent apps</string>
     <!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_tutorial_done_button">Done</string>
+    <!-- Screen title after gesture was not done correctly [CHAR LIMIT=NONE] -->
+    <string name="gesture_error_title">Try again!</string>
     <!-- BACK GESTURE -->
     <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_back_gesture_action_title">Go back</string>
@@ -3941,6 +3943,8 @@
     <string name="touchpad_back_gesture_success_title">Nice!</string>
     <!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_back_gesture_success_body">You completed the go back gesture.</string>
+    <!-- Text shown to the user after back gesture was not done correctly [CHAR LIMIT=NONE] -->
+    <string name="touchpad_back_gesture_error_body">To go back using your touchpad, swipe left or right using three fingers</string>
     <!-- HOME GESTURE -->
     <!-- Touchpad home gesture action name in tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_home_gesture_action_title">Go home</string>
@@ -3950,6 +3954,8 @@
     <string name="touchpad_home_gesture_success_title">Great job!</string>
     <!-- Text shown to the user after they complete home gesture tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_home_gesture_success_body">You completed the go home gesture</string>
+    <!-- Text shown to the user after home gesture was not done correctly [CHAR LIMIT=NONE] -->
+    <string name="touchpad_home_gesture_error_body">Swipe up with three fingers on your touchpad to go to your home screen</string>
     <!-- RECENT APPS GESTURE -->
     <!-- Touchpad recent apps gesture action name in tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_recent_apps_gesture_action_title">View recent apps</string>
@@ -3959,6 +3965,8 @@
     <string name="touchpad_recent_apps_gesture_success_title">Great job!</string>
     <!-- Text shown to the user after they complete recent apps gesture tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_recent_apps_gesture_success_body">You completed the view recent apps gesture.</string>
+    <!-- Text shown to the user after recent gesture was not done correctly [CHAR LIMIT=NONE] -->
+    <string name="touchpad_recent_gesture_error_body">To view recent apps, swipe up and hold using three fingers on your touchpad</string>
 
     <!-- KEYBOARD TUTORIAL-->
     <!-- Action key tutorial title [CHAR LIMIT=NONE] -->
@@ -3969,6 +3977,9 @@
     <string name="tutorial_action_key_success_title">Well done!</string>
     <!-- Text shown to the user after they complete action key tutorial [CHAR LIMIT=NONE] -->
     <string name="tutorial_action_key_success_body">You completed the view all apps gesture</string>
+    <!-- Text shown to the user after action key tutorial was not done correctly [CHAR LIMIT=NONE] -->
+    <string name="touchpad_action_key_error_body">Press the action key on your keyboard to view all of your apps</string>
+
     <!-- Content description for the animation playing during the tutorial. The user can click the animation to pause and unpause playback. [CHAR LIMIT=NONE] -->
     <string name="tutorial_animation_content_description">Tutorial animation, click to pause and resume play.</string>
 
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 12f6e69..3156a50 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -84,6 +84,16 @@
         <item name="android:textColor">?android:attr/colorPrimary</item>
     </style>
 
+    <!-- Style for a status bar chip text that has a maximum width. Since there's so little room in
+         the status bar chip area, don't ellipsize the text and instead just fade it out a bit at
+         the end. -->
+    <style name="StatusBar.Chip.Text.LimitedWidth">
+        <item name="android:ellipsize">none</item>
+        <item name="android:requiresFadingEdge">horizontal</item>
+        <item name="android:fadingEdgeLength">@dimen/ongoing_activity_chip_text_fading_edge_length</item>
+        <item name="android:maxWidth">@dimen/ongoing_activity_chip_max_text_width</item>
+    </style>
+
     <style name="Chipbar" />
 
     <style name="Chipbar.Text" parent="@*android:style/TextAppearance.DeviceDefault.Notification.Title">
diff --git a/packages/SystemUI/src/com/android/keyguard/EmptyLockIconViewController.kt b/packages/SystemUI/src/com/android/keyguard/EmptyLockIconViewController.kt
deleted file mode 100644
index 306d682..0000000
--- a/packages/SystemUI/src/com/android/keyguard/EmptyLockIconViewController.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.keyguard
-
-import android.view.MotionEvent
-import android.view.View
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.ui.view.KeyguardRootView
-import com.android.systemui.res.R
-import dagger.Lazy
-import javax.inject.Inject
-
-/**
- * Lock icon view logic now lives in DeviceEntryIconViewBinder and ViewModels. Icon is positioned in
- * [com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection].
- *
- * This class is to bridge the gap between the logic when the DeviceEntryUdfpsRefactor is enabled
- * and the KeyguardBottomAreaRefactor is NOT enabled. This class can and should be removed when both
- * flags are enabled.
- */
-@SysUISingleton
-class EmptyLockIconViewController
-@Inject
-constructor(private val keyguardRootView: Lazy<KeyguardRootView>) : LockIconViewController {
-    private val deviceEntryIconViewId = R.id.device_entry_icon_view
-
-    override fun setLockIconView(lockIconView: View) {
-        // no-op
-    }
-
-    override fun getTop(): Float {
-        return keyguardRootView.get().getViewById(deviceEntryIconViewId)?.top?.toFloat() ?: 0f
-    }
-
-    override fun getBottom(): Float {
-        return keyguardRootView.get().getViewById(deviceEntryIconViewId)?.bottom?.toFloat() ?: 0f
-    }
-
-    override fun dozeTimeTick() {
-        // no-op
-    }
-
-    override fun setAlpha(alpha: Float) {
-        // no-op
-    }
-
-    override fun willHandleTouchWhileDozing(event: MotionEvent): Boolean {
-        return false
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index ec0f582..0e1eccc 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -55,7 +55,6 @@
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
 import com.android.systemui.util.ViewController;
@@ -85,7 +84,6 @@
     private final DumpManager mDumpManager;
     private final ClockEventController mClockEventController;
     private final LogBuffer mLogBuffer;
-    private final NotificationIconContainerAlwaysOnDisplayViewBinder mNicViewBinder;
     private FrameLayout mSmallClockFrame; // top aligned clock
     private FrameLayout mLargeClockFrame; // centered clock
 
@@ -147,7 +145,6 @@
             ClockRegistry clockRegistry,
             KeyguardSliceViewController keyguardSliceViewController,
             LockscreenSmartspaceController smartspaceController,
-            NotificationIconContainerAlwaysOnDisplayViewBinder nicViewBinder,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             SecureSettings secureSettings,
             @Main DelayableExecutor uiExecutor,
@@ -163,7 +160,6 @@
         mClockRegistry = clockRegistry;
         mKeyguardSliceViewController = keyguardSliceViewController;
         mSmartspaceController = smartspaceController;
-        mNicViewBinder = nicViewBinder;
         mSecureSettings = secureSettings;
         mUiExecutor = uiExecutor;
         mBgExecutor = bgExecutor;
@@ -277,7 +273,6 @@
             hideSliceViewAndNotificationIconContainer();
             return;
         }
-        updateAodIcons();
         mStatusArea = mView.findViewById(R.id.keyguard_status_area);
 
         mBgExecutor.execute(() -> {
@@ -569,21 +564,6 @@
         return mLargeClockFrame.getVisibility() != View.VISIBLE;
     }
 
-    private void updateAodIcons() {
-        if (!MigrateClocksToBlueprint.isEnabled()) {
-            NotificationIconContainer nic = (NotificationIconContainer)
-                    mView.findViewById(
-                            com.android.systemui.res.R.id.left_aligned_notification_icon_container);
-            if (mAodIconsBindHandle != null) {
-                mAodIconsBindHandle.dispose();
-            }
-            if (nic != null) {
-                mAodIconsBindHandle = mNicViewBinder.bindWhileAttached(nic);
-                mAodIconContainer = nic;
-            }
-        }
-    }
-
     private void setClock(ClockController clock) {
         if (MigrateClocksToBlueprint.isEnabled()) {
             return;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 36afe1e..63d4fe3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -51,6 +51,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BlendMode;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.graphics.drawable.BitmapDrawable;
@@ -97,6 +98,7 @@
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.settingslib.Utils;
 import com.android.settingslib.drawable.CircleFramedDrawable;
+import com.android.systemui.Flags;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.classifier.FalsingA11yDelegate;
 import com.android.systemui.plugins.FalsingManager;
@@ -346,8 +348,7 @@
         setPadding(getPaddingLeft(), getPaddingTop() + getResources().getDimensionPixelSize(
                         R.dimen.keyguard_security_container_padding_top), getPaddingRight(),
                 getPaddingBottom());
-        setBackgroundColor(
-                getContext().getColor(com.android.internal.R.color.materialColorSurfaceDim));
+        reloadBackgroundColor();
     }
 
     void onResume(SecurityMode securityMode, boolean faceAuthEnabled) {
@@ -812,10 +813,20 @@
         mDisappearAnimRunning = false;
     }
 
+    private void reloadBackgroundColor() {
+        if (Flags.bouncerUiRevamp()) {
+            // Keep the background transparent, otherwise the background color looks like a box
+            // while scaling the bouncer for back animation or while transitioning to the bouncer.
+            setBackgroundColor(Color.TRANSPARENT);
+        } else {
+            setBackgroundColor(
+                    getContext().getColor(com.android.internal.R.color.materialColorSurfaceDim));
+        }
+    }
+
     void reloadColors() {
         mViewMode.reloadColors();
-        setBackgroundColor(getContext().getColor(
-                com.android.internal.R.color.materialColorSurfaceDim));
+        reloadBackgroundColor();
     }
 
     /** Handles density or font scale changes. */
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.kt b/packages/SystemUI/src/com/android/systemui/FontStyles.kt
similarity index 62%
rename from packages/SystemUI/src/com/android/keyguard/LockIconViewController.kt
rename to packages/SystemUI/src/com/android/systemui/FontStyles.kt
index c5012b0..d8cd6c87 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/FontStyles.kt
@@ -14,22 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.keyguard
+package com.android.systemui
 
-import android.view.MotionEvent
-import android.view.View
+/** String tokens for the different GSF font families. */
+object FontStyles {
 
-/** Controls the [LockIconView]. */
-interface LockIconViewController {
-    fun setLockIconView(lockIconView: View)
+    const val GSF_LABEL_MEDIUM = "gsf-label-medium"
+    const val GSF_LABEL_LARGE = "gsf-label-large"
 
-    fun getTop(): Float
+    const val GSF_BODY_MEDIUM = "gsf-body-medium"
 
-    fun getBottom(): Float
-
-    fun dozeTimeTick()
-
-    fun setAlpha(alpha: Float)
-
-    fun willHandleTouchWhileDozing(event: MotionEvent): Boolean
+    const val GSF_TITLE_SMALL_EMPHASIZED = "gsf-title-small-emphasized"
 }
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 1176cb0..c170557 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -52,6 +52,7 @@
 
 import com.android.app.animation.Interpolators;
 import com.android.systemui.DualToneHandler;
+import com.android.systemui.FontStyles;
 import com.android.systemui.battery.unified.BatteryColors;
 import com.android.systemui.battery.unified.BatteryDrawableState;
 import com.android.systemui.battery.unified.BatteryLayersDrawable;
@@ -387,7 +388,8 @@
         float fontHeight = mBatteryPercentView.getPaint().getFontMetricsInt(null);
         mBatteryPercentView.setLineHeight(TypedValue.COMPLEX_UNIT_PX, fontHeight);
         if (gsfQuickSettings()) {
-            mBatteryPercentView.setTypeface(Typeface.create("gsf-label-large", Typeface.NORMAL));
+            mBatteryPercentView.setTypeface(
+                    Typeface.create(FontStyles.GSF_LABEL_LARGE, Typeface.NORMAL));
         }
         if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
         addView(mBatteryPercentView, new LayoutParams(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index db4b0f2..54c52b5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -43,7 +43,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.airbnb.lottie.LottieAnimationView
 import com.airbnb.lottie.LottieCompositionFactory
-import com.android.systemui.Flags.bpIconA11y
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.biometrics.Utils.ellipsize
 import com.android.systemui.biometrics.shared.model.BiometricModalities
 import com.android.systemui.biometrics.shared.model.BiometricModality
@@ -63,7 +63,6 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 private const val TAG = "BiometricViewBinder"
 
@@ -327,31 +326,30 @@
 
                 // reuse the icon as a confirm button
                 launch {
-                    if (bpIconA11y()) {
-                        viewModel.isIconConfirmButton.collect { isButton ->
-                            if (isButton) {
-                                iconView.onTouchListener { _: View, event: MotionEvent ->
-                                    viewModel.onOverlayTouch(event)
-                                }
+                    viewModel.isIconConfirmButton.collect { isButton ->
+                        if (isButton && !accessibilityManager.isEnabled) {
+                            iconView.onTouchListener { _: View, event: MotionEvent ->
+                                viewModel.onOverlayTouch(event)
+                            }
+                        } else {
+                            iconView.setOnTouchListener(null)
+                        }
+                    }
+                }
+
+                launch {
+                    combine(viewModel.isIconConfirmButton, viewModel.isAuthenticated, ::Pair)
+                        .collect { (isIconConfirmButton, authState) ->
+                            // Only use the icon as a button for talkback when coex and pending
+                            // confirmation
+                            if (
+                                accessibilityManager.isEnabled &&
+                                    isIconConfirmButton &&
+                                    authState.isAuthenticated
+                            ) {
                                 iconView.setOnClickListener { viewModel.confirmAuthenticated() }
-                            } else {
-                                iconView.setOnTouchListener(null)
-                                iconView.setOnClickListener(null)
                             }
                         }
-                    } else {
-                        viewModel.isIconConfirmButton
-                            .map { isPending ->
-                                when {
-                                    isPending && modalities.hasFaceAndFingerprint ->
-                                        View.OnTouchListener { _: View, event: MotionEvent ->
-                                            viewModel.onOverlayTouch(event)
-                                        }
-                                    else -> null
-                                }
-                            }
-                            .collect { onTouch -> iconView.setOnTouchListener(onTouch) }
-                    }
                 }
 
                 // dismiss prompt when authenticated and confirmed
@@ -365,22 +363,8 @@
                             backgroundView.setOnClickListener(null)
                             backgroundView.importantForAccessibility =
                                 IMPORTANT_FOR_ACCESSIBILITY_NO
-
-                            // Allow icon to be used as confirmation button with udfps and a11y
-                            // enabled
-                            if (
-                                !bpIconA11y() &&
-                                    accessibilityManager.isTouchExplorationEnabled &&
-                                    modalities.hasUdfps
-                            ) {
-                                iconView.setOnClickListener { viewModel.confirmAuthenticated() }
-                            }
                         }
                         if (authState.isAuthenticatedAndConfirmed) {
-                            view.announceForAccessibility(
-                                view.resources.getString(R.string.biometric_dialog_authenticated)
-                            )
-
                             launch {
                                 delay(authState.delay)
                                 if (authState.isAuthenticatedAndExplicitlyConfirmed) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
index 574c40d..788c792 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
@@ -43,7 +43,7 @@
     enum class AuthType {
         Fingerprint,
         Face,
-        Coex
+        Coex,
     }
 
     /**
@@ -53,7 +53,7 @@
     val activeAuthType: Flow<AuthType> =
         combine(
             promptViewModel.modalities.distinctUntilChanged(),
-            promptViewModel.faceMode.distinctUntilChanged()
+            promptViewModel.faceMode.distinctUntilChanged(),
         ) { modalities, faceMode ->
             if (modalities.hasFaceAndFingerprint && !faceMode) {
                 AuthType.Coex
@@ -102,7 +102,7 @@
                         promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
-                        promptViewModel.showingError
+                        promptViewModel.showingError,
                     ) {
                         rotation: DisplayRotation,
                         isInRearDisplayMode: Boolean,
@@ -117,13 +117,13 @@
                                     isInRearDisplayMode,
                                     authState.isAuthenticated,
                                     isAuthenticating,
-                                    showingError
+                                    showingError,
                                 )
                             else ->
                                 getFingerprintIconViewAsset(
                                     authState.isAuthenticated,
                                     isAuthenticating,
-                                    showingError
+                                    showingError,
                                 )
                         }
                     }
@@ -132,7 +132,7 @@
                         promptViewModel.isAuthenticated.distinctUntilChanged(),
                         promptViewModel.isAuthenticating.distinctUntilChanged(),
                         promptViewModel.isPendingConfirmation.distinctUntilChanged(),
-                        promptViewModel.showingError.distinctUntilChanged()
+                        promptViewModel.showingError.distinctUntilChanged(),
                     ) {
                         authState: PromptAuthState,
                         isAuthenticating: Boolean,
@@ -142,7 +142,7 @@
                             authState,
                             isAuthenticating,
                             isPendingConfirmation,
-                            showingError
+                            showingError,
                         )
                     }
                 AuthType.Coex ->
@@ -170,14 +170,14 @@
                                     authState,
                                     isAuthenticating,
                                     isPendingConfirmation,
-                                    showingError
+                                    showingError,
                                 )
                             else ->
                                 getCoexIconViewAsset(
                                     authState,
                                     isAuthenticating,
                                     isPendingConfirmation,
-                                    showingError
+                                    showingError,
                                 )
                         }
                     }
@@ -187,7 +187,7 @@
     private fun getFingerprintIconViewAsset(
         isAuthenticated: Boolean,
         isAuthenticating: Boolean,
-        showingError: Boolean
+        showingError: Boolean,
     ): Int {
         return if (isAuthenticated) {
             if (_previousIconWasError.value) {
@@ -214,7 +214,7 @@
         isInRearDisplayMode: Boolean,
         isAuthenticated: Boolean,
         isAuthenticating: Boolean,
-        showingError: Boolean
+        showingError: Boolean,
     ): Int {
         return if (isAuthenticated) {
             if (_previousIconWasError.value) {
@@ -240,7 +240,7 @@
         authState: PromptAuthState,
         isAuthenticating: Boolean,
         isPendingConfirmation: Boolean,
-        showingError: Boolean
+        showingError: Boolean,
     ): Int {
         return if (authState.isAuthenticated && isPendingConfirmation) {
             R.raw.face_dialog_wink_from_dark
@@ -262,7 +262,7 @@
         authState: PromptAuthState,
         isAuthenticating: Boolean,
         isPendingConfirmation: Boolean,
-        showingError: Boolean
+        showingError: Boolean,
     ): Int {
         return if (authState.isAuthenticatedAndExplicitlyConfirmed) {
             R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie
@@ -298,7 +298,7 @@
         authState: PromptAuthState,
         isAuthenticating: Boolean,
         isPendingConfirmation: Boolean,
-        showingError: Boolean
+        showingError: Boolean,
     ): Int {
         return if (authState.isAuthenticatedAndExplicitlyConfirmed) {
             R.raw.biometricprompt_sfps_unlock_to_success
@@ -338,7 +338,7 @@
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.isPendingConfirmation,
-                        promptViewModel.showingError
+                        promptViewModel.showingError,
                     ) {
                         sensorType: FingerprintSensorType,
                         authState: PromptAuthState,
@@ -350,7 +350,7 @@
                             authState.isAuthenticated,
                             isAuthenticating,
                             isPendingConfirmation,
-                            showingError
+                            showingError,
                         )
                     }
                 AuthType.Face ->
@@ -370,7 +370,7 @@
         isAuthenticated: Boolean,
         isAuthenticating: Boolean,
         isPendingConfirmation: Boolean,
-        showingError: Boolean
+        showingError: Boolean,
     ): Int =
         if (isPendingConfirmation) {
             when (sensorType) {
@@ -381,7 +381,7 @@
             when (sensorType) {
                 FingerprintSensorType.POWER_BUTTON ->
                     R.string.security_settings_sfps_enroll_find_sensor_message
-                else -> R.string.fingerprint_dialog_touch_sensor
+                else -> R.string.accessibility_fingerprint_label
             }
         } else if (showingError) {
             R.string.biometric_dialog_try_again
@@ -392,7 +392,7 @@
     private fun getFaceIconContentDescriptionId(
         authState: PromptAuthState,
         isAuthenticating: Boolean,
-        showingError: Boolean
+        showingError: Boolean,
     ): Int =
         if (authState.isAuthenticatedAndExplicitlyConfirmed) {
             R.string.biometric_dialog_face_icon_description_confirmed
@@ -415,7 +415,7 @@
                         promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
-                        promptViewModel.showingError
+                        promptViewModel.showingError,
                     ) {
                         sensorType: FingerprintSensorType,
                         authState: PromptAuthState,
@@ -427,7 +427,7 @@
                                 shouldAnimateFingerprintIconView(
                                     authState.isAuthenticated,
                                     isAuthenticating,
-                                    showingError
+                                    showingError,
                                 )
                         }
                     }
@@ -435,7 +435,7 @@
                     combine(
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
-                        promptViewModel.showingError
+                        promptViewModel.showingError,
                     ) { authState: PromptAuthState, isAuthenticating: Boolean, showingError: Boolean
                         ->
                         isAuthenticating ||
@@ -463,7 +463,7 @@
                                     authState.isAuthenticated,
                                     isAuthenticating,
                                     isPendingConfirmation,
-                                    showingError
+                                    showingError,
                                 )
                         }
                     }
@@ -483,14 +483,14 @@
     private fun shouldAnimateFingerprintIconView(
         isAuthenticated: Boolean,
         isAuthenticating: Boolean,
-        showingError: Boolean
+        showingError: Boolean,
     ) = (isAuthenticating && _previousIconWasError.value) || isAuthenticated || showingError
 
     private fun shouldAnimateCoexIconView(
         isAuthenticated: Boolean,
         isAuthenticating: Boolean,
         isPendingConfirmation: Boolean,
-        showingError: Boolean
+        showingError: Boolean,
     ) =
         (isAuthenticating && _previousIconWasError.value) ||
             isPendingConfirmation ||
@@ -522,7 +522,7 @@
         listOf(
             R.raw.biometricprompt_sfps_fingerprint_authenticating,
             R.raw.biometricprompt_sfps_rear_display_fingerprint_authenticating,
-            R.raw.biometricprompt_sfps_rear_display_fingerprint_authenticating
+            R.raw.biometricprompt_sfps_rear_display_fingerprint_authenticating,
         )
 
     /** Called on configuration changes */
@@ -579,7 +579,7 @@
                 R.raw.fingerprint_dialogue_error_to_success_lottie,
                 R.raw.fingerprint_dialogue_fingerprint_to_success_lottie,
                 R.raw.fingerprint_dialogue_error_to_fingerprint_lottie,
-                R.raw.fingerprint_dialogue_fingerprint_to_error_lottie
+                R.raw.fingerprint_dialogue_fingerprint_to_error_lottie,
             )
         }
 
@@ -620,7 +620,7 @@
                 R.raw.fingerprint_dialogue_error_to_fingerprint_lottie,
                 R.raw.fingerprint_dialogue_error_to_success_lottie,
                 R.raw.fingerprint_dialogue_fingerprint_to_error_lottie,
-                R.raw.fingerprint_dialogue_fingerprint_to_success_lottie
+                R.raw.fingerprint_dialogue_fingerprint_to_success_lottie,
             )
         }
 
@@ -632,7 +632,7 @@
             R.raw.face_dialog_dark_to_error,
             R.raw.face_dialog_error_to_idle,
             R.raw.face_dialog_idle_static,
-            R.raw.face_dialog_authenticating
+            R.raw.face_dialog_authenticating,
         )
 
     private fun getSfpsAsset_fingerprintAuthenticating(isInRearDisplayMode: Boolean): Int =
@@ -644,7 +644,7 @@
 
     private fun getSfpsAsset_fingerprintToError(
         rotation: DisplayRotation,
-        isInRearDisplayMode: Boolean
+        isInRearDisplayMode: Boolean,
     ): Int =
         if (isInRearDisplayMode) {
             when (rotation) {
@@ -668,7 +668,7 @@
 
     private fun getSfpsAsset_errorToFingerprint(
         rotation: DisplayRotation,
-        isInRearDisplayMode: Boolean
+        isInRearDisplayMode: Boolean,
     ): Int =
         if (isInRearDisplayMode) {
             when (rotation) {
@@ -692,7 +692,7 @@
 
     private fun getSfpsAsset_fingerprintToUnlock(
         rotation: DisplayRotation,
-        isInRearDisplayMode: Boolean
+        isInRearDisplayMode: Boolean,
     ): Int =
         if (isInRearDisplayMode) {
             when (rotation) {
@@ -716,7 +716,7 @@
 
     private fun getSfpsAsset_fingerprintToSuccess(
         rotation: DisplayRotation,
-        isInRearDisplayMode: Boolean
+        isInRearDisplayMode: Boolean,
     ): Int =
         if (isInRearDisplayMode) {
             when (rotation) {
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingStartModule.kt b/packages/SystemUI/src/com/android/systemui/classifier/FalsingStartModule.kt
index a9f8f37..4246430 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingStartModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingStartModule.kt
@@ -27,5 +27,5 @@
     @Binds
     @IntoMap
     @ClassKey(FalsingCoreStartable::class)
-    fun bindFalsingCoreStartable(falsingCoreStartable: FalsingCoreStartable?): CoreStartable?
+    fun bindFalsingCoreStartable(falsingCoreStartable: FalsingCoreStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
index 4d804d0..747a2a9 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
@@ -53,8 +53,12 @@
     val onConfigurationChange: Flow<Unit>
 
     val scaleForResolution: Flow<Float>
+
     val configurationValues: Flow<Configuration>
 
+    /** Emits the latest display this configuration controller has been moved to. */
+    val onMovedToDisplay: Flow<Int>
+
     fun getResolutionScale(): Float
 
     /** Convenience to context.resources.getDimensionPixelSize() */
@@ -117,6 +121,20 @@
         configurationController.addCallback(callback)
         awaitClose { configurationController.removeCallback(callback) }
     }
+    override val onMovedToDisplay: Flow<Int>
+        get() = conflatedCallbackFlow {
+            val callback =
+                object : ConfigurationController.ConfigurationListener {
+                    override fun onMovedToDisplay(
+                        newDisplayId: Int,
+                        newConfiguration: Configuration?,
+                    ) {
+                        trySend(newDisplayId)
+                    }
+                }
+            configurationController.addCallback(callback)
+            awaitClose { configurationController.removeCallback(callback) }
+        }
 
     override val scaleForResolution: StateFlow<Float> =
         onConfigurationChange
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalAppWidgetHostViewBinder.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalAppWidgetHostViewBinder.kt
deleted file mode 100644
index 71bfe0c..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalAppWidgetHostViewBinder.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.communal.ui.binder
-
-import android.content.Context
-import android.os.Bundle
-import android.util.SizeF
-import android.view.View
-import android.view.ViewGroup
-import android.widget.FrameLayout
-import androidx.compose.ui.unit.IntSize
-import androidx.core.view.doOnLayout
-import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.Flags.communalWidgetResizing
-import com.android.systemui.common.ui.view.onLayoutChanged
-import com.android.systemui.communal.domain.model.CommunalContentModel
-import com.android.systemui.communal.util.WidgetViewFactory
-import com.android.systemui.util.kotlin.DisposableHandles
-import com.android.systemui.util.kotlin.toDp
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOn
-
-object CommunalAppWidgetHostViewBinder {
-    private const val TAG = "CommunalAppWidgetHostViewBinder"
-
-    fun bind(
-        context: Context,
-        applicationScope: CoroutineScope,
-        mainContext: CoroutineContext,
-        backgroundContext: CoroutineContext,
-        container: FrameLayout,
-        model: CommunalContentModel.WidgetContent.Widget,
-        size: SizeF?,
-        factory: WidgetViewFactory,
-    ): DisposableHandle {
-        val disposables = DisposableHandles()
-
-        val loadingJob =
-            applicationScope.launch("$TAG#createWidgetView") {
-                val widget = factory.createWidget(context, model, size)
-                waitForLayout(container)
-                container.post { container.setView(widget) }
-                if (communalWidgetResizing()) {
-                    // Update the app widget size in the background.
-                    launch("$TAG#updateSize", backgroundContext) {
-                        container.sizeFlow().flowOn(mainContext).distinctUntilChanged().collect {
-                            (width, height) ->
-                            widget.updateAppWidgetSize(
-                                /* newOptions = */ Bundle(),
-                                /* minWidth = */ width,
-                                /* minHeight = */ height,
-                                /* maxWidth = */ width,
-                                /* maxHeight = */ height,
-                                /* ignorePadding = */ true,
-                            )
-                        }
-                    }
-                }
-            }
-
-        disposables += DisposableHandle { loadingJob.cancel() }
-        disposables += DisposableHandle { container.removeAllViews() }
-
-        return disposables
-    }
-
-    private suspend fun waitForLayout(container: FrameLayout) = suspendCoroutine { cont ->
-        container.doOnLayout { cont.resume(Unit) }
-    }
-}
-
-private fun ViewGroup.setView(view: View) {
-    if (view.parent == this) {
-        return
-    }
-    (view.parent as? ViewGroup)?.removeView(view)
-    addView(view)
-}
-
-private fun View.sizeAsDp(): IntSize = IntSize(width.toDp(context), height.toDp(context))
-
-private fun View.sizeFlow(): Flow<IntSize> = conflatedCallbackFlow {
-    if (isLaidOut && !isLayoutRequested) {
-        trySend(sizeAsDp())
-    }
-    val disposable = onLayoutChanged { trySend(sizeAsDp()) }
-    awaitClose { disposable.dispose() }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt
index 2e12bad..9f19562 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalAppWidgetSection.kt
@@ -16,98 +16,132 @@
 
 package com.android.systemui.communal.ui.view.layout.sections
 
+import android.os.Bundle
 import android.util.SizeF
+import android.view.View
 import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
 import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-import android.widget.FrameLayout
+import android.view.accessibility.AccessibilityNodeInfo
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.viewinterop.AndroidView
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.systemui.Flags.communalWidgetResizing
+import com.android.systemui.Flags.communalHubUseThreadPoolForWidgets
 import com.android.systemui.communal.domain.model.CommunalContentModel
-import com.android.systemui.communal.ui.binder.CommunalAppWidgetHostViewBinder
-import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
-import com.android.systemui.communal.util.WidgetViewFactory
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.communal.ui.viewmodel.CommunalAppWidgetViewModel
+import com.android.systemui.communal.widgets.CommunalAppWidgetHostView
+import com.android.systemui.communal.widgets.WidgetInteractionHandler
 import com.android.systemui.dagger.qualifiers.UiBackground
+import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.res.R
+import java.util.concurrent.Executor
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.ThreadPoolExecutor
+import java.util.concurrent.TimeUnit
 import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.DisposableHandle
 
 class CommunalAppWidgetSection
 @Inject
 constructor(
-    @Application private val applicationScope: CoroutineScope,
-    @Main private val mainContext: CoroutineContext,
-    @UiBackground private val backgroundContext: CoroutineContext,
-    private val factory: WidgetViewFactory,
+    @UiBackground private val uiBgExecutor: Executor,
+    private val interactionHandler: WidgetInteractionHandler,
+    private val viewModelFactory: CommunalAppWidgetViewModel.Factory,
 ) {
 
     private companion object {
-        val DISPOSABLE_TAG = R.id.communal_widget_disposable_tag
+        const val TAG = "CommunalAppWidgetSection"
+        val LISTENER_TAG = R.id.communal_widget_listener_tag
+
+        val poolSize by lazy { Runtime.getRuntime().availableProcessors().coerceAtLeast(2) }
+
+        /**
+         * This executor is used for widget inflation. Parameters match what launcher uses. See
+         * [com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR].
+         */
+        val widgetExecutor by lazy {
+            ThreadPoolExecutor(
+                /*corePoolSize*/ poolSize,
+                /*maxPoolSize*/ poolSize,
+                /*keepAlive*/ 1,
+                /*unit*/ TimeUnit.SECONDS,
+                /*workQueue*/ LinkedBlockingQueue(),
+            )
+        }
     }
 
     @Composable
     fun Widget(
-        viewModel: BaseCommunalViewModel,
+        isFocusable: Boolean,
+        openWidgetEditor: () -> Unit,
         model: CommunalContentModel.WidgetContent.Widget,
         size: SizeF,
         modifier: Modifier = Modifier,
     ) {
-        val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false)
+        val viewModel = rememberViewModel("$TAG#viewModel") { viewModelFactory.create() }
+        val longClickLabel = stringResource(R.string.accessibility_action_label_edit_widgets)
+        val accessibilityDelegate =
+            remember(longClickLabel, openWidgetEditor) {
+                WidgetAccessibilityDelegate(longClickLabel, openWidgetEditor)
+            }
 
         AndroidView(
             factory = { context ->
-                FrameLayout(context).apply {
-                    layoutParams =
-                        FrameLayout.LayoutParams(
-                            FrameLayout.LayoutParams.MATCH_PARENT,
-                            FrameLayout.LayoutParams.MATCH_PARENT,
-                        )
-
-                    // Need to attach the disposable handle to the view here instead of storing
-                    // the state in the composable in order to properly support lazy lists. In a
-                    // lazy list, when the composable is no longer in view - it will exit
-                    // composition and any state inside the composable will be lost. However,
-                    // the View instance will be re-used. Therefore we can store data on the view
-                    // in order to preserve it.
-                    setTag(
-                        DISPOSABLE_TAG,
-                        CommunalAppWidgetHostViewBinder.bind(
-                            context = context,
-                            container = this,
-                            model = model,
-                            size = if (!communalWidgetResizing()) size else null,
-                            factory = factory,
-                            applicationScope = applicationScope,
-                            mainContext = mainContext,
-                            backgroundContext = backgroundContext,
-                        ),
-                    )
-
-                    accessibilityDelegate = viewModel.widgetAccessibilityDelegate
+                CommunalAppWidgetHostView(context, interactionHandler).apply {
+                    if (communalHubUseThreadPoolForWidgets()) {
+                        setExecutor(widgetExecutor)
+                    } else {
+                        setExecutor(uiBgExecutor)
+                    }
                 }
             },
-            update = { container ->
-                container.importantForAccessibility =
+            update = { view ->
+                view.accessibilityDelegate = accessibilityDelegate
+                view.importantForAccessibility =
                     if (isFocusable) {
                         IMPORTANT_FOR_ACCESSIBILITY_AUTO
                     } else {
                         IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
                     }
-            },
-            onRelease = { view ->
-                val disposable = (view.getTag(DISPOSABLE_TAG) as? DisposableHandle)
-                disposable?.dispose()
+                view.setAppWidget(model.appWidgetId, model.providerInfo)
+                // To avoid calling the expensive setListener method on every recomposition if
+                // the appWidgetId hasn't changed, we store the current appWidgetId of the view in
+                // a tag.
+                if ((view.getTag(LISTENER_TAG) as? Int) != model.appWidgetId) {
+                    viewModel.setListener(model.appWidgetId, view)
+                    view.setTag(LISTENER_TAG, model.appWidgetId)
+                }
+                viewModel.updateSize(size, view)
             },
             modifier = modifier,
             // For reusing composition in lazy lists.
             onReset = {},
         )
     }
+
+    private class WidgetAccessibilityDelegate(
+        private val longClickLabel: String,
+        private val longClickAction: () -> Unit,
+    ) : View.AccessibilityDelegate() {
+        override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
+            super.onInitializeAccessibilityNodeInfo(host, info)
+            // Hint user to long press in order to enter edit mode
+            info.addAction(
+                AccessibilityNodeInfo.AccessibilityAction(
+                    AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
+                    longClickLabel.lowercase(),
+                )
+            )
+        }
+
+        override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
+            when (action) {
+                AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id -> {
+                    longClickAction()
+                    return true
+                }
+            }
+            return super.performAccessibilityAction(host, action, args)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index a339af3..099a859 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -19,7 +19,6 @@
 import android.appwidget.AppWidgetProviderInfo
 import android.content.ComponentName
 import android.os.UserHandle
-import android.view.View
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.TransitionKey
@@ -80,9 +79,6 @@
      */
     val glanceableTouchAvailable: Flow<Boolean> = anyOf(not(isTouchConsumed), isNestedScrolling)
 
-    /** Accessibility delegate to be set on CommunalAppWidgetHostView. */
-    open val widgetAccessibilityDelegate: View.AccessibilityDelegate? = null
-
     /**
      * The up-to-date value of the grid scroll offset. persisted to interactor on
      * {@link #persistScrollPosition}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModel.kt
new file mode 100644
index 0000000..6bafd14f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalAppWidgetViewModel.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import android.appwidget.AppWidgetHost.AppWidgetHostListener
+import android.appwidget.AppWidgetHostView
+import android.os.Bundle
+import android.util.SizeF
+import com.android.app.tracing.coroutines.coroutineScopeTraced
+import com.android.app.tracing.coroutines.withContextTraced
+import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper
+import com.android.systemui.communal.widgets.AppWidgetHostListenerDelegate
+import com.android.systemui.communal.widgets.CommunalAppWidgetHost
+import com.android.systemui.communal.widgets.GlanceableHubWidgetManager
+import com.android.systemui.dagger.qualifiers.UiBackground
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import dagger.Lazy
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.receiveAsFlow
+
+/** View model for showing a widget. */
+class CommunalAppWidgetViewModel
+@AssistedInject
+constructor(
+    @UiBackground private val backgroundContext: CoroutineContext,
+    private val appWidgetHostLazy: Lazy<CommunalAppWidgetHost>,
+    private val listenerDelegateFactory: AppWidgetHostListenerDelegate.Factory,
+    private val glanceableHubWidgetManagerLazy: Lazy<GlanceableHubWidgetManager>,
+    private val multiUserHelper: GlanceableHubMultiUserHelper,
+) : ExclusiveActivatable() {
+
+    private companion object {
+        const val TAG = "CommunalAppWidgetViewModel"
+        const val CHANNEL_CAPACITY = 10
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): CommunalAppWidgetViewModel
+    }
+
+    private val requests =
+        Channel<Request>(capacity = CHANNEL_CAPACITY, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+
+    fun setListener(appWidgetId: Int, listener: AppWidgetHostListener) {
+        requests.trySend(SetListener(appWidgetId, listener))
+    }
+
+    fun updateSize(size: SizeF, view: AppWidgetHostView) {
+        requests.trySend(UpdateSize(size, view))
+    }
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScopeTraced("$TAG#onActivated") {
+            requests.receiveAsFlow().collect { request ->
+                when (request) {
+                    is SetListener -> handleSetListener(request.appWidgetId, request.listener)
+                    is UpdateSize -> handleUpdateSize(request.size, request.view)
+                }
+            }
+        }
+
+        awaitCancellation()
+    }
+
+    private suspend fun handleSetListener(appWidgetId: Int, listener: AppWidgetHostListener) =
+        withContextTraced("$TAG#setListenerInner", backgroundContext) {
+            if (
+                multiUserHelper.glanceableHubHsumFlagEnabled &&
+                    multiUserHelper.isInHeadlessSystemUser()
+            ) {
+                // If the widget view is created in the headless system user, the widget host lives
+                // remotely in the foreground user, and therefore the host listener needs to be
+                // registered through the widget manager.
+                with(glanceableHubWidgetManagerLazy.get()) {
+                    setAppWidgetHostListener(appWidgetId, listenerDelegateFactory.create(listener))
+                }
+            } else {
+                // Instead of setting the view as the listener directly, we wrap the view in a
+                // delegate which ensures the callbacks always get called on the main thread.
+                with(appWidgetHostLazy.get()) {
+                    setListener(appWidgetId, listenerDelegateFactory.create(listener))
+                }
+            }
+        }
+
+    private suspend fun handleUpdateSize(size: SizeF, view: AppWidgetHostView) =
+        withContextTraced("$TAG#updateSizeInner", backgroundContext) {
+            view.updateAppWidgetSize(
+                /* newOptions = */ Bundle(),
+                /* minWidth = */ size.width.toInt(),
+                /* minHeight = */ size.height.toInt(),
+                /* maxWidth = */ size.width.toInt(),
+                /* maxHeight = */ size.height.toInt(),
+                /* ignorePadding = */ true,
+            )
+        }
+}
+
+private sealed interface Request
+
+/**
+ * [Request] to call [CommunalAppWidgetHost.setListener] to tie this view to a particular widget.
+ * Since this is involves an IPC to system_server, the call is asynchronous and happens in the
+ * background.
+ */
+private data class SetListener(val appWidgetId: Int, val listener: AppWidgetHostListener) : Request
+
+/**
+ * [Request] to call [AppWidgetHostView.updateAppWidgetSize] to notify the widget provider of the
+ * new size. Since this is involves an IPC to system_server, the call is asynchronous and happens in
+ * the background.
+ */
+private data class UpdateSize(val size: SizeF, val view: AppWidgetHostView) : Request
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt
index 63a4972..ce3a2be 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt
@@ -17,21 +17,17 @@
 package com.android.systemui.communal.ui.viewmodel
 
 import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
 
 /** View model for communal tutorial indicator on keyguard */
 class CommunalTutorialIndicatorViewModel
 @Inject
-constructor(
-    private val communalTutorialInteractor: CommunalTutorialInteractor,
-    bottomAreaInteractor: KeyguardBottomAreaInteractor,
-) {
+constructor(private val communalTutorialInteractor: CommunalTutorialInteractor) {
     /**
      * An observable for whether the tutorial indicator view should be visible.
      *
@@ -46,5 +42,6 @@
     }
 
     /** An observable for the alpha level for the tutorial indicator. */
-    val alpha: Flow<Float> = bottomAreaInteractor.alpha.distinctUntilChanged()
+    // TODO("b/383587536") find replacement for keyguardBottomAreaInteractor alpha
+    val alpha: Flow<Float> = flowOf(0f)
 }
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 83bd265..ddc4d1c 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
@@ -17,10 +17,6 @@
 package com.android.systemui.communal.ui.viewmodel
 
 import android.content.ComponentName
-import android.content.res.Resources
-import android.os.Bundle
-import android.view.View
-import android.view.accessibility.AccessibilityNodeInfo
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.Flags
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -45,7 +41,6 @@
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.media.dagger.MediaModule
-import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.KeyguardIndicationController
@@ -85,7 +80,6 @@
     @Main val mainDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
     @Background private val bgScope: CoroutineScope,
-    @Main private val resources: Resources,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     keyguardInteractor: KeyguardInteractor,
     private val keyguardIndicationController: KeyguardIndicationController,
@@ -219,39 +213,6 @@
             }
             .distinctUntilChanged()
 
-    override val widgetAccessibilityDelegate =
-        object : View.AccessibilityDelegate() {
-            override fun onInitializeAccessibilityNodeInfo(
-                host: View,
-                info: AccessibilityNodeInfo,
-            ) {
-                super.onInitializeAccessibilityNodeInfo(host, info)
-                // Hint user to long press in order to enter edit mode
-                info.addAction(
-                    AccessibilityNodeInfo.AccessibilityAction(
-                        AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
-                        resources
-                            .getString(R.string.accessibility_action_label_edit_widgets)
-                            .lowercase(),
-                    )
-                )
-            }
-
-            override fun performAccessibilityAction(
-                host: View,
-                action: Int,
-                args: Bundle?,
-            ): Boolean {
-                when (action) {
-                    AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id -> {
-                        onOpenWidgetEditor()
-                        return true
-                    }
-                }
-                return super.performAccessibilityAction(host, action, args)
-            }
-        }
-
     private val _isEnableWidgetDialogShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
     val isEnableWidgetDialogShowing: Flow<Boolean> = _isEnableWidgetDialogShowing.asStateFlow()
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt b/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt
deleted file mode 100644
index 50d86a2..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.communal.util
-
-import android.content.Context
-import android.os.Bundle
-import android.util.SizeF
-import com.android.app.tracing.coroutines.withContextTraced as withContext
-import com.android.systemui.Flags
-import com.android.systemui.communal.domain.model.CommunalContentModel
-import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper
-import com.android.systemui.communal.widgets.AppWidgetHostListenerDelegate
-import com.android.systemui.communal.widgets.CommunalAppWidgetHost
-import com.android.systemui.communal.widgets.CommunalAppWidgetHostView
-import com.android.systemui.communal.widgets.GlanceableHubWidgetManager
-import com.android.systemui.communal.widgets.WidgetInteractionHandler
-import com.android.systemui.dagger.qualifiers.UiBackground
-import dagger.Lazy
-import java.util.concurrent.Executor
-import java.util.concurrent.LinkedBlockingQueue
-import java.util.concurrent.ThreadPoolExecutor
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
-
-/** Factory for creating [CommunalAppWidgetHostView] in a background thread. */
-class WidgetViewFactory
-@Inject
-constructor(
-    @UiBackground private val uiBgContext: CoroutineContext,
-    @UiBackground private val uiBgExecutor: Executor,
-    private val appWidgetHostLazy: Lazy<CommunalAppWidgetHost>,
-    private val interactionHandler: WidgetInteractionHandler,
-    private val listenerFactory: AppWidgetHostListenerDelegate.Factory,
-    private val glanceableHubWidgetManagerLazy: Lazy<GlanceableHubWidgetManager>,
-    private val multiUserHelper: GlanceableHubMultiUserHelper,
-) {
-    suspend fun createWidget(
-        context: Context,
-        model: CommunalContentModel.WidgetContent.Widget,
-        size: SizeF?,
-    ): CommunalAppWidgetHostView =
-        withContext("$TAG#createWidget", uiBgContext) {
-            val view =
-                CommunalAppWidgetHostView(context, interactionHandler).apply {
-                    if (Flags.communalHubUseThreadPoolForWidgets()) {
-                        setExecutor(widgetExecutor)
-                    } else {
-                        setExecutor(uiBgExecutor)
-                    }
-                    setAppWidget(model.appWidgetId, model.providerInfo)
-                }
-
-            if (
-                multiUserHelper.glanceableHubHsumFlagEnabled &&
-                    multiUserHelper.isInHeadlessSystemUser()
-            ) {
-                // If the widget view is created in the headless system user, the widget host lives
-                // remotely in the foreground user, and therefore the host listener needs to be
-                // registered through the widget manager.
-                with(glanceableHubWidgetManagerLazy.get()) {
-                    setAppWidgetHostListener(model.appWidgetId, listenerFactory.create(view))
-                }
-            } else {
-                // Instead of setting the view as the listener directly, we wrap the view in a
-                // delegate which ensures the callbacks always get called on the main thread.
-                with(appWidgetHostLazy.get()) {
-                    setListener(model.appWidgetId, listenerFactory.create(view))
-                }
-            }
-
-            if (size != null) {
-                view.updateAppWidgetSize(
-                    /* newOptions = */ Bundle(),
-                    /* minWidth = */ size.width.toInt(),
-                    /* minHeight = */ size.height.toInt(),
-                    /* maxWidth = */ size.width.toInt(),
-                    /* maxHeight = */ size.height.toInt(),
-                    /* ignorePadding = */ true,
-                )
-            }
-            view
-        }
-
-    private companion object {
-        const val TAG = "WidgetViewFactory"
-
-        val poolSize = Runtime.getRuntime().availableProcessors().coerceAtLeast(2)
-
-        /**
-         * This executor is used for widget inflation. Parameters match what launcher uses. See
-         * [com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR].
-         */
-        val widgetExecutor =
-            ThreadPoolExecutor(
-                /*corePoolSize*/ poolSize,
-                /*maxPoolSize*/ poolSize,
-                /*keepAlive*/ 1,
-                /*unit*/ TimeUnit.SECONDS,
-                /*workQueue*/ LinkedBlockingQueue(),
-            )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/AppWidgetHostListenerDelegate.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/AppWidgetHostListenerDelegate.kt
index f341621..7d80acd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/AppWidgetHostListenerDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/AppWidgetHostListenerDelegate.kt
@@ -36,7 +36,7 @@
 ) : AppWidgetHostListener {
 
     @AssistedFactory
-    interface Factory {
+    fun interface Factory {
         fun create(listener: AppWidgetHostListener): AppWidgetHostListenerDelegate
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 1fc5494..3050cba 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -61,6 +61,7 @@
 import com.android.systemui.screenshot.ReferenceScreenshotModule;
 import com.android.systemui.settings.MultiUserUtilsModule;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.settings.brightness.dagger.BrightnessSliderModule;
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.ShadeModule;
 import com.android.systemui.startable.Dependencies;
@@ -124,6 +125,7 @@
         AccessibilityRepositoryModule.class,
         AospPolicyModule.class,
         BatterySaverModule.class,
+        BrightnessSliderModule.class,
         CentralSurfacesModule.class,
         ClipboardOverlayOverrideModule.class,
         CollapsedStatusBarFragmentStartableModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt
index 6c335e7..0ab9661 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt
@@ -16,18 +16,13 @@
 
 package com.android.systemui.deviceentry
 
-import com.android.keyguard.EmptyLockIconViewController
-import com.android.keyguard.LockIconViewController
 import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepositoryModule
 import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigModule
 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import dagger.Binds
-import dagger.Lazy
 import dagger.Module
-import dagger.Provides
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
 import dagger.multibindings.Multibinds
@@ -45,14 +40,4 @@
     abstract fun deviceUnlockedInteractorActivator(
         activator: DeviceUnlockedInteractor.Activator
     ): CoreStartable
-
-    companion object {
-        @Provides
-        @SysUISingleton
-        fun provideLockIconViewController(
-            emptyLockIconViewController: Lazy<EmptyLockIconViewController>
-        ): LockIconViewController {
-            return emptyLockIconViewController.get()
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 91b44e7..e1ebf7c 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -20,6 +20,7 @@
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
 import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON;
 
@@ -120,6 +121,8 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository;
+import com.android.systemui.display.shared.model.DisplayWindowProperties;
 import com.android.systemui.globalactions.domain.interactor.GlobalActionsInteractor;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.GlobalActions.GlobalActionsManager;
@@ -127,6 +130,7 @@
 import com.android.systemui.scrim.ScrimDrawable;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.LightBarController;
@@ -149,6 +153,8 @@
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+
 /**
  * Helper to show the global actions dialog.  Each item is an {@link Action} that may show depending
  * on whether the keyguard is showing, and whether the device is provisioned.
@@ -194,6 +200,8 @@
     // See NotificationManagerService.LONG_DELAY
     private static final int TOAST_VISIBLE_TIME = 3500;
 
+    private static final int DIALOG_WINDOW_TYPE = TYPE_STATUS_BAR_SUB_PANEL;
+
     private final Context mContext;
     private final GlobalActionsManager mWindowManagerFuncs;
     private final AudioManager mAudioManager;
@@ -261,6 +269,7 @@
     private final DialogTransitionAnimator mDialogTransitionAnimator;
     private final UserLogoutInteractor mLogoutInteractor;
     private final GlobalActionsInteractor mInteractor;
+    private final Lazy<DisplayWindowPropertiesRepository> mDisplayWindowPropertiesRepositoryLazy;
 
     @VisibleForTesting
     public enum GlobalActionsEvent implements UiEventLogger.UiEventEnum {
@@ -376,7 +385,8 @@
             DialogTransitionAnimator dialogTransitionAnimator,
             SelectedUserInteractor selectedUserInteractor,
             UserLogoutInteractor logoutInteractor,
-            GlobalActionsInteractor interactor) {
+            GlobalActionsInteractor interactor,
+            Lazy<DisplayWindowPropertiesRepository> displayWindowPropertiesRepository) {
         mContext = context;
         mWindowManagerFuncs = windowManagerFuncs;
         mAudioManager = audioManager;
@@ -413,6 +423,7 @@
         mSelectedUserInteractor = selectedUserInteractor;
         mLogoutInteractor = logoutInteractor;
         mInteractor = interactor;
+        mDisplayWindowPropertiesRepositoryLazy = displayWindowPropertiesRepository;
 
         // receive broadcasts
         IntentFilter filter = new IntentFilter();
@@ -473,9 +484,10 @@
      * @param isDeviceProvisioned True if device is provisioned
      * @param expandable          The expandable from which we should animate the dialog when
      *                            showing it
+     * @param displayId           Display that should show the dialog
      */
     public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned,
-            @Nullable Expandable expandable) {
+            @Nullable Expandable expandable, int displayId) {
         mKeyguardShowing = keyguardShowing;
         mDeviceProvisioned = isDeviceProvisioned;
         if (mDialog != null && mDialog.isShowing()) {
@@ -487,7 +499,7 @@
             mDialog.dismiss();
             mDialog = null;
         } else {
-            handleShow(expandable);
+            handleShow(expandable, displayId);
         }
     }
 
@@ -507,8 +519,8 @@
         mHandler.sendEmptyMessage(MESSAGE_DISMISS);
     }
 
-    protected void handleShow(@Nullable Expandable expandable) {
-        mDialog = createDialog();
+    protected void handleShow(@Nullable Expandable expandable, int displayId) {
+        mDialog = createDialog(displayId);
         prepareDialog();
 
         WindowManager.LayoutParams attrs = mDialog.getWindow().getAttributes();
@@ -686,16 +698,44 @@
         mPowerAdapter = new MyPowerOptionsAdapter();
     }
 
+
     /**
      * Create the global actions dialog.
      *
      * @return A new dialog.
      */
     protected ActionsDialogLite createDialog() {
+        return createDialog(mContext.getDisplayId());
+    }
+
+    private Context getContextForDisplay(int displayId) {
+        if (!ShadeWindowGoesAround.isEnabled()) {
+            Log.e(TAG, "Asked for the displayId=" + displayId
+                    + " context but returning default display one as ShadeWindowGoesAround flag "
+                    + "is disabled.");
+            return mContext;
+        }
+        try {
+            DisplayWindowProperties properties = mDisplayWindowPropertiesRepositoryLazy.get().get(
+                    displayId,
+                    DIALOG_WINDOW_TYPE);
+            return properties.getContext();
+        } catch (Exception e) {
+            Log.e(TAG, "Couldn't get context for displayId=" + displayId);
+            return mContext;
+        }
+    }
+    /**
+     * Create the global actions dialog with a specific context.
+     *
+     * @return A new dialog.
+     */
+    protected ActionsDialogLite createDialog(int displayId) {
+        final Context context = getContextForDisplay(displayId);
         initDialogItems();
 
         ActionsDialogLite dialog = new ActionsDialogLite(
-                mContext,
+                context,
                 com.android.systemui.res.R.style.Theme_SystemUI_Dialog_GlobalActionsLite,
                 mAdapter,
                 mOverflowAdapter,
@@ -704,7 +744,7 @@
                 mLightBarController,
                 mKeyguardStateController,
                 mNotificationShadeWindowController,
-                mStatusBarWindowControllerStore.getDefaultDisplay(),
+                mStatusBarWindowControllerStore.forDisplay(context.getDisplayId()),
                 this::onRefresh,
                 mKeyguardShowing,
                 mPowerAdapter,
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java
index c5027cc..a6255d0 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java
@@ -63,7 +63,8 @@
     public void showGlobalActions(GlobalActionsManager manager) {
         if (mDisabled) return;
         mGlobalActionsDialog.showOrHideDialog(mKeyguardStateController.isShowing(),
-                mDeviceProvisionedController.isDeviceProvisioned(), null /* view */);
+                mDeviceProvisionedController.isDeviceProvisioned(), null /* view */,
+                mContext.getDisplayId());
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
index 058e587..950a727 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
@@ -79,6 +79,9 @@
                 bodyResId = R.string.tutorial_action_key_guidance,
                 titleSuccessResId = R.string.tutorial_action_key_success_title,
                 bodySuccessResId = R.string.tutorial_action_key_success_body,
+                // error state for action key is not implemented yet so below should never appear
+                titleErrorResId = R.string.gesture_error_title,
+                bodyErrorResId = R.string.touchpad_action_key_error_body,
             ),
         animations = TutorialScreenConfig.Animations(educationResId = R.raw.action_key_edu),
     )
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
index 0c1bc83..c40adfe 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
@@ -45,18 +45,33 @@
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted
 
 sealed interface TutorialActionState {
     data object NotStarted : TutorialActionState
 
     data class InProgress(
-        val progress: Float = 0f,
-        val startMarker: String? = null,
-        val endMarker: String? = null,
-    ) : TutorialActionState
+        override val progress: Float = 0f,
+        override val startMarker: String? = null,
+        override val endMarker: String? = null,
+    ) : TutorialActionState, Progress
 
     data class Finished(@RawRes val successAnimation: Int) : TutorialActionState
+
+    data object Error : TutorialActionState
+
+    data class InProgressAfterError(val inProgress: InProgress) :
+        TutorialActionState, Progress by inProgress
+}
+
+interface Progress {
+    val progress: Float
+    val startMarker: String?
+    val endMarker: String?
 }
 
 @Composable
@@ -133,10 +148,13 @@
     val focusRequester = remember { FocusRequester() }
     LaunchedEffect(Unit) { focusRequester.requestFocus() }
     val (titleTextId, bodyTextId) =
-        if (actionState is Finished) {
-            config.strings.titleSuccessResId to config.strings.bodySuccessResId
-        } else {
-            config.strings.titleResId to config.strings.bodyResId
+        when (actionState) {
+            is Finished -> config.strings.titleSuccessResId to config.strings.bodySuccessResId
+            Error,
+            is InProgressAfterError ->
+                config.strings.titleErrorResId to config.strings.bodyErrorResId
+            is NotStarted,
+            is InProgress -> config.strings.titleResId to config.strings.bodyResId
         }
     Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
         Text(
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt
index ad18817..b0816ce 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt
@@ -47,8 +47,10 @@
 import com.airbnb.lottie.compose.LottieDynamicProperties
 import com.airbnb.lottie.compose.animateLottieCompositionAsState
 import com.airbnb.lottie.compose.rememberLottieComposition
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted
 import com.android.systemui.res.R
 
@@ -72,16 +74,18 @@
             },
         ) { state ->
             when (state) {
-                NotStarted::class ->
+                NotStarted::class,
+                Error::class ->
                     EducationAnimation(
                         config.animations.educationResId,
                         config.colors.animationColors,
                     )
-                InProgress::class ->
+                InProgress::class,
+                InProgressAfterError::class ->
                     InProgressAnimation(
                         // actionState can be already of different class while this composable is
                         // transitioning to another one
-                        actionState as? InProgress,
+                        actionState as? Progress,
                         config.animations.educationResId,
                         config.colors.animationColors,
                     )
@@ -138,14 +142,14 @@
 
 @Composable
 private fun InProgressAnimation(
-    state: InProgress?,
+    state: Progress?,
     @RawRes inProgressAnimationId: Int,
     animationProperties: LottieDynamicProperties,
 ) {
     // Caching latest progress for when we're animating this view away and state is null.
     // Without this there's jumpcut in the animation while it's animating away.
     // state should never be null when composable appears, only when disappearing
-    val cached = remember { Ref<InProgress>() }
+    val cached = remember { Ref<Progress>() }
     cached.value = state ?: cached.value
     val progress = cached.value?.progress ?: 0f
 
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt
index 60dfed3..2625991 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt
@@ -38,6 +38,8 @@
         @StringRes val bodyResId: Int,
         @StringRes val titleSuccessResId: Int,
         @StringRes val bodySuccessResId: Int,
+        @StringRes val titleErrorResId: Int,
+        @StringRes val bodyErrorResId: Int,
     )
 
     data class Animations(@RawRes val educationResId: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt
index 3020e5d..b597136 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt
@@ -49,7 +49,7 @@
  * @param isCustomShortcut If Shortcut is user customized or system defined.
  */
 data class InternalKeyboardShortcutInfo(
-    val label: String,
+    val label: String = "",
     val keycode: Int,
     val modifiers: Int,
     val baseCharacter: Char = Char.MIN_VALUE,
@@ -60,4 +60,4 @@
 data class InternalGroupsSource(
     val groups: List<InternalKeyboardShortcutGroup>,
     val type: ShortcutCategoryType,
-)
\ No newline at end of file
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepository.kt
new file mode 100644
index 0000000..b029b03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepository.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.hardware.input.AppLaunchData
+import android.hardware.input.InputGestureData.KeyTrigger
+import android.hardware.input.InputManager
+import android.util.Log
+import android.view.InputDevice
+import com.android.systemui.Flags.shortcutHelperKeyGlyph
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import javax.inject.Inject
+
+@SysUISingleton
+class AppLaunchDataRepository
+@Inject
+constructor(
+    private val inputManager: InputManager,
+    @Background private val backgroundScope: CoroutineScope,
+    private val shortcutCategoriesUtils: ShortcutCategoriesUtils,
+    inputDeviceRepository: ShortcutHelperInputDeviceRepository,
+) {
+
+    private val shortcutCommandToAppLaunchDataMap:
+        StateFlow<Map<ShortcutCommandKey, AppLaunchData>> =
+        inputDeviceRepository.activeInputDevice
+            .map { inputDevice ->
+                if (inputDevice == null) {
+                    emptyMap()
+                }
+                else{
+                    buildCommandToAppLaunchDataMap(inputDevice)
+                }
+            }
+            .stateIn(
+                scope = backgroundScope,
+                started = SharingStarted.Eagerly,
+                initialValue = mapOf(),
+            )
+
+    fun getAppLaunchDataForShortcutWithCommand(shortcutCommand: ShortcutCommand): AppLaunchData? {
+        val shortcutCommandAsKey = ShortcutCommandKey(shortcutCommand)
+        return shortcutCommandToAppLaunchDataMap.value[shortcutCommandAsKey]
+    }
+
+    private fun buildCommandToAppLaunchDataMap(inputDevice: InputDevice):
+            Map<ShortcutCommandKey, AppLaunchData> {
+        val commandToAppLaunchDataMap =
+            mutableMapOf<ShortcutCommandKey, AppLaunchData>()
+        val appLaunchInputGestures = inputManager.appLaunchBookmarks
+        appLaunchInputGestures.forEach { inputGesture ->
+            val keyGlyphMap =
+                if (shortcutHelperKeyGlyph()) {
+                    inputManager.getKeyGlyphMap(inputDevice.id)
+                } else null
+
+            val shortcutCommand =
+                shortcutCategoriesUtils.toShortcutCommand(
+                    keyGlyphMap,
+                    inputDevice.keyCharacterMap,
+                    inputGesture.trigger as KeyTrigger,
+                )
+
+            if (shortcutCommand != null) {
+                commandToAppLaunchDataMap[ShortcutCommandKey(shortcutCommand)] =
+                    inputGesture.action.appLaunchData()!!
+            } else {
+                Log.w(
+                    TAG,
+                    "could not get Shortcut Command. inputGesture: $inputGesture",
+                )
+            }
+        }
+
+        return commandToAppLaunchDataMap
+    }
+
+    private data class ShortcutCommandKey(val keys: List<ShortcutKey>) {
+        constructor(
+            shortcutCommand: ShortcutCommand
+        ) : this(shortcutCommand.keys.sortedBy { it.toString() })
+    }
+
+    private companion object {
+        private const val TAG = "AppLaunchDataRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
index 8afec04..18ca877 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
@@ -30,48 +30,37 @@
 import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult
 import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
 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.ShortcutCustomizationRequestInfo
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.SingleShortcutCustomization
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
 import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
 
 @SysUISingleton
 class CustomShortcutCategoriesRepository
 @Inject
 constructor(
-    stateRepository: ShortcutHelperStateRepository,
+    inputDeviceRepository: ShortcutHelperInputDeviceRepository,
     @Background private val backgroundScope: CoroutineScope,
-    @Background private val bgCoroutineContext: CoroutineContext,
     private val shortcutCategoriesUtils: ShortcutCategoriesUtils,
     private val inputGestureDataAdapter: InputGestureDataAdapter,
     private val customInputGesturesRepository: CustomInputGesturesRepository,
-    private val inputManager: InputManager
+    private val inputManager: InputManager,
+    private val appLaunchDataRepository: AppLaunchDataRepository,
 ) : ShortcutCategoriesRepository {
 
     private val _selectedKeyCombination = MutableStateFlow<KeyCombination?>(null)
     private val _shortcutBeingCustomized = mutableStateOf<ShortcutCustomizationRequestInfo?>(null)
 
-    private val activeInputDevice =
-        stateRepository.state.map {
-            if (it is Active) {
-                withContext(bgCoroutineContext) { inputManager.getInputDevice(it.deviceId) }
-            } else {
-                null
-            }
-        }
-
     val pressedKeys =
         _selectedKeyCombination
-            .combine(activeInputDevice) { keyCombination, inputDevice ->
+            .combine(inputDeviceRepository.activeInputDevice) { keyCombination, inputDevice ->
                 if (inputDevice == null || keyCombination == null) {
                     return@combine emptyList()
                 } else {
@@ -105,8 +94,10 @@
             )
 
     override val categories: Flow<List<ShortcutCategory>> =
-        combine(activeInputDevice, customInputGesturesRepository.customInputGestures)
-        { inputDevice, inputGestures ->
+        combine(
+                inputDeviceRepository.activeInputDevice,
+                customInputGesturesRepository.customInputGestures,
+            ) { inputDevice, inputGestures ->
                 if (inputDevice == null) {
                     emptyList()
                 } else {
@@ -147,10 +138,10 @@
     fun buildInputGestureDataForShortcutBeingCustomized(): InputGestureData? {
         try {
             return Builder()
-                .addKeyGestureTypeFromShortcutLabel()
+                .addKeyGestureTypeForShortcutBeingCustomized()
                 .addTriggerFromSelectedKeyCombination()
+                .addAppLaunchDataFromShortcutBeingCustomized()
                 .build()
-            // TODO(b/379648200) add app launch data after dynamic label/icon mapping implementation
         } catch (e: IllegalArgumentException) {
             Log.w(TAG, "could not add custom shortcut: $e")
             return null
@@ -158,9 +149,10 @@
     }
 
     private fun retrieveInputGestureDataForShortcutBeingDeleted(): InputGestureData? {
-        val keyGestureType = getKeyGestureTypeFromShortcutBeingDeletedLabel()
-        return customInputGesturesRepository.retrieveCustomInputGestures()
-            .firstOrNull { it.action.keyGestureType() == keyGestureType }
+        val keyGestureType = getKeyGestureTypeForShortcutBeingCustomized()
+        return customInputGesturesRepository.retrieveCustomInputGestures().firstOrNull {
+            it.action.keyGestureType() == keyGestureType
+        }
     }
 
     suspend fun confirmAndSetShortcutCurrentlyBeingCustomized():
@@ -183,8 +175,8 @@
         return customInputGesturesRepository.resetAllCustomInputGestures()
     }
 
-    private fun Builder.addKeyGestureTypeFromShortcutLabel(): Builder {
-        val keyGestureType = getKeyGestureTypeFromShortcutBeingCustomizedLabel()
+    private fun Builder.addKeyGestureTypeForShortcutBeingCustomized(): Builder {
+        val keyGestureType = getKeyGestureTypeForShortcutBeingCustomized()
 
         if (keyGestureType == null) {
             Log.w(
@@ -193,31 +185,28 @@
             )
             return this
         }
-
         return setKeyGestureType(keyGestureType)
     }
 
-    @KeyGestureType
-    private fun getKeyGestureTypeFromShortcutBeingCustomizedLabel(): Int? {
+    private fun Builder.addAppLaunchDataFromShortcutBeingCustomized(): Builder {
         val shortcutBeingCustomized =
-            getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Add
+            (_shortcutBeingCustomized.value as? SingleShortcutCustomization) ?: return this
 
-        if (shortcutBeingCustomized == null) {
-            Log.w(
-                TAG,
-                "Requested key gesture type from label but shortcut being customized is null",
-            )
-            return null
+        if (shortcutBeingCustomized.categoryType != ShortcutCategoryType.AppCategories) {
+            return this
         }
 
-        return inputGestureDataAdapter
-            .getKeyGestureTypeFromShortcutLabel(shortcutBeingCustomized.label)
+        val defaultShortcutCommand = shortcutBeingCustomized.shortcutCommand
+
+        val appLaunchData =
+            appLaunchDataRepository.getAppLaunchDataForShortcutWithCommand(defaultShortcutCommand)
+
+        return if (appLaunchData == null) this else this.setAppLaunchData(appLaunchData)
     }
 
     @KeyGestureType
-    private fun getKeyGestureTypeFromShortcutBeingDeletedLabel(): Int? {
-        val shortcutBeingCustomized =
-            getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Delete
+    private fun getKeyGestureTypeForShortcutBeingCustomized(): Int? {
+        val shortcutBeingCustomized = getShortcutBeingCustomized() as? SingleShortcutCustomization
 
         if (shortcutBeingCustomized == null) {
             Log.w(
@@ -227,8 +216,10 @@
             return null
         }
 
-        return inputGestureDataAdapter
-            .getKeyGestureTypeFromShortcutLabel(shortcutBeingCustomized.label)
+        return inputGestureDataAdapter.getKeyGestureTypeForShortcut(
+            shortcutLabel = shortcutBeingCustomized.label,
+            shortcutCategoryType = shortcutBeingCustomized.categoryType,
+        )
     }
 
     private fun Builder.addTriggerFromSelectedKeyCombination(): Builder {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt
index 5bb7cdd..db35d49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.keyboard.shortcut.data.repository
 
-import android.hardware.input.InputManager
 import android.view.KeyboardShortcutGroup
 import android.view.KeyboardShortcutInfo
 import com.android.systemui.dagger.SysUISingleton
@@ -36,29 +35,24 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
 
 @SysUISingleton
 class DefaultShortcutCategoriesRepository
 @Inject
 constructor(
     @Background private val backgroundScope: CoroutineScope,
-    @Background private val backgroundDispatcher: CoroutineDispatcher,
     @SystemShortcuts private val systemShortcutsSource: KeyboardShortcutGroupsSource,
     @MultitaskingShortcuts private val multitaskingShortcutsSource: KeyboardShortcutGroupsSource,
     @AppCategoriesShortcuts private val appCategoriesShortcutsSource: KeyboardShortcutGroupsSource,
     @InputShortcuts private val inputShortcutsSource: KeyboardShortcutGroupsSource,
     @CurrentAppShortcuts private val currentAppShortcutsSource: KeyboardShortcutGroupsSource,
-    private val inputManager: InputManager,
-    stateRepository: ShortcutHelperStateRepository,
+    inputDeviceRepository: ShortcutHelperInputDeviceRepository,
     shortcutCategoriesUtils: ShortcutCategoriesUtils,
 ) : ShortcutCategoriesRepository {
 
@@ -83,17 +77,8 @@
             ),
         )
 
-    private val activeInputDevice =
-        stateRepository.state.map {
-            if (it is Active) {
-                withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) }
-            } else {
-                null
-            }
-        }
-
     override val categories: Flow<List<ShortcutCategory>> =
-        activeInputDevice
+        inputDeviceRepository.activeInputDevice
             .map { inputDevice ->
                 if (inputDevice == null) {
                     return@map emptyList()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapter.kt
index df7101e..6e754a3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapter.kt
@@ -48,49 +48,54 @@
 import com.android.systemui.settings.UserTracker
 import javax.inject.Inject
 
-
+/**
+ * Serves as a bridge for converting InputGestureData API Models to Shortcut Helper Data Layer
+ * Models and vice versa.
+ */
 class InputGestureDataAdapter
 @Inject
 constructor(
     private val userTracker: UserTracker,
     private val inputGestureMaps: InputGestureMaps,
-    private val context: Context
+    private val context: Context,
 ) {
     private val userContext: Context
         get() = userTracker.createCurrentUserContext(userTracker.userContext)
 
-    fun toInternalGroupSources(
-        inputGestures: List<InputGestureData>
-    ): List<InternalGroupsSource> {
+    fun toInternalGroupSources(inputGestures: List<InputGestureData>): List<InternalGroupsSource> {
         val ungroupedInternalGroupSources =
             inputGestures.mapNotNull { gestureData ->
                 val keyTrigger = gestureData.trigger as KeyTrigger
                 val keyGestureType = gestureData.action.keyGestureType()
                 val appLaunchData: AppLaunchData? = gestureData.action.appLaunchData()
                 fetchGroupLabelByGestureType(keyGestureType)?.let { groupLabel ->
-                    toInternalKeyboardShortcutInfo(
-                        keyGestureType,
-                        keyTrigger,
-                        appLaunchData
-                    )?.let { internalKeyboardShortcutInfo ->
-                        val group =
-                            InternalKeyboardShortcutGroup(
-                                label = groupLabel,
-                                items = listOf(internalKeyboardShortcutInfo),
-                            )
+                    toInternalKeyboardShortcutInfo(keyGestureType, keyTrigger, appLaunchData)
+                        ?.let { internalKeyboardShortcutInfo ->
+                            val group =
+                                InternalKeyboardShortcutGroup(
+                                    label = groupLabel,
+                                    items = listOf(internalKeyboardShortcutInfo),
+                                )
 
-                        fetchShortcutCategoryTypeByGestureType(keyGestureType)?.let {
-                            InternalGroupsSource(groups = listOf(group), type = it)
+                            fetchShortcutCategoryTypeByGestureType(keyGestureType)?.let {
+                                InternalGroupsSource(groups = listOf(group), type = it)
+                            }
                         }
-                    }
                 }
             }
 
         return ungroupedInternalGroupSources
     }
 
-    fun getKeyGestureTypeFromShortcutLabel(label: String): Int? {
-        return inputGestureMaps.shortcutLabelToKeyGestureTypeMap[label]
+    fun getKeyGestureTypeForShortcut(
+        shortcutLabel: String,
+        shortcutCategoryType: ShortcutCategoryType,
+    ): Int? {
+        if (shortcutCategoryType == ShortcutCategoryType.AppCategories) {
+            return KEY_GESTURE_TYPE_LAUNCH_APPLICATION
+        }
+        val result = inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutLabel]
+        return result
     }
 
     private fun toInternalKeyboardShortcutInfo(
@@ -104,16 +109,14 @@
                 keycode = keyTrigger.keycode,
                 modifiers = keyTrigger.modifierState,
                 isCustomShortcut = true,
-                icon = appLaunchData?.let { fetchShortcutIconByAppLaunchData(appLaunchData) }
+                icon = appLaunchData?.let { fetchShortcutIconByAppLaunchData(appLaunchData) },
             )
         }
         return null
     }
 
     @SuppressLint("QueryPermissionsNeeded")
-    private fun fetchShortcutIconByAppLaunchData(
-        appLaunchData: AppLaunchData
-    ): Icon? {
+    private fun fetchShortcutIconByAppLaunchData(appLaunchData: AppLaunchData): Icon? {
         val intent = fetchIntentFromAppLaunchData(appLaunchData) ?: return null
         val resolvedActivity = resolveSingleMatchingActivityFrom(intent)
 
@@ -132,7 +135,7 @@
 
     private fun fetchShortcutLabelByGestureType(
         @KeyGestureType keyGestureType: Int,
-        appLaunchData: AppLaunchData?
+        appLaunchData: AppLaunchData?,
     ): String? {
         inputGestureMaps.gestureToInternalKeyboardShortcutInfoLabelResIdMap[keyGestureType]?.let {
             return context.getString(it)
@@ -152,16 +155,14 @@
         return if (resolvedActivity == null) {
             getIntentCategoryLabel(intent.selector?.categories?.iterator()?.next())
         } else resolvedActivity.loadLabel(userContext.packageManager).toString()
-
     }
 
     @SuppressLint("QueryPermissionsNeeded")
     private fun resolveSingleMatchingActivityFrom(intent: Intent): ActivityInfo? {
         val packageManager = userContext.packageManager
-        val resolvedActivity = intent.resolveActivityInfo(
-            packageManager,
-            /* flags= */ MATCH_DEFAULT_ONLY
-        ) ?: return null
+        val resolvedActivity =
+            intent.resolveActivityInfo(packageManager, /* flags= */ MATCH_DEFAULT_ONLY)
+                ?: return null
 
         val matchesMultipleActivities =
             ResolverActivity::class.qualifiedName.equals(resolvedActivity.name)
@@ -172,22 +173,26 @@
     }
 
     private fun getIntentCategoryLabel(category: String?): String? {
-        val categoryLabelRes = when (category.toString()) {
-            Intent.CATEGORY_APP_BROWSER -> R.string.keyboard_shortcut_group_applications_browser
-            Intent.CATEGORY_APP_CONTACTS -> R.string.keyboard_shortcut_group_applications_contacts
-            Intent.CATEGORY_APP_EMAIL -> R.string.keyboard_shortcut_group_applications_email
-            Intent.CATEGORY_APP_CALENDAR -> R.string.keyboard_shortcut_group_applications_calendar
-            Intent.CATEGORY_APP_MAPS -> R.string.keyboard_shortcut_group_applications_maps
-            Intent.CATEGORY_APP_MUSIC -> R.string.keyboard_shortcut_group_applications_music
-            Intent.CATEGORY_APP_MESSAGING -> R.string.keyboard_shortcut_group_applications_sms
-            Intent.CATEGORY_APP_CALCULATOR -> R.string.keyboard_shortcut_group_applications_calculator
-            else -> {
-                Log.w(TAG, ("No label for app category $category"))
-                null
+        val categoryLabelRes =
+            when (category.toString()) {
+                Intent.CATEGORY_APP_BROWSER -> R.string.keyboard_shortcut_group_applications_browser
+                Intent.CATEGORY_APP_CONTACTS ->
+                    R.string.keyboard_shortcut_group_applications_contacts
+                Intent.CATEGORY_APP_EMAIL -> R.string.keyboard_shortcut_group_applications_email
+                Intent.CATEGORY_APP_CALENDAR ->
+                    R.string.keyboard_shortcut_group_applications_calendar
+                Intent.CATEGORY_APP_MAPS -> R.string.keyboard_shortcut_group_applications_maps
+                Intent.CATEGORY_APP_MUSIC -> R.string.keyboard_shortcut_group_applications_music
+                Intent.CATEGORY_APP_MESSAGING -> R.string.keyboard_shortcut_group_applications_sms
+                Intent.CATEGORY_APP_CALCULATOR ->
+                    R.string.keyboard_shortcut_group_applications_calculator
+                else -> {
+                    Log.w(TAG, ("No label for app category $category"))
+                    null
+                }
             }
-        }
 
-        return if (categoryLabelRes == null){
+        return if (categoryLabelRes == null) {
             return null
         } else {
             context.getString(categoryLabelRes)
@@ -196,41 +201,48 @@
 
     private fun fetchIntentFromAppLaunchData(appLaunchData: AppLaunchData): Intent? {
         return when (appLaunchData) {
-            is CategoryData -> Intent.makeMainSelectorActivity(
-                /* selectorAction= */ ACTION_MAIN,
-                /* selectorCategory= */ appLaunchData.category
-            )
+            is CategoryData ->
+                Intent.makeMainSelectorActivity(
+                    /* selectorAction= */ ACTION_MAIN,
+                    /* selectorCategory= */ appLaunchData.category,
+                )
 
             is RoleData -> getRoleLaunchIntent(appLaunchData.role)
-            is ComponentData -> resolveComponentNameIntent(
-                packageName = appLaunchData.packageName,
-                className = appLaunchData.className
-            )
+            is ComponentData ->
+                resolveComponentNameIntent(
+                    packageName = appLaunchData.packageName,
+                    className = appLaunchData.className,
+                )
 
             else -> null
         }
     }
 
     private fun resolveComponentNameIntent(packageName: String, className: String): Intent? {
-        buildIntentFromComponentName(ComponentName(packageName, className))?.let { return it }
-        buildIntentFromComponentName(ComponentName(
-            userContext.packageManager.canonicalToCurrentPackageNames(arrayOf(packageName))[0],
-            className
-        ))?.let { return it }
+        buildIntentFromComponentName(ComponentName(packageName, className))?.let {
+            return it
+        }
+        buildIntentFromComponentName(
+                ComponentName(
+                    userContext.packageManager
+                        .canonicalToCurrentPackageNames(arrayOf(packageName))[0],
+                    className,
+                )
+            )
+            ?.let {
+                return it
+            }
         return null
     }
 
     private fun buildIntentFromComponentName(componentName: ComponentName): Intent? {
-        try{
+        try {
             val flags =
                 MATCH_DIRECT_BOOT_UNAWARE or MATCH_DIRECT_BOOT_AWARE or MATCH_UNINSTALLED_PACKAGES
             // attempt to retrieve activity info to see if a NameNotFoundException is thrown.
             userContext.packageManager.getActivityInfo(componentName, flags)
         } catch (e: NameNotFoundException) {
-            Log.w(
-                TAG,
-                "Unable to find activity info for componentName: $componentName"
-            )
+            Log.w(TAG, "Unable to find activity info for componentName: $componentName")
             return null
         }
 
@@ -246,8 +258,9 @@
         val roleManager = userContext.getSystemService(RoleManager::class.java)!!
         if (roleManager.isRoleAvailable(role)) {
             roleManager.getDefaultApplication(role)?.let { rolePackage ->
-                packageManager.getLaunchIntentForPackage(rolePackage)?.let { return it }
-                    ?: Log.w(TAG, "No launch intent for role $role")
+                packageManager.getLaunchIntentForPackage(rolePackage)?.let {
+                    return it
+                } ?: Log.w(TAG, "No launch intent for role $role")
             } ?: Log.w(TAG, "No default application for role $role, user= ${userContext.user}")
         } else {
             Log.w(TAG, "Role $role is not available.")
@@ -264,4 +277,4 @@
     private companion object {
         private const val TAG = "InputGestureDataUtils"
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
index 4a725ec..cf5460f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.graphics.drawable.Icon
+import android.hardware.input.InputGestureData.KeyTrigger
 import android.hardware.input.InputManager
 import android.hardware.input.KeyGlyphMap
 import android.util.Log
@@ -137,8 +138,7 @@
             label = shortcutInfo.label,
             icon = toShortcutIcon(keepIcon, shortcutInfo),
             commands = listOf(shortcutCommand),
-            isCustomizable =
-                shortcutHelperExclusions.isShortcutCustomizable(shortcutInfo.label),
+            isCustomizable = shortcutHelperExclusions.isShortcutCustomizable(shortcutInfo.label),
         )
     }
 
@@ -158,6 +158,22 @@
         return ShortcutIcon(packageName = icon.resPackage, resourceId = icon.resId)
     }
 
+    fun toShortcutCommand(
+        keyGlyphMap: KeyGlyphMap?,
+        keyCharacterMap: KeyCharacterMap,
+        keyTrigger: KeyTrigger,
+    ): ShortcutCommand? {
+        return toShortcutCommand(
+            keyGlyphMap = keyGlyphMap,
+            keyCharacterMap = keyCharacterMap,
+            info =
+                InternalKeyboardShortcutInfo(
+                    keycode = keyTrigger.keycode,
+                    modifiers = keyTrigger.modifierState,
+                ),
+        )
+    }
+
     private fun toShortcutCommand(
         keyGlyphMap: KeyGlyphMap?,
         keyCharacterMap: KeyCharacterMap,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepository.kt
new file mode 100644
index 0000000..1361373
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepository.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.hardware.input.InputManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+class ShortcutHelperInputDeviceRepository
+@Inject
+constructor(
+    stateRepository: ShortcutHelperStateRepository,
+    @Background private val backgroundScope: CoroutineScope,
+    @Background private val bgCoroutineContext: CoroutineContext,
+    private val inputManager: InputManager,
+) {
+    val activeInputDevice =
+        stateRepository.state
+            .map {
+                if (it is Active) {
+                    withContext(bgCoroutineContext) { inputManager.getInputDevice(it.deviceId) }
+                } else {
+                    null
+                }
+            }
+            .stateIn(scope = backgroundScope, started = SharingStarted.Lazily, initialValue = null)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt
index c7e6b43..d8bad25 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt
@@ -18,7 +18,10 @@
 
 import androidx.annotation.DrawableRes
 
-data class ShortcutCommand(val keys: List<ShortcutKey>, val isCustom: Boolean = false)
+data class ShortcutCommand(
+    val keys: List<ShortcutKey> = emptyList(),
+    val isCustom: Boolean = false,
+)
 
 class ShortcutCommandBuilder {
     private val keys = mutableListOf<ShortcutKey>()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt
index 095de41..f183247 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt
@@ -17,17 +17,27 @@
 package com.android.systemui.keyboard.shortcut.shared.model
 
 sealed interface ShortcutCustomizationRequestInfo {
-    data class Add(
-        val label: String = "",
-        val categoryType: ShortcutCategoryType = ShortcutCategoryType.System,
-        val subCategoryLabel: String = "",
-    ) : ShortcutCustomizationRequestInfo
 
-    data class Delete(
-        val label: String = "",
-        val categoryType: ShortcutCategoryType = ShortcutCategoryType.System,
-        val subCategoryLabel: String = "",
-    ) : ShortcutCustomizationRequestInfo
+    sealed interface SingleShortcutCustomization: ShortcutCustomizationRequestInfo {
+        val label: String
+        val categoryType: ShortcutCategoryType
+        val subCategoryLabel: String
+        val shortcutCommand: ShortcutCommand
+
+        data class Add(
+            override val label: String = "",
+            override val categoryType: ShortcutCategoryType = ShortcutCategoryType.System,
+            override val subCategoryLabel: String = "",
+            override val shortcutCommand: ShortcutCommand = ShortcutCommand(),
+        ) : SingleShortcutCustomization
+
+        data class Delete(
+            override val label: String = "",
+            override val categoryType: ShortcutCategoryType = ShortcutCategoryType.System,
+            override val subCategoryLabel: String = "",
+            override val shortcutCommand: ShortcutCommand = ShortcutCommand(),
+        ) : SingleShortcutCustomization
+    }
 
     data object Reset : ShortcutCustomizationRequestInfo
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index af6f0cb..aea583d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -115,7 +115,6 @@
 import androidx.compose.ui.util.fastForEachIndexed
 import com.android.compose.modifiers.thenIf
 import com.android.compose.ui.graphics.painter.rememberDrawablePainter
-import com.android.systemui.keyboard.shortcut.shared.model.Shortcut as ShortcutModel
 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.ShortcutCustomizationRequestInfo
@@ -127,6 +126,7 @@
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
 import com.android.systemui.res.R
 import kotlinx.coroutines.delay
+import com.android.systemui.keyboard.shortcut.shared.model.Shortcut as ShortcutModel
 
 @Composable
 fun ShortcutHelper(
@@ -505,10 +505,10 @@
                 isCustomizing = isCustomizing and category.type.includeInCustomization,
                 onCustomizationRequested = { requestInfo ->
                     when (requestInfo) {
-                        is ShortcutCustomizationRequestInfo.Add ->
+                        is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
                             onCustomizationRequested(requestInfo.copy(categoryType = category.type))
 
-                        is ShortcutCustomizationRequestInfo.Delete ->
+                        is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
                             onCustomizationRequested(requestInfo.copy(categoryType = category.type))
 
                         ShortcutCustomizationRequestInfo.Reset ->
@@ -568,12 +568,12 @@
                     isCustomizing = isCustomizing && shortcut.isCustomizable,
                     onCustomizationRequested = { requestInfo ->
                         when (requestInfo) {
-                            is ShortcutCustomizationRequestInfo.Add ->
+                            is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add ->
                                 onCustomizationRequested(
                                     requestInfo.copy(subCategoryLabel = subCategory.label)
                                 )
 
-                            is ShortcutCustomizationRequestInfo.Delete ->
+                            is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete ->
                                 onCustomizationRequested(
                                     requestInfo.copy(subCategoryLabel = subCategory.label)
                                 )
@@ -644,12 +644,18 @@
             isCustomizing = isCustomizing,
             onAddShortcutRequested = {
                 onCustomizationRequested(
-                    ShortcutCustomizationRequestInfo.Add(label = shortcut.label)
+                    ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add(
+                        label = shortcut.label,
+                        shortcutCommand = shortcut.commands.first(),
+                    )
                 )
             },
             onDeleteShortcutRequested = {
                 onCustomizationRequested(
-                    ShortcutCustomizationRequestInfo.Delete(label = shortcut.label)
+                    ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete(
+                        label = shortcut.label,
+                        shortcutCommand = shortcut.commands.first(),
+                    )
                 )
             },
         )
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
index 92e2592..373eb25 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
@@ -66,8 +66,9 @@
             }
 
     fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) {
+        shortcutCustomizationInteractor.onCustomizationRequested(requestInfo)
         when (requestInfo) {
-            is ShortcutCustomizationRequestInfo.Add -> {
+            is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> {
                 _shortcutCustomizationUiState.value =
                     AddShortcutDialog(
                         shortcutLabel = requestInfo.label,
@@ -75,12 +76,10 @@
                             shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey(),
                         pressedKeys = emptyList(),
                     )
-                shortcutCustomizationInteractor.onCustomizationRequested(requestInfo)
             }
 
-            is ShortcutCustomizationRequestInfo.Delete -> {
+            is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> {
                 _shortcutCustomizationUiState.value = DeleteShortcutDialog
-                shortcutCustomizationInteractor.onCustomizationRequested(requestInfo)
             }
 
             ShortcutCustomizationRequestInfo.Reset -> {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt
deleted file mode 100644
index 779b27b..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the keyguard bottom area refactor flag. */
-@Suppress("NOTHING_TO_INLINE")
-object KeyguardBottomAreaRefactor {
-    /** The aconfig flag name */
-    const val FLAG_NAME = Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
-
-    /** A token used for dependency declaration */
-    val token: FlagToken
-        get() = FlagToken(FLAG_NAME, isEnabled)
-
-    /** Is the refactor enabled */
-    @JvmStatic
-    inline val isEnabled
-        get() = Flags.keyguardBottomAreaRefactor()
-
-    /**
-     * 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/keyguard/KeyguardIndication.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java
index a0b25b9..984541b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndication.java
@@ -27,7 +27,7 @@
  * Data class containing display information (message, icon, styling) for indication to show at
  * the bottom of the keyguard.
  *
- * See {@link com.android.systemui.statusbar.phone.KeyguardBottomAreaView}.
+ * See {@link com.android.systemui.keyguard.ui.view.KeyguardRootView}.
  */
 public class KeyguardIndication {
     @Nullable
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 5ec6d37..e8eb497 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -41,7 +41,6 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
 import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder
-import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
 import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder
 import com.android.systemui.keyguard.ui.composable.LockscreenContent
@@ -50,7 +49,6 @@
 import com.android.systemui.keyguard.ui.view.KeyguardRootView
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel
@@ -59,7 +57,6 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.shade.NotificationShadeWindowView
 import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.KeyguardIndicationController
@@ -86,9 +83,6 @@
 constructor(
     private val keyguardRootView: KeyguardRootView,
     private val keyguardRootViewModel: KeyguardRootViewModel,
-    private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel,
-    private val notificationShadeWindowView: NotificationShadeWindowView,
-    private val indicationController: KeyguardIndicationController,
     private val screenOffAnimationController: ScreenOffAnimationController,
     private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
     private val chipbarCoordinator: ChipbarCoordinator,
@@ -163,23 +157,6 @@
         }
     }
 
-    fun bindIndicationArea() {
-        indicationAreaHandle?.dispose()
-
-        if (!KeyguardBottomAreaRefactor.isEnabled) {
-            keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let {
-                keyguardRootView.removeView(it)
-            }
-        }
-
-        indicationAreaHandle =
-            KeyguardIndicationAreaBinder.bind(
-                notificationShadeWindowView.requireViewById(R.id.keyguard_indication_area),
-                keyguardIndicationAreaViewModel,
-                indicationController,
-            )
-    }
-
     /** Initialize views so that corresponding controllers have a view set. */
     private fun initializeViews() {
         val indicationArea = KeyguardIndicationArea(context, null)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 096439b..4370abf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -62,6 +62,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransitionModule;
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransitionModule;
 import com.android.systemui.keyguard.ui.view.AlternateBouncerWindowViewBinder;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModelModule;
 import com.android.systemui.log.SessionTracker;
@@ -112,6 +113,7 @@
         includes = {
             DeviceEntryIconTransitionModule.class,
             FalsingModule.class,
+            PrimaryBouncerTransitionModule.class,
             KeyguardDataQuickAffordanceModule.class,
             KeyguardQuickAffordancesCombinedViewModelModule.class,
             KeyguardRepositoryModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index d3c17cc..ac04dd5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -75,12 +75,6 @@
      */
     val animateBottomAreaDozingTransitions: StateFlow<Boolean>
 
-    /**
-     * Observable for the current amount of alpha that should be used for rendering the bottom area.
-     * UI.
-     */
-    val bottomAreaAlpha: StateFlow<Float>
-
     val keyguardAlpha: StateFlow<Float>
 
     val panelAlpha: MutableStateFlow<Float>
@@ -283,9 +277,6 @@
     /** Sets whether the bottom area UI should animate the transition out of doze state. */
     fun setAnimateDozingTransitions(animate: Boolean)
 
-    /** Sets the current amount of alpha that should be used for rendering the bottom area. */
-    @Deprecated("Deprecated as part of b/278057014") fun setBottomAreaAlpha(alpha: Float)
-
     /** Sets the current amount of alpha that should be used for rendering the keyguard. */
     fun setKeyguardAlpha(alpha: Float)
 
@@ -392,9 +383,6 @@
     override val animateBottomAreaDozingTransitions =
         _animateBottomAreaDozingTransitions.asStateFlow()
 
-    private val _bottomAreaAlpha = MutableStateFlow(1f)
-    override val bottomAreaAlpha = _bottomAreaAlpha.asStateFlow()
-
     private val _keyguardAlpha = MutableStateFlow(1f)
     override val keyguardAlpha = _keyguardAlpha.asStateFlow()
 
@@ -675,10 +663,6 @@
         _animateBottomAreaDozingTransitions.value = animate
     }
 
-    override fun setBottomAreaAlpha(alpha: Float) {
-        _bottomAreaAlpha.value = alpha
-    }
-
     override fun setKeyguardAlpha(alpha: Float) {
         _keyguardAlpha.value = alpha
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
deleted file mode 100644
index 53f2416..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- *  Copyright (C) 2022 The Android Open Source Project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.domain.interactor
-
-import com.android.systemui.common.shared.model.Position
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-/** Encapsulates business-logic specifically related to the keyguard bottom area. */
-@SysUISingleton
-class KeyguardBottomAreaInteractor
-@Inject
-constructor(
-    private val repository: KeyguardRepository,
-) {
-    /** Whether to animate the next doze mode transition. */
-    val animateDozingTransitions: Flow<Boolean> = repository.animateBottomAreaDozingTransitions
-    /** The amount of alpha for the UI components of the bottom area. */
-    val alpha: Flow<Float> = repository.bottomAreaAlpha
-    /** The position of the keyguard clock. */
-    private val _clockPosition = MutableStateFlow(Position(0, 0))
-    /** See [ClockSection] */
-    @Deprecated("with MigrateClocksToBlueprint.isEnabled")
-    val clockPosition: Flow<Position> = _clockPosition.asStateFlow()
-
-    fun setClockPosition(x: Int, y: Int) {
-        _clockPosition.value = Position(x, y)
-    }
-
-    fun setAlpha(alpha: Float) {
-        repository.setBottomAreaAlpha(alpha)
-    }
-
-    fun setAnimateDozingTransitions(animate: Boolean) {
-        repository.setAnimateDozingTransitions(animate)
-    }
-
-    /**
-     * Returns whether the keyguard bottom area should be constrained to the top of the lock icon
-     */
-    fun shouldConstrainToTopOfLockIcon(): Boolean = repository.isUdfpsSupported()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 5bad016..261c130 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -24,7 +24,6 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.customization.R as customR
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
@@ -51,26 +50,11 @@
                         (prevBlueprint, blueprint) ->
                         val config = Config.DEFAULT
                         val transition =
-                            if (
-                                !KeyguardBottomAreaRefactor.isEnabled &&
-                                    prevBlueprint != null &&
-                                    prevBlueprint != blueprint
-                            ) {
-                                BaseBlueprintTransition(clockViewModel)
-                                    .addTransition(
-                                        IntraBlueprintTransition(
-                                            config,
-                                            clockViewModel,
-                                            smartspaceViewModel,
-                                        )
-                                    )
-                            } else {
-                                IntraBlueprintTransition(
-                                    config,
-                                    clockViewModel,
-                                    smartspaceViewModel,
-                                )
-                            }
+                            IntraBlueprintTransition(
+                                config,
+                                clockViewModel,
+                                smartspaceViewModel,
+                            )
 
                         viewModel.runTransition(constraintLayout, transition, config) {
                             // Replace sections from the previous blueprint with the new ones
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
deleted file mode 100644
index c59fe53..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ /dev/null
@@ -1,586 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.binder
-
-import android.annotation.SuppressLint
-import android.content.res.ColorStateList
-import android.graphics.Rect
-import android.graphics.drawable.Animatable2
-import android.util.Size
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewGroup.MarginLayoutParams
-import android.view.WindowInsets
-import android.widget.ImageView
-import androidx.core.animation.CycleInterpolator
-import androidx.core.animation.ObjectAnimator
-import androidx.core.view.isInvisible
-import androidx.core.view.isVisible
-import androidx.core.view.marginLeft
-import androidx.core.view.marginRight
-import androidx.core.view.marginTop
-import androidx.core.view.updateLayoutParams
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.app.animation.Interpolators
-import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.animation.ActivityTransitionAnimator
-import com.android.systemui.animation.Expandable
-import com.android.systemui.animation.view.LaunchableLinearLayout
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.ui.binder.IconViewBinder
-import com.android.systemui.common.ui.binder.TextViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
-import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils
-import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils.LAUNCH_SOURCE_KEYGUARD
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.doOnEnd
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-
-/**
- * Binds a keyguard bottom area view to its view-model.
- *
- * To use this properly, users should maintain a one-to-one relationship between the [View] and the
- * view-binding, binding each view only once. It is okay and expected for the same instance of the
- * view-model to be reused for multiple view/view-binder bindings.
- */
-@OptIn(ExperimentalCoroutinesApi::class)
-@Deprecated("Deprecated as part of b/278057014")
-object KeyguardBottomAreaViewBinder {
-
-    private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
-    private const val SCALE_SELECTED_BUTTON = 1.23f
-    private const val DIM_ALPHA = 0.3f
-    private const val TAG = "KeyguardBottomAreaViewBinder"
-
-    /**
-     * Defines interface for an object that acts as the binding between the view and its view-model.
-     *
-     * Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after
-     * it is bound.
-     */
-    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-    @Deprecated("Deprecated as part of b/278057014")
-    interface Binding {
-        /** Notifies that device configuration has changed. */
-        fun onConfigurationChanged()
-
-        /**
-         * Returns whether the keyguard bottom area should be constrained to the top of the lock
-         * icon
-         */
-        fun shouldConstrainToTopOfLockIcon(): Boolean
-
-        /** Destroys this binding, releases resources, and cancels any coroutines. */
-        fun destroy()
-    }
-
-    /** Binds the view to the view-model, continuing to update the former based on the latter. */
-    @Deprecated("Deprecated as part of b/278057014")
-    @SuppressLint("ClickableViewAccessibility")
-    @JvmStatic
-    fun bind(
-        view: ViewGroup,
-        viewModel: KeyguardBottomAreaViewModel,
-        falsingManager: FalsingManager?,
-        vibratorHelper: VibratorHelper?,
-        activityStarter: ActivityStarter?,
-        messageDisplayer: (Int) -> Unit,
-    ): Binding {
-        val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
-        val startButton: ImageView = view.requireViewById(R.id.start_button)
-        val endButton: ImageView = view.requireViewById(R.id.end_button)
-        val overlayContainer: View = view.requireViewById(R.id.overlay_container)
-        val settingsMenu: LaunchableLinearLayout =
-            view.requireViewById(R.id.keyguard_settings_button)
-
-        startButton.setOnApplyWindowInsetsListener { inView, windowInsets ->
-            val bottomInset = windowInsets.displayCutout?.safeInsetBottom ?: 0
-            val marginBottom =
-                inView.resources.getDimension(R.dimen.keyguard_affordance_vertical_offset).toInt()
-            inView.layoutParams =
-                (inView.layoutParams as MarginLayoutParams).apply {
-                    setMargins(
-                        inView.marginLeft,
-                        inView.marginTop,
-                        inView.marginRight,
-                        marginBottom + bottomInset
-                    )
-                }
-            WindowInsets.CONSUMED
-        }
-
-        endButton.setOnApplyWindowInsetsListener { inView, windowInsets ->
-            val bottomInset = windowInsets.displayCutout?.safeInsetBottom ?: 0
-            val marginBottom =
-                inView.resources.getDimension(R.dimen.keyguard_affordance_vertical_offset).toInt()
-            inView.layoutParams =
-                (inView.layoutParams as MarginLayoutParams).apply {
-                    setMargins(
-                        inView.marginLeft,
-                        inView.marginTop,
-                        inView.marginRight,
-                        marginBottom + bottomInset
-                    )
-                }
-            WindowInsets.CONSUMED
-        }
-
-        view.clipChildren = false
-        view.clipToPadding = false
-        view.setOnTouchListener { _, event ->
-            if (settingsMenu.isVisible) {
-                val hitRect = Rect()
-                settingsMenu.getHitRect(hitRect)
-                if (!hitRect.contains(event.x.toInt(), event.y.toInt())) {
-                    viewModel.onTouchedOutsideLockScreenSettingsMenu()
-                }
-            }
-
-            false
-        }
-
-        val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
-
-        val disposableHandle =
-            view.repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.STARTED) {
-                    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-                    launch("$TAG#viewModel.startButton") {
-                        viewModel.startButton.collect { buttonModel ->
-                            updateButton(
-                                view = startButton,
-                                viewModel = buttonModel,
-                                falsingManager = falsingManager,
-                                messageDisplayer = messageDisplayer,
-                                vibratorHelper = vibratorHelper,
-                            )
-                        }
-                    }
-
-                    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-                    launch("$TAG#viewModel.endButton") {
-                        viewModel.endButton.collect { buttonModel ->
-                            updateButton(
-                                view = endButton,
-                                viewModel = buttonModel,
-                                falsingManager = falsingManager,
-                                messageDisplayer = messageDisplayer,
-                                vibratorHelper = vibratorHelper,
-                            )
-                        }
-                    }
-
-                    launch("$TAG#viewModel.isOverlayContainerVisible") {
-                        viewModel.isOverlayContainerVisible.collect { isVisible ->
-                            overlayContainer.visibility =
-                                if (isVisible) {
-                                    View.VISIBLE
-                                } else {
-                                    View.INVISIBLE
-                                }
-                        }
-                    }
-
-                    launch("$TAG#viewModel.alpha") {
-                        viewModel.alpha.collect { alpha ->
-                            ambientIndicationArea?.apply {
-                                this.importantForAccessibility =
-                                    if (alpha == 0f) {
-                                        View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                                    } else {
-                                        View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
-                                    }
-                                this.alpha = alpha
-                            }
-                        }
-                    }
-
-                    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-                    launch("$TAG#updateButtonAlpha") {
-                        updateButtonAlpha(
-                            view = startButton,
-                            viewModel = viewModel.startButton,
-                            alphaFlow = viewModel.alpha,
-                        )
-                    }
-
-                    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-                    launch("$TAG#updateButtonAlpha") {
-                        updateButtonAlpha(
-                            view = endButton,
-                            viewModel = viewModel.endButton,
-                            alphaFlow = viewModel.alpha,
-                        )
-                    }
-
-                    launch("$TAG#viewModel.indicationAreaTranslationX") {
-                        viewModel.indicationAreaTranslationX.collect { translationX ->
-                            ambientIndicationArea?.translationX = translationX
-                        }
-                    }
-
-                    launch("$TAG#viewModel.indicationAreaTranslationY") {
-                        configurationBasedDimensions
-                            .map { it.defaultBurnInPreventionYOffsetPx }
-                            .flatMapLatest { defaultBurnInOffsetY ->
-                                viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
-                            }
-                            .collect { translationY ->
-                                ambientIndicationArea?.translationY = translationY
-                            }
-                    }
-
-                    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-                    launch("$TAG#startButton.updateLayoutParams<ViewGroup") {
-                        configurationBasedDimensions.collect { dimensions ->
-                            startButton.updateLayoutParams<ViewGroup.LayoutParams> {
-                                width = dimensions.buttonSizePx.width
-                                height = dimensions.buttonSizePx.height
-                            }
-                            endButton.updateLayoutParams<ViewGroup.LayoutParams> {
-                                width = dimensions.buttonSizePx.width
-                                height = dimensions.buttonSizePx.height
-                            }
-                        }
-                    }
-
-                    launch("$TAG#viewModel.settingsMenuViewModel") {
-                        viewModel.settingsMenuViewModel.isVisible.distinctUntilChanged().collect {
-                            isVisible ->
-                            settingsMenu.animateVisibility(visible = isVisible)
-                            if (isVisible) {
-                                vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Activated)
-                                settingsMenu.setOnTouchListener(
-                                    KeyguardSettingsButtonOnTouchListener(
-                                        viewModel = viewModel.settingsMenuViewModel,
-                                    )
-                                )
-                                IconViewBinder.bind(
-                                    icon = viewModel.settingsMenuViewModel.icon,
-                                    view = settingsMenu.requireViewById(R.id.icon),
-                                )
-                                TextViewBinder.bind(
-                                    view = settingsMenu.requireViewById(R.id.text),
-                                    viewModel = viewModel.settingsMenuViewModel.text,
-                                )
-                            }
-                        }
-                    }
-
-                    // activityStarter will only be null when rendering the preview that
-                    // shows up in the Wallpaper Picker app. If we do that, then the
-                    // settings menu should never be visible.
-                    if (activityStarter != null) {
-                        launch("$TAG#viewModel.settingsMenuViewModel") {
-                            viewModel.settingsMenuViewModel.shouldOpenSettings
-                                .filter { it }
-                                .collect {
-                                    navigateToLockScreenSettings(
-                                        activityStarter = activityStarter,
-                                        view = settingsMenu,
-                                    )
-                                    viewModel.settingsMenuViewModel.onSettingsShown()
-                                }
-                        }
-                    }
-                }
-            }
-
-        return object : Binding {
-            override fun onConfigurationChanged() {
-                configurationBasedDimensions.value = loadFromResources(view)
-            }
-
-            override fun shouldConstrainToTopOfLockIcon(): Boolean =
-                viewModel.shouldConstrainToTopOfLockIcon()
-
-            override fun destroy() {
-                disposableHandle.dispose()
-            }
-        }
-    }
-
-    @Deprecated("Deprecated as part of b/278057014")
-    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-    @SuppressLint("ClickableViewAccessibility")
-    private fun updateButton(
-        view: ImageView,
-        viewModel: KeyguardQuickAffordanceViewModel,
-        falsingManager: FalsingManager?,
-        messageDisplayer: (Int) -> Unit,
-        vibratorHelper: VibratorHelper?,
-    ) {
-        if (!viewModel.isVisible) {
-            view.isInvisible = true
-            return
-        }
-
-        if (!view.isVisible) {
-            view.isVisible = true
-            if (viewModel.animateReveal) {
-                view.alpha = 0f
-                view.translationY = view.height / 2f
-                view
-                    .animate()
-                    .alpha(1f)
-                    .translationY(0f)
-                    .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
-                    .setDuration(EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS)
-                    .start()
-            }
-        }
-
-        IconViewBinder.bind(viewModel.icon, view)
-
-        (view.drawable as? Animatable2)?.let { animatable ->
-            (viewModel.icon as? Icon.Resource)?.res?.let { iconResourceId ->
-                // Always start the animation (we do call stop() below, if we need to skip it).
-                animatable.start()
-
-                if (view.tag != iconResourceId) {
-                    // Here when we haven't run the animation on a previous update.
-                    //
-                    // Save the resource ID for next time, so we know not to re-animate the same
-                    // animation again.
-                    view.tag = iconResourceId
-                } else {
-                    // Here when we've already done this animation on a previous update and want to
-                    // skip directly to the final frame of the animation to avoid running it.
-                    //
-                    // By calling stop after start, we go to the final frame of the animation.
-                    animatable.stop()
-                }
-            }
-        }
-
-        view.isActivated = viewModel.isActivated
-        view.drawable.setTint(
-            view.context.getColor(
-                if (viewModel.isActivated) {
-                    com.android.internal.R.color.materialColorOnPrimaryFixed
-                } else {
-                    com.android.internal.R.color.materialColorOnSurface
-                }
-            )
-        )
-
-        view.backgroundTintList =
-            if (!viewModel.isSelected) {
-                ColorStateList.valueOf(
-                    view.context.getColor(
-                        if (viewModel.isActivated) {
-                            com.android.internal.R.color.materialColorPrimaryFixed
-                        } else {
-                            com.android.internal.R.color.materialColorSurfaceContainerHigh
-                        }
-                    )
-                )
-            } else {
-                null
-            }
-        view
-            .animate()
-            .scaleX(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
-            .scaleY(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
-            .start()
-
-        view.isClickable = viewModel.isClickable
-        if (viewModel.isClickable) {
-            if (viewModel.useLongPress) {
-                val onTouchListener =
-                    KeyguardQuickAffordanceOnTouchListener(
-                        view,
-                        viewModel,
-                        messageDisplayer,
-                        vibratorHelper,
-                        falsingManager,
-                    )
-                view.setOnTouchListener(onTouchListener)
-                view.setOnClickListener {
-                    messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
-                    val amplitude =
-                        view.context.resources
-                            .getDimensionPixelSize(R.dimen.keyguard_affordance_shake_amplitude)
-                            .toFloat()
-                    val shakeAnimator =
-                        ObjectAnimator.ofFloat(
-                            view,
-                            "translationX",
-                            -amplitude / 2,
-                            amplitude / 2,
-                        )
-                    shakeAnimator.duration =
-                        KeyguardBottomAreaVibrations.ShakeAnimationDuration.inWholeMilliseconds
-                    shakeAnimator.interpolator =
-                        CycleInterpolator(KeyguardBottomAreaVibrations.ShakeAnimationCycles)
-                    shakeAnimator.doOnEnd { view.translationX = 0f }
-                    shakeAnimator.start()
-
-                    vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake)
-                }
-                view.onLongClickListener =
-                    OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener)
-            } else {
-                view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
-            }
-        } else {
-            view.onLongClickListener = null
-            view.setOnClickListener(null)
-            view.setOnTouchListener(null)
-        }
-
-        view.isSelected = viewModel.isSelected
-    }
-
-    @Deprecated("Deprecated as part of b/278057014")
-    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-    private suspend fun updateButtonAlpha(
-        view: View,
-        viewModel: Flow<KeyguardQuickAffordanceViewModel>,
-        alphaFlow: Flow<Float>,
-    ) {
-        combine(viewModel.map { it.isDimmed }, alphaFlow) { isDimmed, alpha ->
-                if (isDimmed) DIM_ALPHA else alpha
-            }
-            .collect { view.alpha = it }
-    }
-
-    @Deprecated("Deprecated as part of b/278057014")
-    private fun View.animateVisibility(visible: Boolean) {
-        animate()
-            .withStartAction {
-                if (visible) {
-                    alpha = 0f
-                    isVisible = true
-                }
-            }
-            .alpha(if (visible) 1f else 0f)
-            .withEndAction {
-                if (!visible) {
-                    isVisible = false
-                }
-            }
-            .start()
-    }
-
-    @Deprecated("Deprecated as part of b/278057014")
-    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-    private class OnLongClickListener(
-        private val falsingManager: FalsingManager?,
-        private val viewModel: KeyguardQuickAffordanceViewModel,
-        private val vibratorHelper: VibratorHelper?,
-        private val onTouchListener: KeyguardQuickAffordanceOnTouchListener
-    ) : View.OnLongClickListener {
-        override fun onLongClick(view: View): Boolean {
-            if (falsingManager?.isFalseLongTap(FalsingManager.MODERATE_PENALTY) == true) {
-                return true
-            }
-
-            if (viewModel.configKey != null) {
-                viewModel.onClicked(
-                    KeyguardQuickAffordanceViewModel.OnClickedParameters(
-                        configKey = viewModel.configKey,
-                        expandable = Expandable.fromView(view),
-                        slotId = viewModel.slotId,
-                    )
-                )
-                vibratorHelper?.vibrate(
-                    if (viewModel.isActivated) {
-                        KeyguardBottomAreaVibrations.Activated
-                    } else {
-                        KeyguardBottomAreaVibrations.Deactivated
-                    }
-                )
-            }
-
-            onTouchListener.cancel()
-            return true
-        }
-
-        override fun onLongClickUseDefaultHapticFeedback(view: View) = false
-    }
-
-    @Deprecated("Deprecated as part of b/278057014")
-    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-    private class OnClickListener(
-        private val viewModel: KeyguardQuickAffordanceViewModel,
-        private val falsingManager: FalsingManager,
-    ) : View.OnClickListener {
-        override fun onClick(view: View) {
-            if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                return
-            }
-
-            if (viewModel.configKey != null) {
-                viewModel.onClicked(
-                    KeyguardQuickAffordanceViewModel.OnClickedParameters(
-                        configKey = viewModel.configKey,
-                        expandable = Expandable.fromView(view),
-                        slotId = viewModel.slotId,
-                    )
-                )
-            }
-        }
-    }
-
-    @Deprecated("Deprecated as part of b/278057014")
-    private fun loadFromResources(view: View): ConfigurationBasedDimensions {
-        return ConfigurationBasedDimensions(
-            defaultBurnInPreventionYOffsetPx =
-                view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset),
-            buttonSizePx =
-                Size(
-                    view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width),
-                    view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
-                ),
-        )
-    }
-
-    @Deprecated("Deprecated as part of b/278057014")
-    /** Opens the wallpaper picker screen after the device is unlocked by the user. */
-    private fun navigateToLockScreenSettings(
-        activityStarter: ActivityStarter,
-        view: View,
-    ) {
-        activityStarter.postStartActivityDismissingKeyguard(
-            WallpaperPickerIntentUtils.getIntent(view.context, LAUNCH_SOURCE_KEYGUARD),
-            /* delay= */ 0,
-            /* animationController= */ ActivityTransitionAnimator.Controller.fromView(view),
-            /* customMessage= */ view.context.getString(R.string.keyguard_unlock_to_customize_ls)
-        )
-    }
-
-    @Deprecated("Deprecated as part of b/278057014")
-    // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
-    private data class ConfigurationBasedDimensions(
-        val defaultBurnInPreventionYOffsetPx: Int,
-        val buttonSizePx: Size,
-    )
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 273d763..0a958e9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -27,7 +27,6 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.ClockSize
@@ -73,7 +72,6 @@
                     // When changing to new clock, we need to remove old views from burnInLayer
                     var lastClock: ClockController? = null
                     launch {
-                            if (!MigrateClocksToBlueprint.isEnabled) return@launch
                             viewModel.currentClock.collect { currentClock ->
                                 if (lastClock != currentClock) {
                                     cleanupClockViews(
@@ -99,7 +97,6 @@
                         }
 
                     launch {
-                        if (!MigrateClocksToBlueprint.isEnabled) return@launch
                         viewModel.clockSize.collect { clockSize ->
                             updateBurnInLayer(keyguardRootView, viewModel, clockSize)
                             blueprintInteractor.refreshBlueprint(Type.ClockSize)
@@ -107,7 +104,6 @@
                     }
 
                     launch {
-                        if (!MigrateClocksToBlueprint.isEnabled) return@launch
                         viewModel.clockShouldBeCentered.collect {
                             viewModel.currentClock.value?.let {
                                 // TODO(b/301502635): remove "!it.config.useCustomClockScene" when
@@ -125,7 +121,6 @@
                     }
 
                     launch {
-                        if (!MigrateClocksToBlueprint.isEnabled) return@launch
                         combine(
                                 viewModel.hasAodIcons,
                                 rootViewModel.isNotifIconContainerVisible.map { it.value },
@@ -143,7 +138,6 @@
                     }
 
                     launch {
-                        if (!MigrateClocksToBlueprint.isEnabled) return@launch
                         aodBurnInViewModel.movement.collect { burnInModel ->
                             viewModel.currentClock.value
                                 ?.largeClock
@@ -159,7 +153,6 @@
                     }
 
                     launch {
-                        if (!MigrateClocksToBlueprint.isEnabled) return@launch
                         viewModel.largeClockTextSize.collect { fontSizePx ->
                             viewModel.currentClock.value
                                 ?.largeClock
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 8b947a3..92b49ed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -23,8 +23,6 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
-import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
@@ -75,16 +73,6 @@
         disposables +=
             view.repeatWhenAttached {
                 repeatOnLifecycle(Lifecycle.State.STARTED) {
-                    launch("$TAG#viewModel.alpha") {
-                        // Do not independently apply alpha, as [KeyguardRootViewModel] should work
-                        // for this and all its children
-                        if (
-                            !(MigrateClocksToBlueprint.isEnabled ||
-                                KeyguardBottomAreaRefactor.isEnabled)
-                        ) {
-                            viewModel.alpha.collect { alpha -> view.alpha = alpha }
-                        }
-                    }
 
                     launch("$TAG#viewModel.indicationAreaTranslationX") {
                         viewModel.indicationAreaTranslationX.collect { translationX ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index c0b3d83..1964cb2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -136,8 +136,16 @@
         viewModel: KeyguardPreviewClockViewModel,
     ) {
         val cs = ConstraintSet().apply { clone(rootView) }
-        previewClock.largeClock.layout.applyPreviewConstraints(clockPreviewConfig, cs)
-        previewClock.smallClock.layout.applyPreviewConstraints(clockPreviewConfig, cs)
+
+        val configWithUpdatedLockId =
+            if (rootView.getViewById(lockId) != null) {
+                clockPreviewConfig.copy(lockId = lockId)
+            } else {
+                clockPreviewConfig
+            }
+
+        previewClock.largeClock.layout.applyPreviewConstraints(configWithUpdatedLockId, cs)
+        previewClock.smallClock.layout.applyPreviewConstraints(configWithUpdatedLockId, cs)
 
         // When selectedClockSize is the initial value, make both clocks invisible to avoid
         // flickering
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
index 5c8a234..8725cdd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
@@ -71,9 +71,6 @@
 
     /**
      * Defines interface for an object that acts as the binding between the view and its view-model.
-     *
-     * Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after
-     * it is bound.
      */
     interface Binding {
         /** Notifies that device configuration has changed. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index f121aab..a2ce4ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -31,14 +31,12 @@
 import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.view.ViewGroup.OnHierarchyChangeListener
-import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
 import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.OnBackPressedDispatcherOwner
 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.app.animation.Interpolators
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
@@ -54,9 +52,7 @@
 import com.android.systemui.common.ui.view.onTouchListener
 import com.android.systemui.customization.R as customR
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.KeyguardViewMediator
-import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -90,12 +86,9 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.update
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -125,35 +118,33 @@
         val disposables = DisposableHandles()
         val childViews = mutableMapOf<Int, View>()
 
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            disposables +=
-                view.onTouchListener { _, event ->
-                    var consumed = false
-                    if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) {
-                        // signifies a primary button click down has reached keyguardrootview
-                        // we need to return true here otherwise an ACTION_UP will never arrive
-                        if (Flags.nonTouchscreenDevicesBypassFalsing()) {
-                            if (
-                                event.action == MotionEvent.ACTION_DOWN &&
-                                    event.buttonState == MotionEvent.BUTTON_PRIMARY &&
-                                    !event.isTouchscreenSource()
-                            ) {
-                                consumed = true
-                            } else if (
-                                event.action == MotionEvent.ACTION_UP &&
-                                    !event.isTouchscreenSource()
-                            ) {
-                                statusBarKeyguardViewManager?.showBouncer(true)
-                                consumed = true
-                            }
+        disposables +=
+            view.onTouchListener { _, event ->
+                var consumed = false
+                if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) {
+                    // signifies a primary button click down has reached keyguardrootview
+                    // we need to return true here otherwise an ACTION_UP will never arrive
+                    if (Flags.nonTouchscreenDevicesBypassFalsing()) {
+                        if (
+                            event.action == MotionEvent.ACTION_DOWN &&
+                            event.buttonState == MotionEvent.BUTTON_PRIMARY &&
+                            !event.isTouchscreenSource()
+                        ) {
+                            consumed = true
+                        } else if (
+                            event.action == MotionEvent.ACTION_UP &&
+                            !event.isTouchscreenSource()
+                        ) {
+                            statusBarKeyguardViewManager?.showBouncer(true)
+                            consumed = true
                         }
-                        viewModel.setRootViewLastTapPosition(
-                            Point(event.x.toInt(), event.y.toInt())
-                        )
                     }
-                    consumed
+                    viewModel.setRootViewLastTapPosition(
+                        Point(event.x.toInt(), event.y.toInt())
+                    )
                 }
-        }
+                consumed
+            }
 
         val burnInParams = MutableStateFlow(BurnInParameters())
         val viewState = ViewStateAccessor(alpha = { view.alpha })
@@ -161,21 +152,19 @@
         disposables +=
             view.repeatWhenAttached(mainImmediateDispatcher) {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
-                    if (MigrateClocksToBlueprint.isEnabled) {
-                        launch("$TAG#topClippingBounds") {
-                            val clipBounds = Rect()
-                            viewModel.topClippingBounds.collect { clipTop ->
-                                if (clipTop == null) {
-                                    view.setClipBounds(null)
-                                } else {
-                                    clipBounds.apply {
-                                        top = clipTop
-                                        left = view.getLeft()
-                                        right = view.getRight()
-                                        bottom = view.getBottom()
-                                    }
-                                    view.setClipBounds(clipBounds)
+                    launch("$TAG#topClippingBounds") {
+                        val clipBounds = Rect()
+                        viewModel.topClippingBounds.collect { clipTop ->
+                            if (clipTop == null) {
+                                view.setClipBounds(null)
+                            } else {
+                                clipBounds.apply {
+                                    top = clipTop
+                                    left = view.getLeft()
+                                    right = view.getRight()
+                                    bottom = view.getBottom()
                                 }
+                                view.setClipBounds(clipBounds)
                             }
                         }
                     }
@@ -183,47 +172,41 @@
                     launch("$TAG#alpha") {
                         viewModel.alpha(viewState).collect { alpha ->
                             view.alpha = alpha
-                            if (KeyguardBottomAreaRefactor.isEnabled) {
-                                childViews[statusViewId]?.alpha = alpha
-                                childViews[burnInLayerId]?.alpha = alpha
-                            }
+                            childViews[statusViewId]?.alpha = alpha
+                            childViews[burnInLayerId]?.alpha = alpha
                         }
                     }
 
-                    if (MigrateClocksToBlueprint.isEnabled) {
-                        launch("$TAG#translationY") {
-                            // When translation happens in burnInLayer, it won't be weather clock
-                            // large clock isn't added to burnInLayer due to its scale transition
-                            // so we also need to add translation to it here
-                            // same as translationX
-                            viewModel.translationY.collect { y ->
-                                childViews[burnInLayerId]?.translationY = y
-                                childViews[largeClockId]?.translationY = y
-                                childViews[aodNotificationIconContainerId]?.translationY = y
-                            }
+                    launch("$TAG#translationY") {
+                        // When translation happens in burnInLayer, it won't be weather clock large
+                        // clock isn't added to burnInLayer due to its scale transition so we also
+                        // need to add translation to it here same as translationX
+                        viewModel.translationY.collect { y ->
+                            childViews[burnInLayerId]?.translationY = y
+                            childViews[largeClockId]?.translationY = y
+                            childViews[aodNotificationIconContainerId]?.translationY = y
                         }
+                    }
 
-                        launch("$TAG#translationX") {
-                            viewModel.translationX.collect { state ->
-                                val px = state.value ?: return@collect
-                                when {
-                                    state.isToOrFrom(KeyguardState.AOD) -> {
-                                        // Large Clock is not translated in the x direction
-                                        childViews[burnInLayerId]?.translationX = px
-                                        childViews[aodNotificationIconContainerId]?.translationX =
-                                            px
-                                    }
-                                    state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> {
-                                        for ((key, childView) in childViews.entries) {
-                                            when (key) {
-                                                indicationArea,
-                                                startButton,
-                                                endButton,
-                                                deviceEntryIcon -> {
-                                                    // Do not move these views
-                                                }
-                                                else -> childView.translationX = px
+                    launch("$TAG#translationX") {
+                        viewModel.translationX.collect { state ->
+                            val px = state.value ?: return@collect
+                            when {
+                                state.isToOrFrom(KeyguardState.AOD) -> {
+                                    // Large Clock is not translated in the x direction
+                                    childViews[burnInLayerId]?.translationX = px
+                                    childViews[aodNotificationIconContainerId]?.translationX = px
+                                }
+                                state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> {
+                                    for ((key, childView) in childViews.entries) {
+                                        when (key) {
+                                            indicationArea,
+                                            startButton,
+                                            endButton,
+                                            deviceEntryIcon -> {
+                                                // Do not move these views
                                             }
+                                            else -> childView.translationX = px
                                         }
                                     }
                                 }
@@ -263,95 +246,92 @@
                         }
                     }
 
-                    if (MigrateClocksToBlueprint.isEnabled) {
-                        launch {
-                            viewModel.burnInLayerVisibility.collect { visibility ->
-                                childViews[burnInLayerId]?.visibility = visibility
-                            }
+                    launch {
+                        viewModel.burnInLayerVisibility.collect { visibility ->
+                            childViews[burnInLayerId]?.visibility = visibility
                         }
+                    }
 
-                        launch {
-                            viewModel.burnInLayerAlpha.collect { alpha ->
-                                childViews[statusViewId]?.alpha = alpha
-                            }
+                    launch {
+                        viewModel.burnInLayerAlpha.collect { alpha ->
+                            childViews[statusViewId]?.alpha = alpha
                         }
+                    }
 
-                        launch {
-                            viewModel.lockscreenStateAlpha(viewState).collect { alpha ->
-                                childViews[statusViewId]?.alpha = alpha
-                            }
+                    launch {
+                        viewModel.lockscreenStateAlpha(viewState).collect { alpha ->
+                            childViews[statusViewId]?.alpha = alpha
                         }
+                    }
 
-                        launch {
-                            viewModel.scale.collect { scaleViewModel ->
-                                if (scaleViewModel.scaleClockOnly) {
-                                    // For clocks except weather clock, we have scale transition
-                                    // besides translate
-                                    childViews[largeClockId]?.let {
-                                        it.scaleX = scaleViewModel.scale
-                                        it.scaleY = scaleViewModel.scale
-                                    }
+                    launch {
+                        viewModel.scale.collect { scaleViewModel ->
+                            if (scaleViewModel.scaleClockOnly) {
+                                // For clocks except weather clock, we have scale transition besides
+                                // translate
+                                childViews[largeClockId]?.let {
+                                    it.scaleX = scaleViewModel.scale
+                                    it.scaleY = scaleViewModel.scale
                                 }
                             }
                         }
+                    }
 
-                        launch {
-                            blueprintViewModel.currentTransition.collect { currentTransition ->
-                                // When blueprint/clock transitions end (null), make sure NSSL is in
-                                // the right place
-                                if (currentTransition == null) {
-                                    childViews[nsslPlaceholderId]?.let { notificationListPlaceholder
-                                        ->
-                                        viewModel.onNotificationContainerBoundsChanged(
-                                            notificationListPlaceholder.top.toFloat(),
-                                            notificationListPlaceholder.bottom.toFloat(),
-                                            animate = true,
-                                        )
-                                    }
-                                }
-                            }
-                        }
-
-                        launch {
-                            val iconsAppearTranslationPx =
-                                configuration
-                                    .getDimensionPixelSize(R.dimen.shelf_appear_translation)
-                                    .stateIn(this)
-                            viewModel.isNotifIconContainerVisible.collect { isVisible ->
-                                if (isVisible.value) {
-                                    blueprintViewModel.refreshBlueprint()
-                                }
-                                childViews[aodNotificationIconContainerId]
-                                    ?.setAodNotifIconContainerIsVisible(
-                                        isVisible,
-                                        iconsAppearTranslationPx.value,
-                                        screenOffAnimationController,
+                    launch {
+                        blueprintViewModel.currentTransition.collect { currentTransition ->
+                            // When blueprint/clock transitions end (null), make sure NSSL is in the
+                            // right place
+                            if (currentTransition == null) {
+                                childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
+                                    viewModel.onNotificationContainerBoundsChanged(
+                                        notificationListPlaceholder.top.toFloat(),
+                                        notificationListPlaceholder.bottom.toFloat(),
+                                        animate = true,
                                     )
+                                }
                             }
                         }
+                    }
 
-                        interactionJankMonitor?.let { jankMonitor ->
-                            launch {
-                                viewModel.goneToAodTransition.collect {
-                                    when (it.transitionState) {
-                                        TransitionState.STARTED -> {
-                                            val clockId = clockInteractor.renderedClockId
-                                            val builder =
-                                                InteractionJankMonitor.Configuration.Builder
-                                                    .withView(CUJ_SCREEN_OFF_SHOW_AOD, view)
-                                                    .setTag(clockId)
-                                            jankMonitor.begin(builder)
-                                        }
-                                        TransitionState.CANCELED ->
-                                            jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
-                                        TransitionState.FINISHED -> {
-                                            if (MigrateClocksToBlueprint.isEnabled) {
-                                                keyguardViewMediator?.maybeHandlePendingLock()
-                                            }
-                                            jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
-                                        }
-                                        TransitionState.RUNNING -> Unit
+                    launch {
+                        val iconsAppearTranslationPx =
+                            configuration
+                                .getDimensionPixelSize(R.dimen.shelf_appear_translation)
+                                .stateIn(this)
+                        viewModel.isNotifIconContainerVisible.collect { isVisible ->
+                            if (isVisible.value) {
+                                blueprintViewModel.refreshBlueprint()
+                            }
+                            childViews[aodNotificationIconContainerId]
+                                ?.setAodNotifIconContainerIsVisible(
+                                    isVisible,
+                                    iconsAppearTranslationPx.value,
+                                    screenOffAnimationController,
+                                )
+                        }
+                    }
+
+                    interactionJankMonitor?.let { jankMonitor ->
+                        launch {
+                            viewModel.goneToAodTransition.collect {
+                                when (it.transitionState) {
+                                    TransitionState.STARTED -> {
+                                        val clockId = clockInteractor.renderedClockId
+                                        val builder =
+                                            InteractionJankMonitor.Configuration.Builder.withView(
+                                                    CUJ_SCREEN_OFF_SHOW_AOD,
+                                                    view,
+                                                )
+                                                .setTag(clockId)
+                                        jankMonitor.begin(builder)
                                     }
+                                    TransitionState.CANCELED ->
+                                        jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
+                                    TransitionState.FINISHED -> {
+                                        keyguardViewMediator?.maybeHandlePendingLock()
+                                        jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
+                                    }
+                                    TransitionState.RUNNING -> Unit
                                 }
                             }
                         }
@@ -406,13 +386,11 @@
                 }
             }
 
-        if (MigrateClocksToBlueprint.isEnabled) {
-            burnInParams.update { current ->
-                current.copy(
-                    translationX = { childViews[burnInLayerId]?.translationX },
-                    translationY = { childViews[burnInLayerId]?.translationY },
-                )
-            }
+        burnInParams.update { current ->
+            current.copy(
+                translationX = { childViews[burnInLayerId]?.translationX },
+                translationY = { childViews[burnInLayerId]?.translationY },
+            )
         }
 
         disposables +=
@@ -515,20 +493,16 @@
             burnInParams.update { current ->
                 current.copy(
                     minViewY =
-                        if (MigrateClocksToBlueprint.isEnabled) {
-                            // To ensure burn-in doesn't enroach the top inset, get the min top Y
-                            childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) ->
-                                min(
-                                    currentMin,
-                                    if (!isUserVisible(view)) {
-                                        Int.MAX_VALUE
-                                    } else {
-                                        view.getTop()
-                                    },
-                                )
-                            }
-                        } else {
-                            childViews[statusViewId]?.top ?: 0
+                        // To ensure burn-in doesn't enroach the top inset, get the min top Y
+                        childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) ->
+                            min(
+                                currentMin,
+                                if (!isUserVisible(view)) {
+                                    Int.MAX_VALUE
+                                } else {
+                                    view.getTop()
+                                },
+                            )
                         }
                 )
             }
@@ -542,28 +516,6 @@
         }
     }
 
-    suspend fun bindAodNotifIconVisibility(
-        view: View,
-        isVisible: Flow<AnimatedValue<Boolean>>,
-        configuration: ConfigurationState,
-        screenOffAnimationController: ScreenOffAnimationController,
-    ) {
-        if (MigrateClocksToBlueprint.isEnabled) {
-            throw IllegalStateException("should only be called in legacy code paths")
-        }
-        coroutineScope {
-            val iconAppearTranslationPx =
-                configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this)
-            isVisible.collect { isVisible ->
-                view.setAodNotifIconContainerIsVisible(
-                    isVisible = isVisible,
-                    iconsAppearTranslationPx = iconAppearTranslationPx.value,
-                    screenOffAnimationController = screenOffAnimationController,
-                )
-            }
-        }
-    }
-
     private fun View.setAodNotifIconContainerIsVisible(
         isVisible: AnimatedValue<Boolean>,
         iconsAppearTranslationPx: Int,
@@ -578,9 +530,6 @@
             }
         when {
             !isVisible.isAnimating -> {
-                if (!MigrateClocksToBlueprint.isEnabled) {
-                    translationY = 0f
-                }
                 visibility =
                     if (isVisible.value) {
                         alpha = 1f
@@ -591,7 +540,6 @@
                     }
             }
             else -> {
-                animateInIconTranslation()
                 if (isVisible.value) {
                     CrossFadeHelper.fadeIn(this, animatorListener)
                 } else {
@@ -601,19 +549,10 @@
         }
     }
 
-    private fun View.animateInIconTranslation() {
-        if (!MigrateClocksToBlueprint.isEnabled) {
-            animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start()
-        }
-    }
-
     private fun MotionEvent.isTouchscreenSource(): Boolean {
         return device?.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) == true
     }
 
-    private fun ViewPropertyAnimator.animateInIconTranslation(): ViewPropertyAnimator =
-        setInterpolator(Interpolators.DECELERATE_QUINT).translationY(0f)
-
     private val statusViewId = R.id.keyguard_status_view
     private val burnInLayerId = R.id.burn_in_layer
     private val aodNotificationIconContainerId = R.id.aod_notification_icon_container
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index de4a1b0..213083d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -22,7 +22,6 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
@@ -44,13 +43,12 @@
         return keyguardRootView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch("$TAG#clockViewModel.hasCustomWeatherDataDisplay") {
-                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay
                         ->
                         updateDateWeatherToBurnInLayer(
                             keyguardRootView,
                             clockViewModel,
-                            smartspaceViewModel
+                            smartspaceViewModel,
                         )
                         blueprintInteractor.refreshBlueprint(
                             Config(
@@ -63,7 +61,6 @@
                 }
 
                 launch("$TAG#smartspaceViewModel.bcSmartspaceVisibility") {
-                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     smartspaceViewModel.bcSmartspaceVisibility.collect {
                         updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel)
                         blueprintInteractor.refreshBlueprint(
@@ -100,7 +97,7 @@
     private fun updateDateWeatherToBurnInLayer(
         keyguardRootView: ConstraintLayout,
         clockViewModel: KeyguardClockViewModel,
-        smartspaceViewModel: KeyguardSmartspaceViewModel
+        smartspaceViewModel: KeyguardSmartspaceViewModel,
     ) {
         if (clockViewModel.hasCustomWeatherDataDisplay.value) {
             removeDateWeatherFromBurnInLayer(keyguardRootView, smartspaceViewModel)
@@ -112,7 +109,7 @@
 
     private fun addDateWeatherToBurnInLayer(
         constraintLayout: ConstraintLayout,
-        smartspaceViewModel: KeyguardSmartspaceViewModel
+        smartspaceViewModel: KeyguardSmartspaceViewModel,
     ) {
         val burnInLayer = constraintLayout.requireViewById<Layer>(R.id.burn_in_layer)
         burnInLayer.apply {
@@ -129,7 +126,7 @@
 
     private fun removeDateWeatherFromBurnInLayer(
         constraintLayout: ConstraintLayout,
-        smartspaceViewModel: KeyguardSmartspaceViewModel
+        smartspaceViewModel: KeyguardSmartspaceViewModel,
     ) {
         val burnInLayer = constraintLayout.requireViewById<Layer>(R.id.burn_in_layer)
         burnInLayer.apply {
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 85725d2..090b659 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
@@ -48,39 +48,28 @@
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import androidx.core.view.isInvisible
-import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.policy.SystemBarUtils
 import com.android.keyguard.ClockEventController
 import com.android.keyguard.KeyguardClockSwitch
 import com.android.systemui.animation.view.LaunchableImageView
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.communal.ui.binder.CommunalTutorialIndicatorViewBinder
 import com.android.systemui.communal.ui.viewmodel.CommunalTutorialIndicatorViewModel
 import com.android.systemui.coroutines.newTracingContext
-import com.android.systemui.customization.R as customR
 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.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.ClockSizeSetting
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
-import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
 import com.android.systemui.keyguard.ui.view.KeyguardRootView
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewSmartspaceViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
-import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
 import com.android.systemui.monet.ColorScheme
 import com.android.systemui.monet.Style
 import com.android.systemui.plugins.clocks.ClockController
@@ -97,9 +86,6 @@
 import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants
 import com.android.systemui.statusbar.KeyguardIndicationController
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.util.kotlin.DisposableHandles
 import com.android.systemui.util.settings.SecureSettings
 import dagger.assisted.Assisted
@@ -115,6 +101,8 @@
 import kotlinx.coroutines.withContext
 import org.json.JSONException
 import org.json.JSONObject
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.systemui.customization.R as customR
 
 /** Renders the preview of the lock screen. */
 class KeyguardPreviewRenderer
@@ -128,29 +116,20 @@
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val clockViewModel: KeyguardPreviewClockViewModel,
     private val smartspaceViewModel: KeyguardPreviewSmartspaceViewModel,
-    private val bottomAreaViewModel: KeyguardBottomAreaViewModel,
     private val quickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
     displayManager: DisplayManager,
     private val windowManager: WindowManager,
-    private val configuration: ConfigurationState,
     private val clockController: ClockEventController,
     private val clockRegistry: ClockRegistry,
     private val broadcastDispatcher: BroadcastDispatcher,
     private val lockscreenSmartspaceController: LockscreenSmartspaceController,
     private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
     private val indicationController: KeyguardIndicationController,
-    private val keyguardRootViewModel: KeyguardRootViewModel,
-    private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel,
     @Assisted bundle: Bundle,
-    private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
-    private val chipbarCoordinator: ChipbarCoordinator,
-    private val screenOffAnimationController: ScreenOffAnimationController,
     private val shadeInteractor: ShadeInteractor,
     private val secureSettings: SecureSettings,
     private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel,
     private val defaultShortcutsSection: DefaultShortcutsSection,
-    private val keyguardClockInteractor: KeyguardClockInteractor,
-    private val keyguardClockViewModel: KeyguardClockViewModel,
     private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
 ) {
     val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
@@ -201,20 +180,12 @@
         disposables += DisposableHandle { coroutineScope.cancel() }
         clockController.setFallbackWeatherData(WeatherData.getPlaceholderWeatherData())
 
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            quickAffordancesCombinedViewModel.enablePreviewMode(
-                initiallySelectedSlotId =
-                    bundle.getString(KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID)
-                        ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
-            )
-        } else {
-            bottomAreaViewModel.enablePreviewMode(
-                initiallySelectedSlotId =
-                    bundle.getString(KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID),
-                shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
-            )
-        }
+        quickAffordancesCombinedViewModel.enablePreviewMode(
+            initiallySelectedSlotId =
+            bundle.getString(KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID)
+                ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+            shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
+        )
         if (MigrateClocksToBlueprint.isEnabled) {
             clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance
         }
@@ -241,10 +212,6 @@
 
             setupKeyguardRootView(previewContext, rootView)
 
-            if (!KeyguardBottomAreaRefactor.isEnabled) {
-                setUpBottomArea(rootView)
-            }
-
             var displayInfo: DisplayInfo? = null
             display?.let {
                 displayInfo = DisplayInfo()
@@ -292,11 +259,7 @@
     }
 
     fun onSlotSelected(slotId: String) {
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId)
-        } else {
-            bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId)
-        }
+        quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId)
     }
 
     fun onPreviewQuickAffordanceSelected(slotId: String, quickAffordanceId: String) {
@@ -322,9 +285,7 @@
         isDestroyed = true
         lockscreenSmartspaceController.disconnect()
         disposables.dispose()
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            shortcutsBindings.forEach { it.destroy() }
-        }
+        shortcutsBindings.forEach { it.destroy() }
     }
 
     /**
@@ -387,47 +348,8 @@
         smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f
     }
 
-    @Deprecated("Deprecated as part of b/278057014")
-    private fun setUpBottomArea(parentView: ViewGroup) {
-        val bottomAreaView =
-            LayoutInflater.from(context).inflate(R.layout.keyguard_bottom_area, parentView, false)
-                as KeyguardBottomAreaView
-        bottomAreaView.init(viewModel = bottomAreaViewModel)
-        parentView.addView(
-            bottomAreaView,
-            FrameLayout.LayoutParams(
-                FrameLayout.LayoutParams.MATCH_PARENT,
-                FrameLayout.LayoutParams.MATCH_PARENT,
-            ),
-        )
-    }
-
-    @OptIn(ExperimentalCoroutinesApi::class)
     private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) {
         val keyguardRootView = KeyguardRootView(previewContext, null)
-        if (!KeyguardBottomAreaRefactor.isEnabled) {
-            disposables +=
-                KeyguardRootViewBinder.bind(
-                    keyguardRootView,
-                    keyguardRootViewModel,
-                    keyguardBlueprintViewModel,
-                    configuration,
-                    occludingAppDeviceEntryMessageViewModel,
-                    chipbarCoordinator,
-                    screenOffAnimationController,
-                    shadeInteractor,
-                    keyguardClockInteractor,
-                    keyguardClockViewModel,
-                    null, // jank monitor not required for preview mode
-                    null, // device entry haptics not required preview mode
-                    null, // device entry haptics not required for preview mode
-                    null, // falsing manager not required for preview mode
-                    null, // keyguard view mediator is not required for preview mode
-                    null, // primary bouncer interactor is not required for preview mode
-                    mainDispatcher,
-                    null,
-                )
-        }
         rootView.addView(
             keyguardRootView,
             FrameLayout.LayoutParams(
@@ -441,9 +363,7 @@
             if (MigrateClocksToBlueprint.isEnabled) keyguardRootView else rootView,
         )
 
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            setupShortcuts(keyguardRootView)
-        }
+        setupShortcuts(keyguardRootView)
 
         if (!shouldHideClock) {
             setUpClock(previewContext, rootView)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt
new file mode 100644
index 0000000..cafc909
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.transitions
+
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToAodTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDozingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGlanceableHubTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Each PrimaryBouncerTransition is responsible for updating various UI states based on the nature
+ * of the transition.
+ *
+ * MUST list implementing classes in dagger module [PrimaryBouncerTransitionModule].
+ */
+interface PrimaryBouncerTransition {
+    /** Radius of blur applied to the window's root view. */
+    val windowBlurRadius: Flow<Float>
+
+    companion object {
+        const val MAX_BACKGROUND_BLUR_RADIUS = 150f
+        const val MIN_BACKGROUND_BLUR_RADIUS = 0f
+    }
+}
+
+/**
+ * Module that installs all the transitions from different keyguard states to and away from the
+ * primary bouncer.
+ */
+@ExperimentalCoroutinesApi
+@Module
+interface PrimaryBouncerTransitionModule {
+    @Binds
+    @IntoSet
+    fun fromAod(impl: AodToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition
+
+    @Binds
+    @IntoSet
+    fun fromAlternateBouncer(
+        impl: AlternateBouncerToPrimaryBouncerTransitionViewModel
+    ): PrimaryBouncerTransition
+
+    @Binds
+    @IntoSet
+    fun fromDozing(impl: DozingToPrimaryBouncerTransitionViewModel): PrimaryBouncerTransition
+
+    @Binds
+    @IntoSet
+    fun fromLockscreen(
+        impl: LockscreenToPrimaryBouncerTransitionViewModel
+    ): PrimaryBouncerTransition
+
+    @Binds
+    @IntoSet
+    fun toAod(impl: PrimaryBouncerToAodTransitionViewModel): PrimaryBouncerTransition
+
+    @Binds
+    @IntoSet
+    fun toLockscreen(impl: PrimaryBouncerToLockscreenTransitionViewModel): PrimaryBouncerTransition
+
+    @Binds
+    @IntoSet
+    fun toDozing(impl: PrimaryBouncerToDozingTransitionViewModel): PrimaryBouncerTransition
+
+    @Binds
+    @IntoSet
+    fun toGlanceableHub(
+        impl: PrimaryBouncerToGlanceableHubTransitionViewModel
+    ): PrimaryBouncerTransition
+
+    @Binds
+    @IntoSet
+    fun toGone(impl: PrimaryBouncerToGoneTransitionViewModel): PrimaryBouncerTransition
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index 4c23adf..6f7872c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -30,7 +30,6 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.customization.R as customR
-import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.res.R
@@ -62,9 +61,6 @@
     private lateinit var nic: NotificationIconContainer
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!MigrateClocksToBlueprint.isEnabled) {
-            return
-        }
         nic =
             NotificationIconContainer(context, null).apply {
                 id = nicId
@@ -81,10 +77,6 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (!MigrateClocksToBlueprint.isEnabled) {
-            return
-        }
-
         nicBindingDisposable?.dispose()
         nicBindingDisposable =
             NotificationIconContainerViewBinder.bindWhileAttached(
@@ -98,10 +90,6 @@
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!MigrateClocksToBlueprint.isEnabled) {
-            return
-        }
-
         val bottomMargin =
             context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
         val isVisible = rootViewModel.isNotifIconContainerVisible.value
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index aa7eb29..e8fce9c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -22,7 +22,6 @@
 import android.graphics.Rect
 import android.util.DisplayMetrics
 import android.util.Log
-import android.view.View
 import android.view.WindowManager
 import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.widget.ConstraintLayout
@@ -32,8 +31,6 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
-import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
@@ -76,10 +73,6 @@
     private var disposableHandle: DisposableHandle? = null
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!KeyguardBottomAreaRefactor.isEnabled && !MigrateClocksToBlueprint.isEnabled) {
-            return
-        }
-
         val view =
             DeviceEntryIconView(
                     context,
@@ -194,38 +187,6 @@
                 sensorRect.left,
             )
         }
-
-        // This is only intended to be here until the KeyguardBottomAreaRefactor flag is enabled
-        // Without this logic, the lock icon location changes but the KeyguardBottomAreaView is not
-        // updated and visible ui layout jank occurs. This is due to AmbientIndicationContainer
-        // being in NPVC and laying out prior to the KeyguardRootView.
-        // Remove when KeyguardBottomAreaRefactor is enabled.
-        if (!KeyguardBottomAreaRefactor.isEnabled) {
-            with(notificationPanelView) {
-                val isUdfpsSupported = deviceEntryIconViewModel.get().isUdfpsSupported.value
-                val bottomAreaViewRight = findViewById<View>(R.id.keyguard_bottom_area)?.right ?: 0
-                findViewById<View>(R.id.ambient_indication_container)?.let {
-                    val (ambientLeft, ambientTop) = it.locationOnScreen
-                    if (isUdfpsSupported) {
-                        // make top of ambient indication view the bottom of the lock icon
-                        it.layout(
-                            ambientLeft,
-                            sensorRect.bottom,
-                            bottomAreaViewRight - ambientLeft,
-                            ambientTop + it.measuredHeight,
-                        )
-                    } else {
-                        // make bottom of ambient indication view the top of the lock icon
-                        it.layout(
-                            ambientLeft,
-                            sensorRect.top - it.measuredHeight,
-                            bottomAreaViewRight - ambientLeft,
-                            sensorRect.top,
-                        )
-                    }
-                }
-            }
-        }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
index 2d9dac4..5bf56e8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
@@ -21,7 +21,6 @@
 import android.view.ViewGroup
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
 import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
@@ -43,21 +42,17 @@
     private var indicationAreaHandle: DisposableHandle? = null
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            val view = KeyguardIndicationArea(context, null)
-            constraintLayout.addView(view)
-        }
+        val view = KeyguardIndicationArea(context, null)
+        constraintLayout.addView(view)
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            indicationAreaHandle =
-                KeyguardIndicationAreaBinder.bind(
-                    constraintLayout.requireViewById(R.id.keyguard_indication_area),
-                    keyguardIndicationAreaViewModel,
-                    indicationController,
-                )
-        }
+        indicationAreaHandle =
+            KeyguardIndicationAreaBinder.bind(
+                constraintLayout.requireViewById(R.id.keyguard_indication_area),
+                keyguardIndicationAreaViewModel,
+                indicationController,
+            )
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
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 5cd5172..f973ced 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
@@ -31,7 +31,6 @@
 import androidx.core.view.isVisible
 import com.android.systemui.animation.view.LaunchableLinearLayout
 import com.android.systemui.dagger.qualifiers.Main
-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.KeyguardRootViewModel
@@ -56,9 +55,6 @@
     private var settingsPopupMenuHandle: DisposableHandle? = null
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!KeyguardBottomAreaRefactor.isEnabled) {
-            return
-        }
         val view =
             LayoutInflater.from(constraintLayout.context)
                 .inflate(R.layout.keyguard_settings_popup_menu, constraintLayout, false)
@@ -71,17 +67,15 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            settingsPopupMenuHandle =
-                KeyguardSettingsViewBinder.bind(
-                    constraintLayout.requireViewById<View>(R.id.keyguard_settings_button),
-                    keyguardSettingsMenuViewModel,
-                    keyguardTouchHandlingViewModel,
-                    keyguardRootViewModel,
-                    vibratorHelper,
-                    activityStarter,
-                )
-        }
+        settingsPopupMenuHandle =
+            KeyguardSettingsViewBinder.bind(
+                constraintLayout.requireViewById<View>(R.id.keyguard_settings_button),
+                keyguardSettingsMenuViewModel,
+                keyguardTouchHandlingViewModel,
+                keyguardRootViewModel,
+                vibratorHelper,
+                activityStarter,
+            )
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index d3895de..82f142b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -28,7 +28,6 @@
 import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE
 import com.android.systemui.animation.view.LaunchableImageView
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
@@ -49,7 +48,6 @@
     @Named(LOCKSCREEN_INSTANCE)
     private val keyguardQuickAffordancesCombinedViewModel:
         KeyguardQuickAffordancesCombinedViewModel,
-    private val keyguardRootViewModel: KeyguardRootViewModel,
     private val indicationController: KeyguardIndicationController,
     private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
     private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
@@ -60,46 +58,42 @@
     private var safeInsetBottom = 0
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            addLeftShortcut(constraintLayout)
-            addRightShortcut(constraintLayout)
+        addLeftShortcut(constraintLayout)
+        addRightShortcut(constraintLayout)
 
-            constraintLayout
-                .requireViewById<LaunchableImageView>(R.id.start_button)
-                .setOnApplyWindowInsetsListener { _, windowInsets ->
-                    val tempSafeInset = windowInsets?.displayCutout?.safeInsetBottom ?: 0
-                    if (safeInsetBottom != tempSafeInset) {
-                        safeInsetBottom = tempSafeInset
-                        keyguardBlueprintInteractor
-                            .get()
-                            .refreshBlueprint(IntraBlueprintTransition.Type.DefaultTransition)
-                    }
-                    WindowInsets.CONSUMED
+        constraintLayout
+            .requireViewById<LaunchableImageView>(R.id.start_button)
+            .setOnApplyWindowInsetsListener { _, windowInsets ->
+                val tempSafeInset = windowInsets?.displayCutout?.safeInsetBottom ?: 0
+                if (safeInsetBottom != tempSafeInset) {
+                    safeInsetBottom = tempSafeInset
+                    keyguardBlueprintInteractor
+                        .get()
+                        .refreshBlueprint(IntraBlueprintTransition.Type.DefaultTransition)
                 }
-        }
+                WindowInsets.CONSUMED
+            }
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            leftShortcutHandle?.destroy()
-            leftShortcutHandle =
-                keyguardQuickAffordanceViewBinder.bind(
-                    constraintLayout.requireViewById(R.id.start_button),
-                    keyguardQuickAffordancesCombinedViewModel.startButton,
-                    keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
-                ) {
-                    indicationController.showTransientIndication(it)
-                }
-            rightShortcutHandle?.destroy()
-            rightShortcutHandle =
-                keyguardQuickAffordanceViewBinder.bind(
-                    constraintLayout.requireViewById(R.id.end_button),
-                    keyguardQuickAffordancesCombinedViewModel.endButton,
-                    keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
-                ) {
-                    indicationController.showTransientIndication(it)
-                }
-        }
+        leftShortcutHandle?.destroy()
+        leftShortcutHandle =
+            keyguardQuickAffordanceViewBinder.bind(
+                constraintLayout.requireViewById(R.id.start_button),
+                keyguardQuickAffordancesCombinedViewModel.startButton,
+                keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
+            ) {
+                indicationController.showTransientIndication(it)
+            }
+        rightShortcutHandle?.destroy()
+        rightShortcutHandle =
+            keyguardQuickAffordanceViewBinder.bind(
+                constraintLayout.requireViewById(R.id.end_button),
+                keyguardQuickAffordancesCombinedViewModel.endButton,
+                keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
+            ) {
+                indicationController.showTransientIndication(it)
+            }
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
index 0ae1400..8186aa3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
@@ -23,7 +23,6 @@
 import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
 import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
 import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.res.R
 import com.android.systemui.shade.ShadeDisplayAware
@@ -67,16 +66,12 @@
                 ConstraintSet.BOTTOM,
             )
 
-            if (KeyguardBottomAreaRefactor.isEnabled) {
-                connect(
-                    viewId,
-                    ConstraintSet.BOTTOM,
-                    R.id.keyguard_indication_area,
-                    ConstraintSet.TOP,
-                )
-            } else {
-                connect(viewId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
-            }
+            connect(
+                viewId,
+                ConstraintSet.BOTTOM,
+                R.id.keyguard_indication_area,
+                ConstraintSet.TOP,
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
index 85ce5cd..8af5b5f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.util.MathUtils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
@@ -23,12 +24,17 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MAX_BACKGROUND_BLUR_RADIUS
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MIN_BACKGROUND_BLUR_RADIUS
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.transitions.TO_BOUNCER_FADE_FRACTION
+import com.android.systemui.window.flag.WindowBlurFlag
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
 
 /**
  * Breaks down ALTERNATE BOUNCER->PRIMARY BOUNCER transition into discrete steps for corresponding
@@ -38,7 +44,10 @@
 @SysUISingleton
 class AlternateBouncerToPrimaryBouncerTransitionViewModel
 @Inject
-constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+    shadeDependentFlows: ShadeDependentFlows,
+) : DeviceEntryIconTransition, PrimaryBouncerTransition {
     private val transitionAnimation =
         animationFlow
             .setup(
@@ -57,12 +66,30 @@
             else -> { step -> 1f - step }
         }
 
-    val lockscreenAlpha: Flow<Float> =
+    private val alphaFlow =
         transitionAnimation.sharedFlow(
             duration = FromAlternateBouncerTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
             onStep = alphaForAnimationStep,
         )
 
+    val lockscreenAlpha: Flow<Float> = if (WindowBlurFlag.isEnabled) alphaFlow else emptyFlow()
+
+    val notificationAlpha: Flow<Float> = alphaFlow
+
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
+
+    override val windowBlurRadius: Flow<Float> =
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsExpanded =
+                transitionAnimation.immediatelyTransitionTo(MAX_BACKGROUND_BLUR_RADIUS),
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = FromAlternateBouncerTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+                    onStep = { step ->
+                        MathUtils.lerp(MIN_BACKGROUND_BLUR_RADIUS, MAX_BACKGROUND_BLUR_RADIUS, step)
+                    },
+                    onFinish = { MAX_BACKGROUND_BLUR_RADIUS },
+                ),
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
index 35f05f5..e6b796f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
@@ -23,6 +23,8 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MAX_BACKGROUND_BLUR_RADIUS
 import com.android.systemui.scene.shared.model.Scenes
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -36,19 +38,19 @@
 @SysUISingleton
 class AodToPrimaryBouncerTransitionViewModel
 @Inject
-constructor(
-    animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) :
+    DeviceEntryIconTransition, PrimaryBouncerTransition {
     private val transitionAnimation =
         animationFlow
             .setup(
                 duration = FromAodTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
                 edge = Edge.create(from = AOD, to = Scenes.Bouncer),
             )
-            .setupWithoutSceneContainer(
-                edge = Edge.create(from = AOD, to = PRIMARY_BOUNCER),
-            )
+            .setupWithoutSceneContainer(edge = Edge.create(from = AOD, to = PRIMARY_BOUNCER))
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
+
+    override val windowBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(MAX_BACKGROUND_BLUR_RADIUS)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt
index 7ddf641..c1670c3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.util.MathUtils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromDozingTransitionInteractor.Companion.TO_PRIMARY_BOUNCER_DURATION
 import com.android.systemui.keyguard.shared.model.Edge
@@ -23,6 +24,9 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MAX_BACKGROUND_BLUR_RADIUS
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MIN_BACKGROUND_BLUR_RADIUS
 import com.android.systemui.scene.shared.model.Scenes
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -36,9 +40,8 @@
 @SysUISingleton
 class DozingToPrimaryBouncerTransitionViewModel
 @Inject
-constructor(
-    animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) :
+    DeviceEntryIconTransition, PrimaryBouncerTransition {
 
     private val transitionAnimation =
         animationFlow
@@ -46,10 +49,17 @@
                 duration = TO_PRIMARY_BOUNCER_DURATION,
                 edge = Edge.create(from = DOZING, to = Scenes.Bouncer),
             )
-            .setupWithoutSceneContainer(
-                edge = Edge.create(from = DOZING, to = PRIMARY_BOUNCER),
-            )
+            .setupWithoutSceneContainer(edge = Edge.create(from = DOZING, to = PRIMARY_BOUNCER))
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
+
+    override val windowBlurRadius: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            TO_PRIMARY_BOUNCER_DURATION,
+            onStep = { step ->
+                MathUtils.lerp(MIN_BACKGROUND_BLUR_RADIUS, MAX_BACKGROUND_BLUR_RADIUS, step)
+            },
+            onFinish = { MAX_BACKGROUND_BLUR_RADIUS },
+        )
 }
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
deleted file mode 100644
index 6fe51ae..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
-import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
-import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
-import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-
-/** View-model for the keyguard bottom area view */
-@OptIn(ExperimentalCoroutinesApi::class)
-class KeyguardBottomAreaViewModel
-@Inject
-constructor(
-    private val keyguardInteractor: KeyguardInteractor,
-    private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
-    private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
-    private val burnInHelperWrapper: BurnInHelperWrapper,
-    private val keyguardTouchHandlingViewModel: KeyguardTouchHandlingViewModel,
-    val settingsMenuViewModel: KeyguardSettingsMenuViewModel,
-) {
-    data class PreviewMode(
-        val isInPreviewMode: Boolean = false,
-        val shouldHighlightSelectedAffordance: Boolean = false,
-    )
-
-    /**
-     * Whether this view-model instance is powering the preview experience that renders exclusively
-     * in the wallpaper picker application. This should _always_ be `false` for the real lock screen
-     * experience.
-     */
-    val previewMode = MutableStateFlow(PreviewMode())
-
-    /**
-     * ID of the slot that's currently selected in the preview that renders exclusively in the
-     * wallpaper picker application. This is ignored for the actual, real lock screen experience.
-     */
-    private val selectedPreviewSlotId =
-        MutableStateFlow(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START)
-
-    /**
-     * Whether quick affordances are "opaque enough" to be considered visible to and interactive by
-     * the user. If they are not interactive, user input should not be allowed on them.
-     *
-     * Note that there is a margin of error, where we allow very, very slightly transparent views to
-     * be considered "fully opaque" for the purpose of being interactive. This is to accommodate the
-     * error margin of floating point arithmetic.
-     *
-     * A view that is visible but with an alpha of less than our threshold either means it's not
-     * fully done fading in or is fading/faded out. Either way, it should not be
-     * interactive/clickable unless "fully opaque" to avoid issues like in b/241830987.
-     */
-    private val areQuickAffordancesFullyOpaque: Flow<Boolean> =
-        bottomAreaInteractor.alpha
-            .map { alpha -> alpha >= AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD }
-            .distinctUntilChanged()
-
-    /** An observable for the view-model of the "start button" quick affordance. */
-    val startButton: Flow<KeyguardQuickAffordanceViewModel> =
-        button(KeyguardQuickAffordancePosition.BOTTOM_START)
-    /** An observable for the view-model of the "end button" quick affordance. */
-    val endButton: Flow<KeyguardQuickAffordanceViewModel> =
-        button(KeyguardQuickAffordancePosition.BOTTOM_END)
-    /** An observable for whether the overlay container should be visible. */
-    val isOverlayContainerVisible: Flow<Boolean> =
-        keyguardInteractor.isDozing.map { !it }.distinctUntilChanged()
-    /** An observable for the alpha level for the entire bottom area. */
-    val alpha: Flow<Float> =
-        previewMode.flatMapLatest {
-            if (it.isInPreviewMode) {
-                flowOf(1f)
-            } else {
-                bottomAreaInteractor.alpha.distinctUntilChanged()
-            }
-        }
-    /** An observable for the x-offset by which the indication area should be translated. */
-    val indicationAreaTranslationX: Flow<Float> =
-        bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
-
-    /** Returns an observable for the y-offset by which the indication area should be translated. */
-    fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {
-        return keyguardInteractor.dozeAmount
-            .map { dozeAmount ->
-                dozeAmount *
-                    (burnInHelperWrapper.burnInOffset(
-                        /* amplitude = */ defaultBurnInOffset * 2,
-                        /* xAxis= */ false,
-                    ) - defaultBurnInOffset)
-            }
-            .distinctUntilChanged()
-    }
-
-    /**
-     * Returns whether the keyguard bottom area should be constrained to the top of the lock icon
-     */
-    fun shouldConstrainToTopOfLockIcon(): Boolean =
-        bottomAreaInteractor.shouldConstrainToTopOfLockIcon()
-
-    /**
-     * Puts this view-model in "preview mode", which means it's being used for UI that is rendering
-     * the lock screen preview in wallpaper picker / settings and not the real experience on the
-     * lock screen.
-     *
-     * @param initiallySelectedSlotId The ID of the initial slot to render as the selected one.
-     * @param shouldHighlightSelectedAffordance Whether the selected quick affordance should be
-     *   highlighted (while all others are dimmed to make the selected one stand out).
-     */
-    fun enablePreviewMode(
-        initiallySelectedSlotId: String?,
-        shouldHighlightSelectedAffordance: Boolean,
-    ) {
-        previewMode.value =
-            PreviewMode(
-                isInPreviewMode = true,
-                shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
-            )
-        onPreviewSlotSelected(
-            initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
-        )
-    }
-
-    /**
-     * Notifies that a slot with the given ID has been selected in the preview experience that is
-     * rendering in the wallpaper picker. This is ignored for the real lock screen experience.
-     *
-     * @see enablePreviewMode
-     */
-    fun onPreviewSlotSelected(slotId: String) {
-        selectedPreviewSlotId.value = slotId
-    }
-
-    /**
-     * Notifies that some input gesture has started somewhere in the bottom area that's outside of
-     * the lock screen settings menu item pop-up.
-     */
-    fun onTouchedOutsideLockScreenSettingsMenu() {
-        keyguardTouchHandlingViewModel.onTouchedOutside()
-    }
-
-    private fun button(
-        position: KeyguardQuickAffordancePosition
-    ): Flow<KeyguardQuickAffordanceViewModel> {
-        return previewMode.flatMapLatest { previewMode ->
-            combine(
-                    if (previewMode.isInPreviewMode) {
-                        quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
-                    } else {
-                        quickAffordanceInteractor.quickAffordance(position = position)
-                    },
-                    bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(),
-                    areQuickAffordancesFullyOpaque,
-                    selectedPreviewSlotId,
-                    quickAffordanceInteractor.useLongPress(),
-                ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress ->
-                    val slotId = position.toSlotId()
-                    val isSelected = selectedPreviewSlotId == slotId
-                    model.toViewModel(
-                        animateReveal = !previewMode.isInPreviewMode && animateReveal,
-                        isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
-                        isSelected =
-                            previewMode.isInPreviewMode &&
-                                previewMode.shouldHighlightSelectedAffordance &&
-                                isSelected,
-                        isDimmed =
-                            previewMode.isInPreviewMode &&
-                                previewMode.shouldHighlightSelectedAffordance &&
-                                !isSelected,
-                        forceInactive = previewMode.isInPreviewMode,
-                        slotId = slotId,
-                        useLongPress = useLongPress,
-                    )
-                }
-                .distinctUntilChanged()
-        }
-    }
-
-    private fun KeyguardQuickAffordanceModel.toViewModel(
-        animateReveal: Boolean,
-        isClickable: Boolean,
-        isSelected: Boolean,
-        isDimmed: Boolean,
-        forceInactive: Boolean,
-        slotId: String,
-        useLongPress: Boolean,
-    ): KeyguardQuickAffordanceViewModel {
-        return when (this) {
-            is KeyguardQuickAffordanceModel.Visible ->
-                KeyguardQuickAffordanceViewModel(
-                    configKey = configKey,
-                    isVisible = true,
-                    animateReveal = animateReveal,
-                    icon = icon,
-                    onClicked = { parameters ->
-                        quickAffordanceInteractor.onQuickAffordanceTriggered(
-                            configKey = parameters.configKey,
-                            expandable = parameters.expandable,
-                            slotId = parameters.slotId,
-                        )
-                    },
-                    isClickable = isClickable,
-                    isActivated = !forceInactive && activationState is ActivationState.Active,
-                    isSelected = isSelected,
-                    useLongPress = useLongPress,
-                    isDimmed = isDimmed,
-                    slotId = slotId,
-                )
-            is KeyguardQuickAffordanceModel.Hidden ->
-                KeyguardQuickAffordanceViewModel(
-                    slotId = slotId,
-                )
-        }
-    }
-
-    companion object {
-        // We select a value that's less than 1.0 because we want floating point math precision to
-        // not be a factor in determining whether the affordance UI is fully opaque. The number we
-        // choose needs to be close enough 1.0 such that the user can't easily tell the difference
-        // between the UI with an alpha at the threshold and when the alpha is 1.0. At the same
-        // time, we don't want the number to be too close to 1.0 such that there is a chance that we
-        // never treat the affordance UI as "fully opaque" as that would risk making it forever not
-        // clickable.
-        @VisibleForTesting const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index bc3ef02..4663a2b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -21,10 +21,8 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.BurnInModel
@@ -48,8 +46,6 @@
 @Inject
 constructor(
     private val keyguardInteractor: KeyguardInteractor,
-    bottomAreaInteractor: KeyguardBottomAreaInteractor,
-    keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
     private val burnInHelperWrapper: BurnInHelperWrapper,
     burnInInteractor: BurnInInteractor,
     @Named(KeyguardQuickAffordancesCombinedViewModelModule.Companion.LOCKSCREEN_INSTANCE)
@@ -64,9 +60,6 @@
     /** Notifies when a new configuration is set */
     val configurationChange: Flow<Unit> = configurationInteractor.onAnyConfigurationChange
 
-    /** An observable for the alpha level for the entire bottom area. */
-    val alpha: Flow<Float> = keyguardBottomAreaViewModel.alpha
-
     /** An observable for the visibility value for the indication area view. */
     val visible: Flow<Boolean> =
         anyOf(
@@ -76,22 +69,12 @@
 
     /** An observable for whether the indication area should be padded. */
     val isIndicationAreaPadded: Flow<Boolean> =
-        if (KeyguardBottomAreaRefactor.isEnabled) {
-            combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) {
-                    startButtonModel,
-                    endButtonModel ->
-                    startButtonModel.isVisible || endButtonModel.isVisible
-                }
-                .distinctUntilChanged()
-        } else {
-            combine(
-                    keyguardBottomAreaViewModel.startButton,
-                    keyguardBottomAreaViewModel.endButton,
-                ) { startButtonModel, endButtonModel ->
-                    startButtonModel.isVisible || endButtonModel.isVisible
-                }
-                .distinctUntilChanged()
+        combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) {
+                startButtonModel,
+                endButtonModel ->
+            startButtonModel.isVisible || endButtonModel.isVisible
         }
+            .distinctUntilChanged()
 
     @OptIn(ExperimentalCoroutinesApi::class)
     private val burnIn: Flow<BurnInModel> =
@@ -114,11 +97,7 @@
 
     /** An observable for the x-offset by which the indication area should be translated. */
     val indicationAreaTranslationX: Flow<Float> =
-        if (MigrateClocksToBlueprint.isEnabled || KeyguardBottomAreaRefactor.isEnabled) {
-            burnIn.map { it.translationX.toFloat() }.flowOn(mainDispatcher)
-        } else {
-            bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
-        }
+        burnIn.map { it.translationX.toFloat() }.flowOn(mainDispatcher)
 
     /** Returns an observable for the y-offset by which the indication area should be translated. */
     fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 0d81604..9066d46 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -92,6 +92,8 @@
         AlternateBouncerToLockscreenTransitionViewModel,
     private val alternateBouncerToOccludedTransitionViewModel:
         AlternateBouncerToOccludedTransitionViewModel,
+    private val alternateBouncerToPrimaryBouncerTransitionViewModel:
+        AlternateBouncerToPrimaryBouncerTransitionViewModel,
     private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
     private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
@@ -238,6 +240,7 @@
                         alternateBouncerToAodTransitionViewModel.lockscreenAlpha(viewState),
                         alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
+                        alternateBouncerToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
                         alternateBouncerToOccludedTransitionViewModel.lockscreenAlpha,
                         aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index 914730e..48cc8ad 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.util.MathUtils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
@@ -23,6 +24,9 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MAX_BACKGROUND_BLUR_RADIUS
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MIN_BACKGROUND_BLUR_RADIUS
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.transitions.TO_BOUNCER_FADE_FRACTION
@@ -42,7 +46,7 @@
 constructor(
     shadeDependentFlows: ShadeDependentFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+) : DeviceEntryIconTransition, PrimaryBouncerTransition {
     private val transitionAnimation =
         animationFlow
             .setup(
@@ -78,4 +82,16 @@
                 ),
             flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
         )
+    override val windowBlurRadius: Flow<Float> =
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsExpanded =
+                transitionAnimation.immediatelyTransitionTo(MAX_BACKGROUND_BLUR_RADIUS),
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+                    onStep = {
+                        MathUtils.lerp(MIN_BACKGROUND_BLUR_RADIUS, MAX_BACKGROUND_BLUR_RADIUS, it)
+                    },
+                ),
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
index 501feca..f14144e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.util.MathUtils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
@@ -24,6 +25,9 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MAX_BACKGROUND_BLUR_RADIUS
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MIN_BACKGROUND_BLUR_RADIUS
 import com.android.systemui.scene.shared.model.Scenes
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -43,16 +47,14 @@
 constructor(
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+) : DeviceEntryIconTransition, PrimaryBouncerTransition {
     private val transitionAnimation =
         animationFlow
             .setup(
                 duration = FromPrimaryBouncerTransitionInteractor.TO_AOD_DURATION,
                 edge = Edge.create(from = Scenes.Bouncer, to = AOD),
             )
-            .setupWithoutSceneContainer(
-                edge = Edge.create(from = PRIMARY_BOUNCER, to = AOD),
-            )
+            .setupWithoutSceneContainer(edge = Edge.create(from = PRIMARY_BOUNCER, to = AOD))
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
@@ -60,7 +62,7 @@
     val lockscreenAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
             duration = FromPrimaryBouncerTransitionInteractor.TO_AOD_DURATION,
-            onStep = { it }
+            onStep = { it },
         )
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
@@ -77,4 +79,13 @@
                 emptyFlow()
             }
         }
+
+    override val windowBlurRadius: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            duration = FromPrimaryBouncerTransitionInteractor.TO_AOD_DURATION,
+            onStep = { step ->
+                MathUtils.lerp(MAX_BACKGROUND_BLUR_RADIUS, MIN_BACKGROUND_BLUR_RADIUS, step)
+            },
+            onFinish = { MIN_BACKGROUND_BLUR_RADIUS },
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt
index e5bb464..a24ed26 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.scene.shared.model.Scenes
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -42,7 +43,7 @@
 constructor(
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+) : DeviceEntryIconTransition, PrimaryBouncerTransition {
 
     private val transitionAnimation =
         animationFlow
@@ -50,9 +51,7 @@
                 duration = TO_DOZING_DURATION,
                 edge = Edge.create(from = Scenes.Bouncer, to = DOZING),
             )
-            .setupWithoutSceneContainer(
-                edge = Edge.create(from = PRIMARY_BOUNCER, to = DOZING),
-            )
+            .setupWithoutSceneContainer(edge = Edge.create(from = PRIMARY_BOUNCER, to = DOZING))
 
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
@@ -66,4 +65,9 @@
                 emptyFlow()
             }
         }
+
+    override val windowBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(
+            PrimaryBouncerTransition.MIN_BACKGROUND_BLUR_RADIUS
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
index 9ec15dc..b52a390 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
@@ -23,13 +23,16 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MIN_BACKGROUND_BLUR_RADIUS
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 
 @SysUISingleton
 class PrimaryBouncerToGlanceableHubTransitionViewModel
 @Inject
-constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) :
+    DeviceEntryIconTransition, PrimaryBouncerTransition {
     private val transitionAnimation =
         animationFlow
             .setup(duration = TO_GLANCEABLE_HUB_DURATION, edge = Edge.INVALID)
@@ -37,4 +40,7 @@
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(1f)
+
+    override val windowBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(MIN_BACKGROUND_BLUR_RADIUS)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
index 17c678e..713ac15 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -16,16 +16,21 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.util.MathUtils
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION
+import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_SHORT_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MAX_BACKGROUND_BLUR_RADIUS
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MIN_BACKGROUND_BLUR_RADIUS
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import dagger.Lazy
 import javax.inject.Inject
@@ -48,16 +53,11 @@
     keyguardDismissActionInteractor: Lazy<KeyguardDismissActionInteractor>,
     bouncerToGoneFlows: BouncerToGoneFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
-) {
+) : PrimaryBouncerTransition {
     private val transitionAnimation =
         animationFlow
-            .setup(
-                duration = TO_GONE_DURATION,
-                edge = Edge.INVALID,
-            )
-            .setupWithoutSceneContainer(
-                edge = Edge.create(from = PRIMARY_BOUNCER, to = GONE),
-            )
+            .setup(duration = TO_GONE_DURATION, edge = Edge.INVALID)
+            .setupWithoutSceneContainer(edge = Edge.create(from = PRIMARY_BOUNCER, to = GONE))
 
     private var leaveShadeOpen: Boolean = false
     private var willRunDismissFromKeyguard: Boolean = false
@@ -96,7 +96,7 @@
 
     private fun createBouncerAlphaFlow(willRunAnimationOnKeyguard: () -> Boolean): Flow<Float> {
         return transitionAnimation.sharedFlow(
-            duration = 200.milliseconds,
+            duration = TO_GONE_SHORT_DURATION,
             onStart = { willRunDismissFromKeyguard = willRunAnimationOnKeyguard() },
             onStep = {
                 if (willRunDismissFromKeyguard) {
@@ -108,6 +108,22 @@
         )
     }
 
+    private fun createBouncerWindowBlurFlow(
+        willRunAnimationOnKeyguard: () -> Boolean
+    ): Flow<Float> {
+        return transitionAnimation.sharedFlow(
+            duration = TO_GONE_SHORT_DURATION,
+            onStart = { willRunDismissFromKeyguard = willRunAnimationOnKeyguard() },
+            onStep = {
+                if (willRunDismissFromKeyguard) {
+                    MIN_BACKGROUND_BLUR_RADIUS
+                } else {
+                    MathUtils.lerp(MAX_BACKGROUND_BLUR_RADIUS, MIN_BACKGROUND_BLUR_RADIUS, it)
+                }
+            },
+        )
+    }
+
     /** Lockscreen alpha */
     val lockscreenAlpha: Flow<Float> =
         if (ComposeBouncerFlags.isEnabled) {
@@ -137,6 +153,16 @@
         )
     }
 
+    override val windowBlurRadius: Flow<Float> =
+        if (ComposeBouncerFlags.isEnabled) {
+            keyguardDismissActionInteractor
+                .get()
+                .willAnimateDismissActionOnLockscreen
+                .flatMapLatest { createBouncerWindowBlurFlow { it } }
+        } else {
+            createBouncerWindowBlurFlow(primaryBouncerInteractor::willRunDismissFromKeyguard)
+        }
+
     val scrimAlpha: Flow<ScrimAlpha> =
         bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, PRIMARY_BOUNCER)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
index d29f512..e737fce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
@@ -25,6 +25,9 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MAX_BACKGROUND_BLUR_RADIUS
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition.Companion.MIN_BACKGROUND_BLUR_RADIUS
 import com.android.systemui.scene.shared.model.Scenes
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -41,22 +44,21 @@
 @Inject
 constructor(
     animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+    shadeDependentFlows: ShadeDependentFlows,
+) : DeviceEntryIconTransition, PrimaryBouncerTransition {
     private val transitionAnimation =
         animationFlow
             .setup(
                 duration = FromPrimaryBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION,
                 edge = Edge.create(from = Scenes.Bouncer, to = LOCKSCREEN),
             )
-            .setupWithoutSceneContainer(
-                edge = Edge.create(from = PRIMARY_BOUNCER, to = LOCKSCREEN),
-            )
+            .setupWithoutSceneContainer(edge = Edge.create(from = PRIMARY_BOUNCER, to = LOCKSCREEN))
 
     val shortcutsAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
             duration = 250.milliseconds,
             interpolator = EMPHASIZED_ACCELERATE,
-            onStep = { it }
+            onStep = { it },
         )
 
     fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
@@ -72,4 +74,17 @@
         transitionAnimation.immediatelyTransitionTo(1f)
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(1f)
+
+    override val windowBlurRadius: Flow<Float> =
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsExpanded =
+                transitionAnimation.immediatelyTransitionTo(MAX_BACKGROUND_BLUR_RADIUS),
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = FromPrimaryBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION,
+                    onStep = {
+                        MathUtils.lerp(MAX_BACKGROUND_BLUR_RADIUS, MIN_BACKGROUND_BLUR_RADIUS, it)
+                    },
+                ),
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 70ca824..dccf61d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -1369,8 +1369,9 @@
         boolean visible = mediaAction != null && !shouldBeHiddenDueToScrubbing;
 
         int notVisibleValue;
-        if ((buttonId == R.id.actionPrev && semanticActions.getReservePrev())
-                || (buttonId == R.id.actionNext && semanticActions.getReserveNext())) {
+        if (!shouldBeHiddenDueToScrubbing
+                && ((buttonId == R.id.actionPrev && semanticActions.getReservePrev())
+                    || (buttonId == R.id.actionNext && semanticActions.getReserveNext()))) {
             notVisibleValue = ConstraintSet.INVISIBLE;
             mMediaViewHolder.getAction(buttonId).setFocusable(visible);
             mMediaViewHolder.getAction(buttonId).setClickable(visible);
@@ -1408,7 +1409,9 @@
         // The scrubbing time views replace the SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING action views,
         // so we should only allow scrubbing times to be shown if those action views are present.
         return semanticActions != null && SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.stream().allMatch(
-                id -> semanticActions.getActionById(id) != null
+                id -> (semanticActions.getActionById(id) != null
+                        || ((id == R.id.actionPrev && semanticActions.getReservePrev())
+                            || (id == R.id.actionNext && semanticActions.getReserveNext())))
         );
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
index 4e97f20..61e4d95 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
@@ -316,8 +316,11 @@
             isVisibleWhenScrubbing = !shouldHideWhenScrubbing,
             notVisibleValue =
                 if (
-                    (buttonId == R.id.actionPrev && model.semanticActionButtons!!.reservePrev) ||
-                        (buttonId == R.id.actionNext && model.semanticActionButtons!!.reserveNext)
+                    !shouldHideWhenScrubbing &&
+                        ((buttonId == R.id.actionPrev &&
+                            model.semanticActionButtons!!.reservePrev) ||
+                            (buttonId == R.id.actionNext &&
+                                model.semanticActionButtons!!.reserveNext))
                 ) {
                     ConstraintSet.INVISIBLE
                 } else {
@@ -382,7 +385,9 @@
         // so we should only allow scrubbing times to be shown if those action views are present.
         return semanticActions?.let {
             SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.stream().allMatch { id: Int ->
-                semanticActions.getActionById(id) != null
+                semanticActions.getActionById(id) != null ||
+                    (id == R.id.actionPrev && semanticActions.reservePrev ||
+                        id == R.id.actionNext && semanticActions.reserveNext)
             }
         } ?: false
     }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index b1719107..037a1b2 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -26,6 +26,7 @@
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEdgeResizePermitted;
 
 import static java.util.stream.Collectors.joining;
 
@@ -965,11 +966,14 @@
         return mDesktopModeExcludeRegion.contains(x, y);
     }
 
-    private boolean isWithinTouchRegion(int x, int y) {
+    private boolean isWithinTouchRegion(MotionEvent ev) {
         // If the point is inside the PiP or Nav bar overlay excluded bounds, then ignore the back
         // gesture
+        int x = (int) ev.getX();
+        int y = (int) ev.getY();
         final boolean isInsidePip = mIsInPip && mPipExcludedBounds.contains(x, y);
-        final boolean isInDesktopExcludeRegion = desktopExcludeRegionContains(x, y);
+        final boolean isInDesktopExcludeRegion = desktopExcludeRegionContains(x, y)
+                && isEdgeResizePermitted(ev);
         if (isInsidePip || isInDesktopExcludeRegion
                 || mNavBarOverlayExcludedBounds.contains(x, y)) {
             return false;
@@ -1098,8 +1102,7 @@
                         && isValidTrackpadBackGesture(true /* isTrackpadEvent */);
             } else {
                 mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets
-                        && isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
-                        && !isButtonPressFromTrackpad(ev);
+                        && isWithinTouchRegion(ev) && !isButtonPressFromTrackpad(ev);
             }
             if (mAllowGesture) {
                 mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java
index dc188c2..e8ee4dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterViewController.java
@@ -26,6 +26,7 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.systemui.FontStyles;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.dagger.QSScope;
@@ -68,7 +69,7 @@
 
         mBuildText = mView.findViewById(R.id.build);
         if (gsfQuickSettings()) {
-            mBuildText.setTypeface(Typeface.create("gsf-body-medium", Typeface.NORMAL));
+            mBuildText.setTypeface(Typeface.create(FontStyles.GSF_BODY_MEDIUM, Typeface.NORMAL));
         }
         mPageIndicator = mView.findViewById(R.id.footer_page_indicator);
         mEditButton = mView.findViewById(android.R.id.edit);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
index c912bd5..790793e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
@@ -17,14 +17,16 @@
 package com.android.systemui.qs.composefragment.ui
 
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.ClipOp
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.asAndroidPath
-import androidx.compose.ui.graphics.drawscope.ContentDrawScope
-import androidx.compose.ui.graphics.drawscope.clipPath
-import androidx.compose.ui.node.DrawModifierNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.clipRect
+import androidx.compose.ui.graphics.layer.CompositingStrategy
+import androidx.compose.ui.graphics.layer.drawLayer
 
 /**
  * Clipping modifier for clipping out the notification scrim as it slides over QS. It will clip out
@@ -32,65 +34,30 @@
  * from the QS container.
  */
 fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier {
-    return this then NotificationScrimClipElement(clipParams)
-}
-
-private class NotificationScrimClipNode(var clipParams: () -> NotificationScrimClipParams) :
-    DrawModifierNode, Modifier.Node() {
-    private val path = Path()
-
-    private var lastClipParams = NotificationScrimClipParams()
-
-    override fun ContentDrawScope.draw() {
-        val newClipParams = clipParams()
-        if (newClipParams != lastClipParams) {
-            lastClipParams = newClipParams
-            applyClipParams(path, lastClipParams)
+    return this.drawWithCache {
+            val params = clipParams()
+            val left = -params.leftInset.toFloat()
+            val right = size.width + params.rightInset.toFloat()
+            val top = params.top.toFloat()
+            val bottom = params.bottom.toFloat()
+            val graphicsLayer = obtainGraphicsLayer()
+            graphicsLayer.compositingStrategy = CompositingStrategy.Offscreen
+            graphicsLayer.record {
+                drawContent()
+                clipRect {
+                    drawRoundRect(
+                        color = Color.Black,
+                        cornerRadius = CornerRadius(params.radius.toFloat()),
+                        blendMode = BlendMode.Clear,
+                        topLeft = Offset(left, top),
+                        size = Size(right - left, bottom - top),
+                    )
+                }
+            }
+            onDrawWithContent {
+                drawLayer(graphicsLayer)
+            }
         }
-        clipPath(path, ClipOp.Difference) { this@draw.drawContent() }
-    }
-
-    private fun ContentDrawScope.applyClipParams(
-        path: Path,
-        clipParams: NotificationScrimClipParams,
-    ) {
-        with(clipParams) {
-            path.rewind()
-            path
-                .asAndroidPath()
-                .addRoundRect(
-                    -leftInset.toFloat(),
-                    top.toFloat(),
-                    size.width + rightInset,
-                    bottom.toFloat(),
-                    radius.toFloat(),
-                    radius.toFloat(),
-                    android.graphics.Path.Direction.CW,
-                )
-        }
-    }
-}
-
-private data class NotificationScrimClipElement(val clipParams: () -> NotificationScrimClipParams) :
-    ModifierNodeElement<NotificationScrimClipNode>() {
-    override fun create(): NotificationScrimClipNode {
-        return NotificationScrimClipNode(clipParams)
-    }
-
-    override fun update(node: NotificationScrimClipNode) {
-        node.clipParams = clipParams
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "notificationScrimClip"
-        with(clipParams()) {
-            properties["leftInset"] = leftInset
-            properties["top"] = top
-            properties["rightInset"] = rightInset
-            properties["bottom"] = bottom
-            properties["radius"] = radius
-        }
-    }
 }
 
 /** Params for [notificationScrimClip]. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
index db778a2..873059e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
@@ -47,6 +47,7 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.FontSizeUtils;
+import com.android.systemui.FontStyles;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.qs.QSEditEvent;
@@ -314,7 +315,7 @@
             v.setMinimumHeight(calculateHeaderMinHeight(context));
             if (gsfQuickSettings()) {
                 ((TextView) v.findViewById(android.R.id.title)).setTypeface(
-                        Typeface.create("gsf-label-large", Typeface.NORMAL));
+                        Typeface.create(FontStyles.GSF_LABEL_LARGE, Typeface.NORMAL));
             }
             return new Holder(v);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
index 7161565..7b9f42c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
@@ -40,6 +40,7 @@
 import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
 import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
 import com.android.systemui.security.data.repository.SecurityRepository
+import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.user.data.repository.UserSwitcherRepository
 import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
@@ -108,6 +109,7 @@
     userSwitcherRepository: UserSwitcherRepository,
     broadcastDispatcher: BroadcastDispatcher,
     @Background bgDispatcher: CoroutineDispatcher,
+    @ShadeDisplayAware private val context: Context,
 ) : FooterActionsInteractor {
     override val securityButtonConfig: Flow<SecurityButtonConfig?> =
         securityRepository.security.map { security ->
@@ -157,6 +159,7 @@
             /* keyguardShowing= */ false,
             /* isDeviceProvisioned= */ true,
             expandable,
+            context.displayId,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index b7ebce2..d401b6e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -60,6 +60,7 @@
 import com.android.settingslib.Utils
 import com.android.systemui.Flags
 import com.android.systemui.FontSizeUtils
+import com.android.systemui.FontStyles
 import com.android.systemui.animation.Expandable
 import com.android.systemui.animation.LaunchableView
 import com.android.systemui.animation.LaunchableViewDelegate
@@ -312,9 +313,11 @@
 
         if (Flags.gsfQuickSettings()) {
             label.apply {
-                typeface = Typeface.create("gsf-title-small-emphasized", Typeface.NORMAL)
+                typeface = Typeface.create(FontStyles.GSF_TITLE_SMALL_EMPHASIZED, Typeface.NORMAL)
             }
-            secondaryLabel.apply { typeface = Typeface.create("gsf-label-medium", Typeface.NORMAL) }
+            secondaryLabel.apply {
+                typeface = Typeface.create(FontStyles.GSF_LABEL_MEDIUM, Typeface.NORMAL)
+            }
         }
 
         addView(labelContainer)
@@ -776,11 +779,15 @@
         lastIconTint = icon.getColor(state)
 
         // Long-press effects
-        longPressEffect?.qsTile?.state?.handlesLongClick = state.handlesLongClick
-        if (
-            state.handlesLongClick &&
-                longPressEffect?.initializeEffect(longPressEffectDuration) == true
-        ) {
+        updateLongPressEffect(state.handlesLongClick)
+    }
+
+    private fun updateLongPressEffect(handlesLongClick: Boolean) {
+        // The long press effect in the tile can't be updated if it is still running
+        if (longPressEffect?.state != QSLongPressEffect.State.IDLE) return
+
+        longPressEffect.qsTile?.state?.handlesLongClick = handlesLongClick
+        if (handlesLongClick && longPressEffect.initializeEffect(longPressEffectDuration)) {
             showRippleEffect = false
             longPressEffect.qsTile?.state?.state = lastState // Store the tile's state
             longPressEffect.resetState()
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
index 2d6181a..1355ba8 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
@@ -47,7 +47,11 @@
     }
 
     override fun createDialog(): SystemUIDialog {
-        return systemUIDialogFactory.create(this, rearDisplayContext)
+        return systemUIDialogFactory.create(
+            this,
+            rearDisplayContext,
+            false, /* shouldAcsdDismissDialog */
+        )
     }
 
     override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
index 6097ef5..33bffc2 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.Flags.sceneContainer
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
@@ -37,7 +36,6 @@
     inline val isEnabled
         get() =
             sceneContainer() && // mainAconfigFlag
-                KeyguardBottomAreaRefactor.isEnabled &&
                 KeyguardWmStateRefactor.isEnabled &&
                 MigrateClocksToBlueprint.isEnabled &&
                 NotificationThrottleHun.isEnabled &&
@@ -51,7 +49,6 @@
     /** The set of secondary flags which must be enabled for scene container to work properly */
     inline fun getSecondaryFlags(): Sequence<FlagToken> =
         sequenceOf(
-            KeyguardBottomAreaRefactor.token,
             KeyguardWmStateRefactor.token,
             MigrateClocksToBlueprint.token,
             NotificationThrottleHun.token,
diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java
index 3c03d28..a7b51faa 100644
--- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java
@@ -35,7 +35,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
-import static com.android.systemui.Flags.notificationShadeBlur;
+import com.android.systemui.window.flag.WindowBlurFlag;
 
 /**
  * Drawable used on SysUI scrims.
@@ -214,8 +214,9 @@
     public void draw(@NonNull Canvas canvas) {
         mPaint.setColor(mMainColor);
         mPaint.setAlpha(mAlpha);
-        if (notificationShadeBlur()) {
+        if (WindowBlurFlag.isEnabled()) {
             // TODO(b/370555223): Match the alpha to the visual spec when it is finalized.
+            // TODO (b/381263600), wire this at ScrimController, move it to PrimaryBouncerTransition
             mPaint.setAlpha((int) (0.5f * mAlpha));
         }
         if (mConcaveInfo != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
index 90d27f4..c65c3b8 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
@@ -21,7 +21,6 @@
 import static com.android.settingslib.display.BrightnessUtils.convertLinearToGammaFloat;
 
 import android.animation.ValueAnimator;
-import android.annotation.NonNull;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.hardware.display.BrightnessInfo;
@@ -42,6 +41,7 @@
 import android.util.Log;
 import android.util.MathUtils;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -72,19 +72,19 @@
 
 public class BrightnessController implements ToggleSlider.Listener, MirroredBrightnessController {
     private static final String TAG = "CentralSurfaces.BrightnessController";
-    private static final int SLIDER_ANIMATION_DURATION = 3000;
+    protected static final int SLIDER_ANIMATION_DURATION = 3000;
 
-    private static final int MSG_UPDATE_SLIDER = 1;
-    private static final int MSG_ATTACH_LISTENER = 2;
-    private static final int MSG_DETACH_LISTENER = 3;
-    private static final int MSG_VR_MODE_CHANGED = 4;
+    protected static final int MSG_UPDATE_SLIDER = 1;
+    protected static final int MSG_ATTACH_LISTENER = 2;
+    protected static final int MSG_DETACH_LISTENER = 3;
+    protected static final int MSG_VR_MODE_CHANGED = 4;
 
-    private static final Uri BRIGHTNESS_MODE_URI =
+    protected static final Uri BRIGHTNESS_MODE_URI =
             Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE);
 
     private final int mDisplayId;
     private final Context mContext;
-    private final ToggleSlider mControl;
+    protected final ToggleSlider mControl;
     private final DisplayManager mDisplayManager;
     private final UserTracker mUserTracker;
     private final DisplayTracker mDisplayTracker;
@@ -109,10 +109,10 @@
     private boolean mTrackingTouch = false; // Brightness adjusted via touch events.
     private volatile boolean mIsVrModeEnabled;
     private boolean mListening;
-    private boolean mExternalChange;
+    protected boolean mExternalChange;
     private boolean mControlValueInitialized;
-    private float mBrightnessMin = PowerManager.BRIGHTNESS_MIN;
-    private float mBrightnessMax = PowerManager.BRIGHTNESS_MAX;
+    protected float mBrightnessMin = PowerManager.BRIGHTNESS_MIN;
+    protected float mBrightnessMax = PowerManager.BRIGHTNESS_MAX;
     private boolean mIsBrightnessOverriddenByWindow = false;
 
     private ValueAnimator mSliderAnimator;
@@ -253,10 +253,8 @@
             if (info == null) {
                 return;
             }
-            mBrightnessMax = info.brightnessMaximum;
-            mBrightnessMin = info.brightnessMinimum;
-            mIsBrightnessOverriddenByWindow = info.isBrightnessOverrideByWindow;
 
+            updateBrightnessInfo(info);
             // Value is passed as intbits, since this is what the message takes.
             final int valueAsIntBits = Float.floatToIntBits(info.brightness);
             mMainHandler.obtainMessage(MSG_UPDATE_SLIDER, valueAsIntBits,
@@ -264,6 +262,12 @@
         }
     };
 
+    protected void updateBrightnessInfo(BrightnessInfo info) {
+        mBrightnessMax = info.brightnessMaximum;
+        mBrightnessMin = info.brightnessMinimum;
+        mIsBrightnessOverriddenByWindow = info.isBrightnessOverrideByWindow;
+    }
+
     private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
         @Override
         public void onVrStateChanged(boolean enabled) {
@@ -301,7 +305,7 @@
         }
     };
 
-    private final Handler mMainHandler;
+    protected final Handler mMainHandler;
 
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
@@ -459,7 +463,7 @@
         return !mAutomatic && !mTrackingTouch;
     }
 
-    private void updateSlider(float brightnessValue, boolean inVrMode) {
+    protected void updateSlider(float brightnessValue, boolean inVrMode) {
         final float min = mBrightnessMin;
         final float max = mBrightnessMax;
 
@@ -502,12 +506,17 @@
         mSliderAnimator.start();
     }
 
-
+    /** Factory interface for creating a {@link BrightnessController}. */
+    public interface Factory {
+        @NonNull
+        BrightnessController create(ToggleSlider toggleSlider);
+    }
 
     /** Factory for creating a {@link BrightnessController}. */
     @AssistedFactory
-    public interface Factory {
+    public interface BrightnessControllerFactory extends Factory {
         /** Create a {@link BrightnessController} */
+        @NonNull
         BrightnessController create(ToggleSlider toggleSlider);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index 503d0bf..02eca74 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -25,6 +25,7 @@
 import android.view.ViewGroup;
 import android.widget.SeekBar;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.UiEventLogger;
@@ -42,6 +43,7 @@
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.time.SystemClock;
+
 import com.google.android.msdl.domain.MSDLPlayer;
 
 import javax.inject.Inject;
@@ -89,7 +91,7 @@
         }
     };
 
-    BrightnessSliderController(
+    protected BrightnessSliderController(
             BrightnessSliderView brightnessSliderView,
             FalsingManager falsingManager,
             UiEventLogger uiEventLogger,
@@ -247,16 +249,20 @@
         return mView.isVisibleToUser();
     }
 
+    protected void handleSliderProgressChange(SeekBar seekBar, int progress, boolean fromUser) {
+        if (mListener != null) {
+            mListener.onChanged(mTracking, progress, false);
+            if (fromUser) {
+                mBrightnessSliderHapticPlugin.onProgressChanged(progress, true);
+            }
+        }
+    }
+
     private final SeekBar.OnSeekBarChangeListener mSeekListener =
             new SeekBar.OnSeekBarChangeListener() {
         @Override
         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-            if (mListener != null) {
-                mListener.onChanged(mTracking, progress, false);
-                if (fromUser) {
-                    mBrightnessSliderHapticPlugin.onProgressChanged(progress, true);
-                }
-            }
+            handleSliderProgressChange(seekBar, progress, fromUser);
         }
 
         @Override
@@ -289,11 +295,21 @@
         }
     };
 
+    /** Factory interface for creating a {@link BrightnessSliderController}. */
+    public interface Factory {
+        @NonNull
+        BrightnessSliderController create(
+                Context context,
+                @Nullable ViewGroup viewRoot);
+
+        int getLayout();
+    }
+
     /**
      * Creates a {@link BrightnessSliderController} with its associated view.
      */
-    public static class Factory {
 
+    public static class BrightnessSliderControllerFactory implements Factory {
         private final FalsingManager mFalsingManager;
         private final UiEventLogger mUiEventLogger;
         private final VibratorHelper mVibratorHelper;
@@ -303,7 +319,7 @@
         private final BrightnessWarningToast mBrightnessWarningToast;
 
         @Inject
-        public Factory(
+        public BrightnessSliderControllerFactory(
                 FalsingManager falsingManager,
                 UiEventLogger uiEventLogger,
                 VibratorHelper vibratorHelper,
@@ -328,6 +344,8 @@
          * @param viewRoot the {@link ViewGroup} that will contain the hierarchy. The inflated
          *                 hierarchy will not be attached
          */
+        @Override
+        @NonNull
         public BrightnessSliderController create(
                 Context context,
                 @Nullable ViewGroup viewRoot) {
@@ -345,7 +363,7 @@
         }
 
         /** Get the layout to inflate based on what slider to use */
-        private int getLayout() {
+        public int getLayout() {
             return R.layout.quick_settings_brightness_dialog;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java
index a39d25a..550ac62 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java
@@ -43,12 +43,12 @@
 public class BrightnessSliderView extends FrameLayout {
 
     @NonNull
-    private ToggleSeekBar mSlider;
+    protected ToggleSeekBar mSlider;
     private DispatchTouchEventListener mListener;
     private Gefingerpoken mOnInterceptListener;
     @Nullable
-    private Drawable mProgressDrawable;
-    private float mScale = 1f;
+    protected Drawable mProgressDrawable;
+    protected float mScale = 1f;
     private final Rect mSystemGestureExclusionRect = new Rect();
 
     public BrightnessSliderView(Context context) {
@@ -65,6 +65,10 @@
         super.onFinishInflate();
         setLayerType(LAYER_TYPE_HARDWARE, null);
 
+        initBrightnessViewComponents();
+    }
+
+    protected void initBrightnessViewComponents() {
         mSlider = requireViewById(R.id.slider);
         mSlider.setAccessibilityLabel(getContentDescription().toString());
         setBoundaryOffset();
@@ -81,7 +85,7 @@
         }
     }
 
-    private void setBoundaryOffset() {
+    protected void setBoundaryOffset() {
          //  BrightnessSliderView uses hardware layer; if the background of its children exceed its
          //  boundary, it'll be cropped. We need to expand its boundary so that the background of
          //  ToggleSeekBar (i.e. the focus state) can be correctly rendered.
@@ -131,7 +135,7 @@
      * @param admin
      * @see ToggleSeekBar#setEnforcedAdmin
      */
-    void setAdminBlocker(ToggleSeekBar.AdminBlocker blocker) {
+    protected void setAdminBlocker(ToggleSeekBar.AdminBlocker blocker) {
         mSlider.setAdminBlocker(blocker);
     }
 
@@ -211,7 +215,7 @@
         }
     }
 
-    private void applySliderScale() {
+    protected void applySliderScale() {
         if (mProgressDrawable != null) {
             final Rect r = mProgressDrawable.getBounds();
             int height = (int) (mProgressDrawable.getIntrinsicHeight() * mScale);
@@ -229,7 +233,7 @@
      * Interface to attach a listener for {@link View#dispatchTouchEvent}.
      */
     @FunctionalInterface
-    interface DispatchTouchEventListener {
+    public interface DispatchTouchEventListener {
         boolean onDispatchTouchEvent(MotionEvent ev);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java
index c241f21..a0985fc6 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSeekBar.java
@@ -85,12 +85,12 @@
         }
     }
 
-    void setAdminBlocker(AdminBlocker blocker) {
+    public void setAdminBlocker(AdminBlocker blocker) {
         mAdminBlocker = blocker;
         setEnabled(blocker == null);
     }
 
-    interface AdminBlocker {
+    public interface AdminBlocker {
         boolean block();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/dagger/BrightnessSliderModule.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/dagger/BrightnessSliderModule.kt
new file mode 100644
index 0000000..fbe442e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/dagger/BrightnessSliderModule.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings.brightness.dagger
+
+import com.android.systemui.settings.brightness.BrightnessController
+import com.android.systemui.settings.brightness.BrightnessSliderController
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class BrightnessSliderModule {
+
+    @Binds
+    abstract fun bindBrightnessSliderControllerFactory(
+        factory: BrightnessSliderController.BrightnessSliderControllerFactory
+    ): BrightnessSliderController.Factory
+
+    @Binds
+    abstract fun bindBrightnessControllerFactory(
+        factory: BrightnessController.BrightnessControllerFactory
+    ): BrightnessController.Factory
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
index b24edd9..d78f4d8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
@@ -24,7 +24,6 @@
 import android.graphics.PixelFormat;
 import android.graphics.drawable.Drawable;
 
-import com.android.keyguard.LockIconViewController;
 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 
@@ -39,7 +38,6 @@
     private final NotificationPanelViewController mNotificationPanelViewController;
     private final NotificationPanelView mView;
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
-    private final LockIconViewController mLockIconViewController;
     private final QuickSettingsController mQsController;
     private final Set<Integer> mDebugTextUsedYPositions;
     private final Paint mDebugPaint;
@@ -48,13 +46,11 @@
             NotificationPanelViewController notificationPanelViewController,
             NotificationPanelView notificationPanelView,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
-            LockIconViewController lockIconViewController,
             QuickSettingsController quickSettingsController
     ) {
         mNotificationPanelViewController = notificationPanelViewController;
         mView = notificationPanelView;
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
-        mLockIconViewController = lockIconViewController;
         mQsController = quickSettingsController;
         mDebugTextUsedYPositions = new HashSet<>();
         mDebugPaint = new Paint();
@@ -91,8 +87,6 @@
         }
         drawDebugInfo(canvas, mNotificationPanelViewController.getClockPositionResult().clockY,
                 Color.GRAY, "mClockPositionResult.clockY");
-        drawDebugInfo(canvas, (int) mLockIconViewController.getTop(), Color.GRAY,
-                "mLockIconViewController.getTop()");
 
         if (mNotificationPanelViewController.isKeyguardShowing()) {
             // Notifications have the space between those two lines.
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index d347084..8e418b7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -25,7 +25,6 @@
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.Flags.msdlFeedback;
 import static com.android.systemui.Flags.predictiveBackAnimateShade;
-import static com.android.systemui.Flags.smartspaceRelocateToBottom;
 import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
 import static com.android.systemui.classifier.Classifier.GENERIC;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
@@ -64,6 +63,8 @@
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.graphics.RenderEffect;
+import android.graphics.Shader;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Trace;
@@ -105,7 +106,6 @@
 import com.android.keyguard.KeyguardStatusViewController;
 import com.android.keyguard.KeyguardUnfoldTransition;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.LockIconViewController;
 import com.android.keyguard.dagger.KeyguardQsUserSwitchComponent;
 import com.android.keyguard.dagger.KeyguardStatusBarViewComponent;
 import com.android.keyguard.dagger.KeyguardStatusViewComponent;
@@ -128,11 +128,9 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.fragments.FragmentService;
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewConfigurator;
 import com.android.systemui.keyguard.MigrateClocksToBlueprint;
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -141,9 +139,9 @@
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder;
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
@@ -187,7 +185,6 @@
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
@@ -195,6 +192,7 @@
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -211,8 +209,6 @@
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardClockPositionAlgorithm;
 import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
@@ -374,8 +370,6 @@
     private float mExpandedHeight = 0;
     /** The current squish amount for the predictive back animation */
     private float mCurrentBackProgress = 0.0f;
-    @Deprecated
-    private KeyguardBottomAreaView mKeyguardBottomArea;
     private boolean mExpanding;
     private boolean mSplitShadeEnabled;
     /** The bottom padding reserved for elements of the keyguard measuring notifications. */
@@ -391,11 +385,8 @@
     private KeyguardUserSwitcherController mKeyguardUserSwitcherController;
     private KeyguardStatusBarViewController mKeyguardStatusBarViewController;
     private KeyguardStatusViewController mKeyguardStatusViewController;
-    private final LockIconViewController mLockIconViewController;
     private NotificationsQuickSettingsContainer mNotificationContainerParent;
     private final NotificationsQSContainerController mNotificationsQSContainerController;
-    private final Provider<KeyguardBottomAreaViewController>
-            mKeyguardBottomAreaViewControllerProvider;
     private boolean mAnimateNextPositionUpdate;
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
@@ -547,8 +538,6 @@
     private final NotificationListContainer mNotificationListContainer;
     private final NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     private final NPVCDownEventState.Buffer mLastDownEvents;
-    private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
-    private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
     private final KeyguardClockInteractor mKeyguardClockInteractor;
     private float mMinExpandHeight;
     private boolean mPanelUpdateWhenAnimatorEnds;
@@ -624,8 +613,6 @@
     private final SplitShadeStateController mSplitShadeStateController;
     private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
             mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
-    private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable =
-            () -> mKeyguardBottomArea.setVisibility(View.GONE);
     private final Runnable mHeadsUpExistenceChangedRunnable = () -> {
         setHeadsUpAnimatingAway(false);
         updateExpansionAndVisibility();
@@ -714,7 +701,6 @@
             MediaDataManager mediaDataManager,
             NotificationShadeDepthController notificationShadeDepthController,
             AmbientState ambientState,
-            LockIconViewController lockIconViewController,
             KeyguardMediaController keyguardMediaController,
             TapAgainViewController tapAgainViewController,
             NavigationModeController navigationModeController,
@@ -730,15 +716,12 @@
             ShadeRepository shadeRepository,
             Optional<SysUIUnfoldComponent> unfoldComponent,
             SysUiState sysUiState,
-            Provider<KeyguardBottomAreaViewController> keyguardBottomAreaViewControllerProvider,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             KeyguardIndicationController keyguardIndicationController,
             NotificationListContainer notificationListContainer,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             SystemClock systemClock,
-            KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
-            KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
             KeyguardClockInteractor keyguardClockInteractor,
             AlternateBouncerInteractor alternateBouncerInteractor,
             DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
@@ -852,7 +835,6 @@
         mNotificationListContainer = notificationListContainer;
         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
         mNavigationBarController = navigationBarController;
-        mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider;
         mNotificationsQSContainerController.init();
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory;
@@ -908,7 +890,6 @@
         mBottomAreaShadeAlphaAnimator.setInterpolator(Interpolators.ALPHA_OUT);
         mConversationNotificationManager = conversationNotificationManager;
         mAuthController = authController;
-        mLockIconViewController = lockIconViewController;
         mScreenOffAnimationController = screenOffAnimationController;
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
         mLastDownEvents = new NPVCDownEventState.Buffer(MAX_DOWN_EVENT_BUFFER_SIZE);
@@ -930,16 +911,13 @@
 
         if (DEBUG_DRAWABLE) {
             mView.getOverlay().add(new DebugDrawable(this, mView,
-                    mNotificationStackScrollLayoutController, mLockIconViewController,
-                    mQsController));
+                    mNotificationStackScrollLayoutController, mQsController));
         }
 
         mKeyguardUnfoldTransition = unfoldComponent.map(
                 SysUIUnfoldComponent::getKeyguardUnfoldTransition);
 
         updateUserSwitcherFlags();
-        mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
-        mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor;
         mKeyguardClockInteractor = keyguardClockInteractor;
         KeyguardLongPressViewBinder.bind(
                 mView.requireViewById(R.id.keyguard_long_press),
@@ -1062,12 +1040,6 @@
         mQsController.init();
         mShadeHeadsUpTracker.addTrackingHeadsUpListener(
                 mNotificationStackScrollLayoutController::setTrackingHeadsUp);
-        if (!KeyguardBottomAreaRefactor.isEnabled()) {
-            setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
-        }
-
-        initBottomArea();
-
         mWakeUpCoordinator.setStackScroller(mNotificationStackScrollLayoutController);
         mWakeUpCoordinator.addListener(new NotificationWakeUpCoordinator.WakeUpListener() {
             @Override
@@ -1183,6 +1155,11 @@
                 }, mMainDispatcher);
         }
 
+        if (com.android.systemui.Flags.bouncerUiRevamp()) {
+            collectFlow(mView, mKeyguardInteractor.primaryBouncerShowing,
+                    this::handleBouncerShowingChanged);
+        }
+
         // Ensures that flags are updated when an activity launches
         collectFlow(mView,
                 mShadeAnimationInteractor.isLaunchingActivity(),
@@ -1232,6 +1209,22 @@
         mQsController.loadDimens();
     }
 
+    private void handleBouncerShowingChanged(Boolean isBouncerShowing) {
+        if (!com.android.systemui.Flags.bouncerUiRevamp()) return;
+
+        if (isBouncerShowing && isExpanded()) {
+            // Blur the shade much lesser than the background surface so that the surface is
+            // distinguishable from the background.
+            float shadeBlurEffect = PrimaryBouncerTransition.MAX_BACKGROUND_BLUR_RADIUS / 3;
+            mView.setRenderEffect(RenderEffect.createBlurEffect(
+                    shadeBlurEffect,
+                    shadeBlurEffect,
+                    Shader.TileMode.MIRROR));
+        } else {
+            mView.setRenderEffect(null);
+        }
+    }
+
     private void updateViewControllers(
             FrameLayout userAvatarView,
             KeyguardUserSwitcherView keyguardUserSwitcherView) {
@@ -1421,23 +1414,6 @@
                         showKeyguardUserSwitcher /* enabled */);
 
         updateViewControllers(userAvatarView, keyguardUserSwitcherView);
-
-        if (!KeyguardBottomAreaRefactor.isEnabled()) {
-            // Update keyguard bottom area
-            int index = mView.indexOfChild(mKeyguardBottomArea);
-            mView.removeView(mKeyguardBottomArea);
-            KeyguardBottomAreaView oldBottomArea = mKeyguardBottomArea;
-            KeyguardBottomAreaViewController keyguardBottomAreaViewController =
-                    mKeyguardBottomAreaViewControllerProvider.get();
-            if (smartspaceRelocateToBottom()) {
-                keyguardBottomAreaViewController.init();
-            }
-            setKeyguardBottomArea(keyguardBottomAreaViewController.getView());
-            mKeyguardBottomArea.initFrom(oldBottomArea);
-            mView.addView(mKeyguardBottomArea, index);
-
-            initBottomArea();
-        }
         mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(),
                 mStatusBarStateController.getInterpolatedDozeAmount());
 
@@ -1462,10 +1438,6 @@
                     false,
                     mBarState);
         }
-
-        if (!KeyguardBottomAreaRefactor.isEnabled()) {
-            setKeyguardBottomAreaVisibility(mBarState, false);
-        }
     }
 
     private void attachSplitShadeMediaPlayerContainer(FrameLayout container) {
@@ -1475,22 +1447,6 @@
         mKeyguardMediaController.attachSplitShadeContainer(container);
     }
 
-    private void initBottomArea() {
-        if (!KeyguardBottomAreaRefactor.isEnabled()) {
-            mKeyguardBottomArea.init(
-                mKeyguardBottomAreaViewModel,
-                mFalsingManager,
-                mLockIconViewController,
-                stringResourceId ->
-                        mKeyguardIndicationController.showTransientIndication(stringResourceId),
-                mVibratorHelper,
-                mActivityStarter);
-
-            // Rebind (for now), as a new bottom area and indication area may have been created
-            mKeyguardViewConfigurator.bindIndicationArea();
-        }
-    }
-
     @VisibleForTesting
     void setMaxDisplayedNotifications(int maxAllowed) {
         mMaxAllowedKeyguardNotifications = maxAllowed;
@@ -1528,11 +1484,6 @@
         return mUnlockedScreenOffAnimationController.isAnimationPlaying();
     }
 
-    @Deprecated
-    private void setKeyguardBottomArea(KeyguardBottomAreaView keyguardBottomArea) {
-        mKeyguardBottomArea = keyguardBottomArea;
-    }
-
     /** Sets a listener to be notified when the shade starts opening or finishes closing. */
     public void setOpenCloseListener(OpenCloseListener openCloseListener) {
         SceneContainerFlag.assertInLegacyMode();
@@ -1660,10 +1611,6 @@
             mKeyguardStatusViewController.setLockscreenClockY(
                     mClockPositionAlgorithm.getExpandedPreferredClockY());
         }
-        if (!(MigrateClocksToBlueprint.isEnabled() || KeyguardBottomAreaRefactor.isEnabled())) {
-            mKeyguardBottomAreaInteractor.setClockPosition(
-                mClockPositionResult.clockX, mClockPositionResult.clockY);
-        }
 
         boolean animate = !SceneContainerFlag.isEnabled()
                 && mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
@@ -2382,25 +2329,6 @@
         }
     }
 
-    @Deprecated
-    private void setKeyguardBottomAreaVisibility(int statusBarState, boolean goingToFullShade) {
-        mKeyguardBottomArea.animate().cancel();
-        if (goingToFullShade) {
-            mKeyguardBottomArea.animate().alpha(0f).setStartDelay(
-                    mKeyguardStateController.getKeyguardFadingAwayDelay()).setDuration(
-                    mKeyguardStateController.getShortenedFadingAwayDuration()).setInterpolator(
-                    Interpolators.ALPHA_OUT).withEndAction(
-                    mAnimateKeyguardBottomAreaInvisibleEndRunnable).start();
-        } else if (statusBarState == KEYGUARD || statusBarState == StatusBarState.SHADE_LOCKED) {
-            mKeyguardBottomArea.setVisibility(View.VISIBLE);
-            if (!mIsOcclusionTransitionRunning) {
-                mKeyguardBottomArea.setAlpha(1f);
-            }
-        } else {
-            mKeyguardBottomArea.setVisibility(View.GONE);
-        }
-    }
-
     /**
      * When the back gesture triggers a fully-expanded shade --> QQS shade collapse transition,
      * the expansionFraction goes down from 1.0 --> 0.0 (collapsing), so the current "squish" amount
@@ -2755,12 +2683,7 @@
 
         float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction());
         alpha *= mBottomAreaShadeAlpha;
-        if (KeyguardBottomAreaRefactor.isEnabled()) {
-            mKeyguardInteractor.setAlpha(alpha);
-        } else {
-            mKeyguardBottomAreaInteractor.setAlpha(alpha);
-        }
-        mLockIconViewController.setAlpha(alpha);
+        mKeyguardInteractor.setAlpha(alpha);
     }
 
     private void onExpandingFinished() {
@@ -2967,11 +2890,7 @@
     }
 
     private void updateDozingVisibilities(boolean animate) {
-        if (KeyguardBottomAreaRefactor.isEnabled()) {
-            mKeyguardInteractor.setAnimateDozingTransitions(animate);
-        } else {
-            mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
-        }
+        mKeyguardInteractor.setAnimateDozingTransitions(animate);
         if (!mDozing && animate) {
             mKeyguardStatusBarViewController.animateKeyguardStatusBarIn();
         }
@@ -3212,11 +3131,7 @@
         mDozing = dozing;
         // TODO (b/) make listeners for this
         mNotificationStackScrollLayoutController.setDozing(mDozing, animate);
-        if (KeyguardBottomAreaRefactor.isEnabled()) {
-            mKeyguardInteractor.setAnimateDozingTransitions(animate);
-        } else {
-            mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
-        }
+        mKeyguardInteractor.setAnimateDozingTransitions(animate);
         mKeyguardStatusBarViewController.setDozing(mDozing);
         mQsController.setDozing(mDozing);
 
@@ -3267,7 +3182,6 @@
     }
 
     public void dozeTimeTick() {
-        mLockIconViewController.dozeTimeTick();
         if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.dozeTimeTick();
         }
@@ -4544,10 +4458,6 @@
                         mBarState);
             }
 
-            if (!KeyguardBottomAreaRefactor.isEnabled()) {
-                setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
-            }
-
             // TODO: maybe add a listener for barstate
             mBarState = statusBarState;
             mQsController.setBarState(statusBarState);
@@ -4813,12 +4723,8 @@
                 stackScroller.setMaxAlphaForKeyguard(alpha, "NPVC.setTransitionAlpha()");
             }
 
-            if (KeyguardBottomAreaRefactor.isEnabled()) {
-                mKeyguardInteractor.setAlpha(alpha);
-            } else {
-                mKeyguardBottomAreaInteractor.setAlpha(alpha);
-            }
-            mLockIconViewController.setAlpha(alpha);
+            mKeyguardInteractor.setAlpha(alpha);
+            //todo was this needed?
 
             if (mKeyguardQsUserSwitchController != null) {
                 mKeyguardQsUserSwitchController.setAlpha(alpha);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index bf672be..48bbb04 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -169,6 +169,10 @@
     public void onMovedToDisplay(int displayId, Configuration config) {
         super.onMovedToDisplay(displayId, config);
         ShadeWindowGoesAround.isUnexpectedlyInLegacyMode();
+        ShadeTraceLogger.logOnMovedToDisplay(displayId, config);
+        if (mConfigurationForwarder != null) {
+            mConfigurationForwarder.dispatchOnMovedToDisplay(displayId, config);
+        }
         // When the window is moved we're only receiving a call to this method instead of the
         // onConfigurationChange itself. Let's just trigegr a normal config change.
         onConfigurationChanged(config);
@@ -177,6 +181,7 @@
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
+        ShadeTraceLogger.logOnConfigChanged(newConfig);
         if (mConfigurationForwarder != null) {
             ShadeWindowGoesAround.isUnexpectedlyInLegacyMode();
             mConfigurationForwarder.onConfigurationChanged(newConfig);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
index a54f6b9..7bfe40c3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
@@ -16,23 +16,23 @@
 
 package com.android.systemui.shade
 
-import android.view.Display
+import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.display.data.repository.DisplayRepository
 import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository
 import com.android.systemui.shade.display.ShadeDisplayPolicy
-import com.android.systemui.shade.display.SpecificDisplayIdPolicy
 import com.android.systemui.statusbar.commandline.Command
 import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.util.settings.GlobalSettings
 import java.io.PrintWriter
 import javax.inject.Inject
-import kotlin.text.toIntOrNull
 
 @SysUISingleton
 class ShadePrimaryDisplayCommand
 @Inject
 constructor(
+    private val globalSettings: GlobalSettings,
     private val commandRegistry: CommandRegistry,
     private val displaysRepository: DisplayRepository,
     private val positionRepository: MutableShadeDisplaysRepository,
@@ -45,7 +45,7 @@
     }
 
     override fun help(pw: PrintWriter) {
-        pw.println("shade_display_override (<displayId>|<policyName>) ")
+        pw.println("shade_display_override <policyName> ")
         pw.println("Set the display which is holding the shade, or the policy that defines it.")
         pw.println()
         pw.println("shade_display_override policies")
@@ -56,9 +56,6 @@
         pw.println()
         pw.println("shade_display_override (list|status) ")
         pw.println("Lists available displays and which has the shade")
-        pw.println()
-        pw.println("shade_display_override any_external")
-        pw.println("Moves the shade to the first not-default display available")
     }
 
     override fun execute(pw: PrintWriter, args: List<String>) {
@@ -74,28 +71,24 @@
         fun execute() {
             when (val command = args.getOrNull(0)?.lowercase()) {
                 "reset" -> reset()
+                "policies" -> printPolicies()
                 "list",
                 "status" -> printStatus()
-                "policies" -> printPolicies()
-                "any_external" -> anyExternal()
                 null -> help(pw)
                 else -> parsePolicy(command)
             }
         }
 
         private fun parsePolicy(policyIdentifier: String) {
-            val displayId = policyIdentifier.toIntOrNull()
-            when {
-                displayId != null -> changeDisplay(displayId = displayId)
-                policies.any { it.name == policyIdentifier } -> {
-                    positionRepository.policy.value = policies.first { it.name == policyIdentifier }
-                }
-                else -> help(pw)
+            if (policies.any { it.name == policyIdentifier }) {
+                globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, policyIdentifier)
+            } else {
+                help(pw)
             }
         }
 
         private fun reset() {
-            positionRepository.policy.value = defaultPolicy
+            globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, defaultPolicy.name)
             pw.println("Reset shade display policy to default policy: ${defaultPolicy.name}")
         }
 
@@ -117,30 +110,5 @@
                 pw.println(if (currentPolicyName == it.name) " (Current policy)" else "")
             }
         }
-
-        private fun anyExternal() {
-            val anyExternalDisplay =
-                displaysRepository.displays.value.firstOrNull {
-                    it.displayId != Display.DEFAULT_DISPLAY
-                }
-            if (anyExternalDisplay == null) {
-                pw.println("No external displays available.")
-                return
-            }
-            setDisplay(anyExternalDisplay.displayId)
-        }
-
-        private fun changeDisplay(displayId: Int) {
-            if (displayId < 0) {
-                pw.println("Error: display id should be positive integer")
-            }
-
-            setDisplay(displayId)
-        }
-
-        private fun setDisplay(id: Int) {
-            positionRepository.policy.value = SpecificDisplayIdPolicy(id)
-            pw.println("New shade primary display id is $id")
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt
new file mode 100644
index 0000000..4516133
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.content.res.Configuration
+import android.os.Trace
+import com.android.app.tracing.TraceUtils.traceAsync
+
+/**
+ * Centralized logging for shade-related events to a dedicated Perfetto track.
+ *
+ * Used by shade components to log events to a track named [TAG]. This consolidates shade-specific
+ * events into a single track for easier analysis in Perfetto, rather than scattering them across
+ * various threads' logs.
+ */
+object ShadeTraceLogger {
+    private const val TAG = "ShadeTraceLogger"
+
+    @JvmStatic
+    fun logOnMovedToDisplay(displayId: Int, config: Configuration) {
+        if (!Trace.isEnabled()) return
+        Trace.instantForTrack(
+            Trace.TRACE_TAG_APP,
+            TAG,
+            "onMovedToDisplay(displayId=$displayId, dpi=" + config.densityDpi + ")",
+        )
+    }
+
+    @JvmStatic
+    fun logOnConfigChanged(config: Configuration) {
+        if (!Trace.isEnabled()) return
+        Trace.instantForTrack(
+            Trace.TRACE_TAG_APP,
+            TAG,
+            "onConfigurationChanged(dpi=" + config.densityDpi + ")",
+        )
+    }
+
+    fun logMoveShadeWindowTo(displayId: Int) {
+        if (!Trace.isEnabled()) return
+        Trace.instantForTrack(Trace.TRACE_TAG_APP, TAG, "moveShadeWindowTo(displayId=$displayId)")
+    }
+
+    fun traceReparenting(r: () -> Unit) {
+        traceAsync(TAG, { "reparenting" }) { r() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
index 8937ce3..e191120 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
@@ -50,7 +50,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.phone.StatusIconContainer
 import com.android.systemui.statusbar.phone.TapAgainView
@@ -145,20 +144,6 @@
             return notificationShadeWindowView.requireViewById(R.id.notification_panel)
         }
 
-        /**
-         * Constructs a new, unattached [KeyguardBottomAreaView].
-         *
-         * Note that this is explicitly _not_ a singleton, as we want to be able to reinflate it
-         */
-        @Provides
-        fun providesKeyguardBottomAreaView(
-            npv: NotificationPanelView,
-            @ShadeDisplayAware layoutInflater: LayoutInflater,
-        ): KeyguardBottomAreaView {
-            return layoutInflater.inflate(R.layout.keyguard_bottom_area, npv, false)
-                as KeyguardBottomAreaView
-        }
-
         @Provides
         @SysUISingleton
         fun providesLightRevealScrim(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
index 756241e..af48231 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
@@ -16,17 +16,22 @@
 
 package com.android.systemui.shade.data.repository
 
+import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS
 import android.view.Display
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.shade.display.ShadeDisplayPolicy
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 
 /** Source of truth for the display currently holding the shade. */
@@ -38,7 +43,7 @@
 /** Allows to change the policy that determines in which display the Shade window is visible. */
 interface MutableShadeDisplaysRepository : ShadeDisplaysRepository {
     /** Updates the policy to select where the shade is visible. */
-    val policy: MutableStateFlow<ShadeDisplayPolicy>
+    val policy: StateFlow<ShadeDisplayPolicy>
 }
 
 /** Keeps the policy and propagates the display id for the shade from it. */
@@ -46,9 +51,27 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class ShadeDisplaysRepositoryImpl
 @Inject
-constructor(defaultPolicy: ShadeDisplayPolicy, @Background bgScope: CoroutineScope) :
-    MutableShadeDisplaysRepository {
-    override val policy = MutableStateFlow<ShadeDisplayPolicy>(defaultPolicy)
+constructor(
+    globalSettings: GlobalSettings,
+    defaultPolicy: ShadeDisplayPolicy,
+    @Background bgScope: CoroutineScope,
+    policies: Set<@JvmSuppressWildcards ShadeDisplayPolicy>,
+) : MutableShadeDisplaysRepository {
+
+    override val policy: StateFlow<ShadeDisplayPolicy> =
+        globalSettings
+            .observerFlow(DEVELOPMENT_SHADE_DISPLAY_AWARENESS)
+            .onStart { emit(Unit) }
+            .map {
+                val current = globalSettings.getString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS)
+                for (policy in policies) {
+                    if (policy.name == current) return@map policy
+                }
+                globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, defaultPolicy.name)
+                return@map defaultPolicy
+            }
+            .distinctUntilChanged()
+            .stateIn(bgScope, SharingStarted.WhileSubscribed(), defaultPolicy)
 
     override val displayId: StateFlow<Int> =
         policy
diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/SpecificDisplayIdPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/DefaultDisplayShadePolicy.kt
similarity index 69%
rename from packages/SystemUI/src/com/android/systemui/shade/display/SpecificDisplayIdPolicy.kt
rename to packages/SystemUI/src/com/android/systemui/shade/display/DefaultDisplayShadePolicy.kt
index d43aad7..3819c6f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/display/SpecificDisplayIdPolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/display/DefaultDisplayShadePolicy.kt
@@ -21,12 +21,9 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 
-/** Policy to specify a display id explicitly. */
-open class SpecificDisplayIdPolicy(id: Int) : ShadeDisplayPolicy {
-    override val name: String = "display_${id}_policy"
+/** Policy to specify a default display explicitly. */
+class DefaultDisplayShadePolicy @Inject constructor() : ShadeDisplayPolicy {
+    override val name: String = "default_display"
 
-    override val displayId: StateFlow<Int> = MutableStateFlow(id)
+    override val displayId: StateFlow<Int> = MutableStateFlow(Display.DEFAULT_DISPLAY)
 }
-
-class DefaultDisplayShadePolicy @Inject constructor() :
-    SpecificDisplayIdPolicy(Display.DEFAULT_DISPLAY)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt
index bb96b0b..17b5e5b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt
@@ -23,6 +23,10 @@
 
 /** Describes the display the shade should be shown in. */
 interface ShadeDisplayPolicy {
+    /**
+     * String used to identify each policy and used to set policy via adb command. This value must
+     * match a value defined in the SettingsLib shade_display_awareness_values string array.
+     */
     val name: String
 
     /** The display id the shade should be at, according to this policy. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
index 08c03e2..8d536ac 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
@@ -27,6 +27,8 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.shade.ShadeTraceLogger.logMoveShadeWindowTo
+import com.android.systemui.shade.ShadeTraceLogger.traceReparenting
 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
 import com.android.systemui.util.kotlin.getOrNull
@@ -68,6 +70,7 @@
     /** Tries to move the shade. If anything wrong happens, fails gracefully without crashing. */
     private suspend fun moveShadeWindowTo(destinationId: Int) {
         Log.d(TAG, "Trying to move shade window to display with id $destinationId")
+        logMoveShadeWindowTo(destinationId)
         // Why using the shade context here instead of the view's Display?
         // The context's display is updated before the view one, so it is a better indicator of
         // which display the shade is supposed to be at. The View display is updated after the first
@@ -83,7 +86,9 @@
             return
         }
         try {
-            withContext(mainThreadContext) { reparentToDisplayId(id = destinationId) }
+            withContext(mainThreadContext) {
+                traceReparenting { reparentToDisplayId(id = destinationId) }
+            }
         } catch (e: IllegalStateException) {
             Log.e(
                 TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
index 50b5607..2d7476c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.shade.domain.interactor
 
-import com.android.keyguard.LockIconViewController
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -38,7 +37,6 @@
     @Background private val backgroundScope: CoroutineScope,
     private val shadeInteractor: ShadeInteractor,
     private val sceneInteractor: SceneInteractor,
-    private val lockIconViewController: LockIconViewController,
     shadeRepository: ShadeRepository,
 ) : ShadeLockscreenInteractor {
 
@@ -61,7 +59,7 @@
     }
 
     override fun dozeTimeTick() {
-        lockIconViewController.dozeTimeTick()
+        // TODO("b/383591086") Implement replacement or delete
     }
 
     @Deprecated("Not supported by scenes")
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
index ea4e065..3a5245d 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
@@ -65,8 +65,8 @@
     @Binds
     @Named(LOCKSCREEN_SMARTSPACE_PRECONDITION)
     abstract fun bindSmartspacePrecondition(
-        lockscreenPrecondition: LockscreenPrecondition?
-    ): SmartspacePrecondition?
+        lockscreenPrecondition: LockscreenPrecondition
+    ): SmartspacePrecondition
 
     @BindsOptionalOf
     @Named(GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
index 71efbab..3180a06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
@@ -14,19 +14,49 @@
 
 package com.android.systemui.statusbar;
 
+import android.annotation.IntDef;
 import android.content.pm.UserInfo;
 import android.util.SparseArray;
 
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 public interface NotificationLockscreenUserManager {
     String PERMISSION_SELF = "com.android.systemui.permission.SELF";
     String NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION
             = "com.android.systemui.statusbar.work_challenge_unlocked_notification_action";
 
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true,
+            prefix = {"REDACTION_TYPE_"},
+            value = {
+                    REDACTION_TYPE_NONE,
+                    REDACTION_TYPE_PUBLIC,
+                    REDACTION_TYPE_SENSITIVE_CONTENT})
+    @interface RedactionType {}
+
+    /**
+     * Indicates that a notification requires no redaction
+     */
+    int REDACTION_TYPE_NONE = 0;
+
+    /**
+     * Indicates that a notification should have all content redacted, showing the public view.
+     * Overrides all other redaction types.
+     */
+    int REDACTION_TYPE_PUBLIC = 1;
+
+    /**
+     * Indicates that a notification should have its main content redacted, due to detected
+     * sensitive content, such as a One-Time Password
+     */
+    int REDACTION_TYPE_SENSITIVE_CONTENT = 1 << 1;
+
     /**
      * @param userId user Id
-     * @return true if we re on a secure lock screen
+     * @return true if we're on a secure lock screen
      */
     boolean isLockscreenPublicMode(int userId);
 
@@ -68,7 +98,13 @@
 
     void updatePublicMode();
 
-    boolean needsRedaction(NotificationEntry entry);
+    /**
+     * Determine what type of redaction is needed, if any. Returns REDACTION_TYPE_NONE if no
+     * redaction type is needed, REDACTION_TYPE_PUBLIC if private notifications are blocked, and
+     * REDACTION_TYPE_SENSITIVE_CONTENT if sensitive content is detected, and REDACTION_TYPE_PUBLIC
+     * doesn't apply.
+     */
+    @RedactionType int getRedactionType(NotificationEntry entry);
 
     /**
      * Has the given user chosen to allow their private (full) notifications to be shown even
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index a79b78f..239257d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -654,8 +654,13 @@
         }
     }
 
-    /** @return true if the entry needs redaction when on the lockscreen. */
-    public boolean needsRedaction(NotificationEntry ent) {
+    /**
+     * Determine what type of redaction is needed, if any. Returns REDACTION_TYPE_NONE if no
+     * redaction type is needed, REDACTION_TYPE_PUBLIC if private notifications are blocked, and
+     * REDACTION_TYPE_SENSITIVE_CONTENT if sensitive content is detected, and REDACTION_TYPE_PUBLIC
+     * doesn't apply.
+     */
+    public @RedactionType int getRedactionType(NotificationEntry ent) {
         int userId = ent.getSbn().getUserId();
 
         boolean isCurrentUserRedactingNotifs =
@@ -675,13 +680,19 @@
                 ent.isNotificationVisibilityPrivate();
         boolean userForcesRedaction = packageHasVisibilityOverride(ent.getSbn().getKey());
 
-        if (keyguardPrivateNotifications()) {
-            return !mKeyguardAllowingNotifications || isNotifSensitive
-                    || userForcesRedaction || (notificationRequestsRedaction && isNotifRedacted);
-        } else {
-            return userForcesRedaction || isNotifSensitive
-                    || (notificationRequestsRedaction && isNotifRedacted);
+        if (userForcesRedaction) {
+            return REDACTION_TYPE_PUBLIC;
         }
+        if (notificationRequestsRedaction && isNotifRedacted) {
+            return REDACTION_TYPE_PUBLIC;
+        }
+        if (keyguardPrivateNotifications() && !mKeyguardAllowingNotifications) {
+            return REDACTION_TYPE_PUBLIC;
+        }
+        if (isNotifSensitive) {
+            return REDACTION_TYPE_SENSITIVE_CONTENT;
+        }
+        return REDACTION_TYPE_NONE;
     }
 
     private boolean packageHasVisibilityOverride(String key) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index 684466a..3408f4f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -34,6 +34,7 @@
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.app.animation.Interpolators
 import com.android.systemui.Dumpable
+import com.android.systemui.Flags.notificationShadeBlur
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
@@ -53,7 +54,6 @@
 import javax.inject.Inject
 import kotlin.math.max
 import kotlin.math.sign
-import com.android.systemui.Flags.notificationShadeBlur
 
 /**
  * Responsible for blurring the notification shade window, and applying a zoom effect to the
@@ -212,19 +212,13 @@
             shadeRadius = 0f
         }
 
-        var zoomOut = MathUtils.saturate(blurUtils.ratioOfBlurRadius(shadeRadius))
         var blur = shadeRadius.toInt()
-
-        if (inSplitShade) {
-            zoomOut = 0f
-        }
-
+        val zoomOut = blurRadiusToZoomOut(blurRadius = shadeRadius)
         // Make blur be 0 if it is necessary to stop blur effect.
         if (scrimsVisible) {
             if (!notificationShadeBlur()) {
                 blur = 0
             }
-            zoomOut = 0f
         }
 
         if (!blurUtils.supportsBlursOnWindows()) {
@@ -237,24 +231,43 @@
         return Pair(blur, zoomOut)
     }
 
+    private fun blurRadiusToZoomOut(blurRadius: Float): Float {
+        var zoomOut = MathUtils.saturate(blurUtils.ratioOfBlurRadius(blurRadius))
+        if (inSplitShade) {
+            zoomOut = 0f
+        }
+
+        if (scrimsVisible) {
+            zoomOut = 0f
+        }
+        return zoomOut
+    }
+
+    private val shouldBlurBeOpaque: Boolean
+        get() = if (notificationShadeBlur()) false else scrimsVisible && !blursDisabledForAppLaunch
+
     /** Callback that updates the window blur value and is called only once per frame. */
     @VisibleForTesting
     val updateBlurCallback =
         Choreographer.FrameCallback {
             updateScheduled = false
-            val (blur, zoomOut) = computeBlurAndZoomOut()
-            val opaque = if (notificationShadeBlur()) false else scrimsVisible && !blursDisabledForAppLaunch
+            val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut()
+            val opaque = shouldBlurBeOpaque
             Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur)
             blurUtils.applyBlur(root.viewRootImpl, blur, opaque)
-            lastAppliedBlur = blur
-            wallpaperController.setNotificationShadeZoom(zoomOut)
-            listeners.forEach {
-                it.onWallpaperZoomOutChanged(zoomOut)
-                it.onBlurRadiusChanged(blur)
-            }
-            notificationShadeWindowController.setBackgroundBlurRadius(blur)
+            onBlurApplied(blur, zoomOutFromShadeRadius)
         }
 
+    private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) {
+        lastAppliedBlur = appliedBlurRadius
+        wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius)
+        listeners.forEach {
+            it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius)
+            it.onBlurRadiusChanged(appliedBlurRadius)
+        }
+        notificationShadeWindowController.setBackgroundBlurRadius(appliedBlurRadius)
+    }
+
     /** Animate blurs when unlocking. */
     private val keyguardStateCallback =
         object : KeyguardStateController.Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
index bbecde8..dff6f56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt
@@ -137,7 +137,7 @@
             }
         }
 
-        return NotificationChipModel(key, statusBarChipIconView, whenTime, promotedContent)
+        return NotificationChipModel(key, statusBarChipIconView, promotedContent)
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
index 9f0638b..c6759da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
@@ -23,7 +23,5 @@
 data class NotificationChipModel(
     val key: String,
     val statusBarChipIconView: StatusBarIconView?,
-    // TODO(b/364653005): Use [PromotedNotificationContentModel.time] instead of a custom field.
-    val whenTime: Long,
     val promotedContent: PromotedNotificationContentModel,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
index 2d16f3b..66af275 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -82,21 +83,51 @@
                     )
                 }
             }
-        return if (headsUpState == PinnedStatus.PinnedByUser) {
+
+        if (headsUpState == PinnedStatus.PinnedByUser) {
             // If the user tapped the chip to show the HUN, we want to just show the icon because
             // the HUN will show the rest of the information.
-            OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener)
-        } else {
-            OngoingActivityChipModel.Shown.ShortTimeDelta(
+            return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener)
+        }
+
+        if (this.promotedContent.shortCriticalText != null) {
+            return OngoingActivityChipModel.Shown.Text(
                 icon,
                 colors,
-                time = this.whenTime,
+                this.promotedContent.shortCriticalText,
                 onClickListener,
             )
         }
-        // TODO(b/364653005): Use Notification.showWhen to determine if we should show the time.
-        // TODO(b/364653005): If Notification.shortCriticalText is set, use that instead of `when`.
-        // TODO(b/364653005): If the app that posted the notification is in the foreground, don't
-        // show that app's chip.
+
+        if (this.promotedContent.time == null) {
+            return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener)
+        }
+        when (this.promotedContent.time.mode) {
+            PromotedNotificationContentModel.When.Mode.BasicTime -> {
+                return OngoingActivityChipModel.Shown.ShortTimeDelta(
+                    icon,
+                    colors,
+                    time = this.promotedContent.time.time,
+                    onClickListener,
+                )
+            }
+            PromotedNotificationContentModel.When.Mode.CountUp -> {
+                return OngoingActivityChipModel.Shown.Timer(
+                    icon,
+                    colors,
+                    startTimeMs = this.promotedContent.time.time,
+                    onClickListener,
+                )
+            }
+            PromotedNotificationContentModel.When.Mode.CountDown -> {
+                // TODO(b/364653005): Support CountDown.
+                return OngoingActivityChipModel.Shown.Timer(
+                    icon,
+                    colors,
+                    startTimeMs = this.promotedContent.time.time,
+                    onClickListener,
+                )
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index cf69d40..0c4c1a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -310,9 +310,13 @@
 
     private fun View.setBackgroundPaddingForEmbeddedPaddingIcon() {
         val sidePadding =
-            context.resources.getDimensionPixelSize(
-                R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon
-            )
+            if (StatusBarNotifChips.isEnabled) {
+                0
+            } else {
+                context.resources.getDimensionPixelSize(
+                    R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon
+                )
+            }
         setPaddingRelative(sidePadding, paddingTop, sidePadding, paddingBottom)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 2dce4e3..18217d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -116,7 +116,8 @@
             override val colors: ColorsModel,
             // TODO(b/361346412): Enforce a max length requirement?
             val text: String,
-        ) : Shown(icon, colors, onClickListener = null) {
+            override val onClickListener: View.OnClickListener? = null,
+        ) : Shown(icon, colors, onClickListener) {
             override val logName = "Shown.Text"
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 2588c7a..46c84fbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl
 import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore
@@ -101,6 +102,19 @@
 
         @Provides
         @SysUISingleton
+        @IntoMap
+        @ClassKey(OngoingCallInteractor::class)
+        fun ongoingCallInteractor(
+            interactor: OngoingCallInteractor
+        ): CoreStartable =
+            if (StatusBarChipsModernization.isEnabled) {
+                interactor
+            } else {
+                CoreStartable.NOP
+            }
+
+        @Provides
+        @SysUISingleton
         fun lightBarController(store: LightBarControllerStore): LightBarController {
             return store.defaultDisplay
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index f75163d..2ecce1f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -416,7 +416,7 @@
                 /* showSnooze = */ adjustment.isSnoozeEnabled(),
                 /* isChildInGroup = */ adjustment.isChildInGroup(),
                 /* isGroupSummary = */ adjustment.isGroupSummary(),
-                /* needsRedaction = */ adjustment.getNeedsRedaction()
+                /* needsRedaction = */ adjustment.getRedactionType()
         );
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
index 04458f3..1875e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.DynamicPrivacyController
 import com.android.systemui.statusbar.notification.collection.GroupEntry
@@ -211,7 +212,8 @@
                 screenshareNotificationHiding() &&
                     sensitiveNotificationProtectionController.shouldProtectNotification(entry)
 
-            val needsRedaction = lockscreenUserManager.needsRedaction(entry)
+            val needsRedaction =
+                lockscreenUserManager.getRedactionType(entry) != REDACTION_TYPE_NONE
             val isSensitive = userPublic && needsRedaction
             entry.setSensitive(isSensitive || shouldProtectNotification, deviceSensitive)
             if (screenshareNotificationHiding()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
index ff72888..ff9d533 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.collection.inflation
 
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.render.NotifViewController
 
@@ -61,6 +62,6 @@
         val showSnooze: Boolean,
         val isChildInGroup: Boolean = false,
         val isGroupSummary: Boolean = false,
-        val needsRedaction: Boolean,
+        @RedactionType val redactionType: Int,
     )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
index e70fb6b..331ef1c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
@@ -20,22 +20,24 @@
 import android.app.RemoteInput
 import android.graphics.drawable.Icon
 import android.text.TextUtils
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation
 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
 
 /**
  * An immutable object which contains minimal state extracted from an entry that represents state
- * which can change without a direct app update (e.g. with a ranking update).
- * Diffing two entries determines if view re-inflation is needed.
+ * which can change without a direct app update (e.g. with a ranking update). Diffing two entries
+ * determines if view re-inflation is needed.
  */
-class NotifUiAdjustment internal constructor(
+class NotifUiAdjustment
+internal constructor(
     val key: String,
     val smartActions: List<Notification.Action>,
     val smartReplies: List<CharSequence>,
     val isConversation: Boolean,
     val isSnoozeEnabled: Boolean,
     val isMinimized: Boolean,
-    val needsRedaction: Boolean,
+    @RedactionType val redactionType: Int,
     val isChildInGroup: Boolean,
     val isGroupSummary: Boolean,
 ) {
@@ -43,65 +45,72 @@
         @JvmStatic
         fun needReinflate(
             oldAdjustment: NotifUiAdjustment,
-            newAdjustment: NotifUiAdjustment
-        ): Boolean = when {
-            oldAdjustment === newAdjustment -> false
-            oldAdjustment.isConversation != newAdjustment.isConversation -> true
-            oldAdjustment.isSnoozeEnabled != newAdjustment.isSnoozeEnabled -> true
-            oldAdjustment.isMinimized != newAdjustment.isMinimized -> true
-            oldAdjustment.needsRedaction != newAdjustment.needsRedaction -> true
-            areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true
-            newAdjustment.smartReplies != oldAdjustment.smartReplies -> true
-            AsyncHybridViewInflation.isEnabled &&
-                    !oldAdjustment.isChildInGroup && newAdjustment.isChildInGroup -> true
-            AsyncGroupHeaderViewInflation.isEnabled &&
-                !oldAdjustment.isGroupSummary && newAdjustment.isGroupSummary -> true
-            else -> false
-        }
+            newAdjustment: NotifUiAdjustment,
+        ): Boolean =
+            when {
+                oldAdjustment === newAdjustment -> false
+                oldAdjustment.isConversation != newAdjustment.isConversation -> true
+                oldAdjustment.isSnoozeEnabled != newAdjustment.isSnoozeEnabled -> true
+                oldAdjustment.isMinimized != newAdjustment.isMinimized -> true
+                oldAdjustment.redactionType != newAdjustment.redactionType -> true
+                areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true
+                newAdjustment.smartReplies != oldAdjustment.smartReplies -> true
+                AsyncHybridViewInflation.isEnabled &&
+                    !oldAdjustment.isChildInGroup &&
+                    newAdjustment.isChildInGroup -> true
+                AsyncGroupHeaderViewInflation.isEnabled &&
+                    !oldAdjustment.isGroupSummary &&
+                    newAdjustment.isGroupSummary -> true
+                else -> false
+            }
 
         private fun areDifferent(
             first: List<Notification.Action>,
-            second: List<Notification.Action>
-        ): Boolean = when {
-            first === second -> false
-            first.size != second.size -> true
-            else -> first.asSequence().zip(second.asSequence()).any {
-                (!TextUtils.equals(it.first.title, it.second.title)) ||
-                    (areDifferent(it.first.getIcon(), it.second.getIcon())) ||
-                    (it.first.actionIntent != it.second.actionIntent) ||
-                    (areDifferent(it.first.remoteInputs, it.second.remoteInputs))
+            second: List<Notification.Action>,
+        ): Boolean =
+            when {
+                first === second -> false
+                first.size != second.size -> true
+                else ->
+                    first.asSequence().zip(second.asSequence()).any {
+                        (!TextUtils.equals(it.first.title, it.second.title)) ||
+                            (areDifferent(it.first.getIcon(), it.second.getIcon())) ||
+                            (it.first.actionIntent != it.second.actionIntent) ||
+                            (areDifferent(it.first.remoteInputs, it.second.remoteInputs))
+                    }
             }
-        }
 
-        private fun areDifferent(first: Icon?, second: Icon?): Boolean = when {
-            first === second -> false
-            first == null || second == null -> true
-            else -> !first.sameAs(second)
-        }
-
-        private fun areDifferent(
-            first: Array<RemoteInput>?,
-            second: Array<RemoteInput>?
-        ): Boolean = when {
-            first === second -> false
-            first == null || second == null -> true
-            first.size != second.size -> true
-            else -> first.asSequence().zip(second.asSequence()).any {
-                (!TextUtils.equals(it.first.label, it.second.label)) ||
-                    (areDifferent(it.first.choices, it.second.choices))
+        private fun areDifferent(first: Icon?, second: Icon?): Boolean =
+            when {
+                first === second -> false
+                first == null || second == null -> true
+                else -> !first.sameAs(second)
             }
-        }
+
+        private fun areDifferent(first: Array<RemoteInput>?, second: Array<RemoteInput>?): Boolean =
+            when {
+                first === second -> false
+                first == null || second == null -> true
+                first.size != second.size -> true
+                else ->
+                    first.asSequence().zip(second.asSequence()).any {
+                        (!TextUtils.equals(it.first.label, it.second.label)) ||
+                            (areDifferent(it.first.choices, it.second.choices))
+                    }
+            }
 
         private fun areDifferent(
             first: Array<CharSequence>?,
-            second: Array<CharSequence>?
-        ): Boolean = when {
-            first === second -> false
-            first == null || second == null -> true
-            first.size != second.size -> true
-            else -> first.asSequence().zip(second.asSequence()).any {
-                !TextUtils.equals(it.first, it.second)
+            second: Array<CharSequence>?,
+        ): Boolean =
+            when {
+                first === second -> false
+                first == null || second == null -> true
+                first.size != second.size -> true
+                else ->
+                    first.asSequence().zip(second.asSequence()).any {
+                        !TextUtils.equals(it.first, it.second)
+                    }
             }
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
index 4c82bc1..97e55c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
@@ -79,7 +80,7 @@
             secureSettings.registerContentObserverForUserSync(
                 SHOW_NOTIFICATION_SNOOZE,
                 settingsObserver,
-                UserHandle.USER_ALL
+                UserHandle.USER_ALL,
             )
         }
         dirtyListeners.addIfAbsent(listener)
@@ -140,10 +141,15 @@
             isConversation = entry.ranking.isConversation,
             isSnoozeEnabled = isSnoozeSettingsEnabled && !entry.isCanceled,
             isMinimized = isEntryMinimized(entry),
-            needsRedaction =
-                lockscreenUserManager.needsRedaction(entry) ||
-                    (screenshareNotificationHiding() &&
-                        sensitiveNotifProtectionController.shouldProtectNotification(entry)),
+            redactionType =
+                if (
+                    screenshareNotificationHiding() &&
+                        sensitiveNotifProtectionController.shouldProtectNotification(entry)
+                ) {
+                    REDACTION_TYPE_PUBLIC
+                } else {
+                    lockscreenUserManager.getRedactionType(entry)
+                },
             isChildInGroup = entry.hasEverBeenGroupChild(),
             isGroupSummary = entry.hasEverBeenGroupSummary(),
         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index e6d22b0..80e8f55 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.notification.collection.inflation;
 
-import static com.android.server.notification.Flags.screenshareNotificationHiding;
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
@@ -256,9 +256,8 @@
         params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED);
         params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
         params.setUseMinimized(isMinimized);
-        boolean needsRedaction = screenshareNotificationHiding()
-                ? inflaterParams.getNeedsRedaction()
-                : mNotificationLockscreenUserManager.needsRedaction(entry);
+        // TODO b/358403414: use the different types of redaction
+        boolean needsRedaction = inflaterParams.getRedactionType() != REDACTION_TYPE_NONE;
 
         if (needsRedaction) {
             params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
index 6aa8d0a..6756077 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
@@ -47,6 +47,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingBannedListener;
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
@@ -65,6 +66,11 @@
 import com.android.systemui.util.settings.GlobalSettings;
 import com.android.systemui.util.time.SystemClock;
 
+import kotlinx.coroutines.flow.Flow;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
 import org.jetbrains.annotations.NotNull;
 
 import java.io.PrintWriter;
@@ -78,11 +84,6 @@
 
 import javax.inject.Inject;
 
-import kotlinx.coroutines.flow.Flow;
-import kotlinx.coroutines.flow.MutableStateFlow;
-import kotlinx.coroutines.flow.StateFlow;
-import kotlinx.coroutines.flow.StateFlowKt;
-
 /**
  * A manager which handles heads up notifications which is a special mode where
  * they simply peek from the top of the screen.
@@ -93,15 +94,15 @@
     private static final String TAG = "BaseHeadsUpManager";
     private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
     private static final String REASON_REORDER_ALLOWED = "mOnReorderingAllowedListener";
-    protected final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>();
+    private final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>();
 
-    protected final Context mContext;
+    private final Context mContext;
 
-    protected int mTouchAcceptanceDelay;
-    protected int mSnoozeLengthMs;
-    protected boolean mHasPinnedNotification;
+    private final int mTouchAcceptanceDelay;
+    private int mSnoozeLengthMs;
+    private boolean mHasPinnedNotification;
     private PinnedStatus mPinnedNotificationStatus = PinnedStatus.NotPinned;
-    protected int mUser;
+    private int mUser;
 
     private final ArrayMap<String, Long> mSnoozedPackages;
     private final AccessibilityManagerWrapper mAccessibilityMgr;
@@ -113,13 +114,14 @@
     private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>();
     private final VisualStabilityProvider mVisualStabilityProvider;
 
-    protected final SystemClock mSystemClock;
-    protected final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>();
-    protected final HeadsUpManagerLogger mLogger;
-    protected int mMinimumDisplayTime;
-    protected int mStickyForSomeTimeAutoDismissTime;
-    protected int mAutoDismissTime;
-    protected DelayableExecutor mExecutor;
+    private final SystemClock mSystemClock;
+    @VisibleForTesting
+    final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>();
+    private final HeadsUpManagerLogger mLogger;
+    private final int mMinimumDisplayTime;
+    private final int mStickyForSomeTimeAutoDismissTime;
+    private final int mAutoDismissTime;
+    private final DelayableExecutor mExecutor;
 
     private final int mExtensionTime;
 
@@ -133,7 +135,7 @@
     private final HashSet<String> mSwipedOutKeys = new HashSet<>();
     private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
     @VisibleForTesting
-    public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
+    final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
             = new ArraySet<>();
 
     private boolean mReleaseOnExpandFinish;
@@ -383,9 +385,7 @@
         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
         mLogger.logUpdateNotificationRequest(key, requestedPinnedStatus, headsUpEntry != null);
 
-        Runnable runnable = () -> {
-            updateNotificationInternal(key, requestedPinnedStatus);
-        };
+        Runnable runnable = () -> updateNotificationInternal(key, requestedPinnedStatus);
         mAvalancheController.update(headsUpEntry, runnable, "updateNotification");
     }
 
@@ -407,9 +407,7 @@
             headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification");
             PinnedStatus pinnedStatus =
                     getNewPinnedStatusForEntry(headsUpEntry, requestedPinnedStatus);
-            if (headsUpEntry != null) {
-                setEntryPinned(headsUpEntry, pinnedStatus, "updateNotificationInternal");
-            }
+            setEntryPinned(headsUpEntry, pinnedStatus, "updateNotificationInternal");
         }
     }
 
@@ -515,8 +513,7 @@
     }
 
     /**
-     * @param key
-     * @return When a HUN entry should be removed in milliseconds from now
+     * @return When a HUN entry with the given key should be removed in milliseconds from now
      */
     @Override
     public long getEarliestRemovalTime(String key) {
@@ -528,7 +525,10 @@
     }
 
     @VisibleForTesting
-    protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) {
+    boolean shouldHeadsUpBecomePinned(@Nullable NotificationEntry entry) {
+        if (entry == null) {
+            return false;
+        }
         boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsShadeOrQsExpanded;
         if (SceneContainerFlag.isEnabled()) {
             pin |= mIsQsExpanded;
@@ -549,24 +549,18 @@
         return hasFullScreenIntent(entry) && !headsUpEntry.mWasUnpinned;
     }
 
-    protected boolean hasFullScreenIntent(@NonNull NotificationEntry entry) {
-        if (entry == null) {
-            return false;
-        }
-        if (entry.getSbn() == null) {
-            return false;
-        }
+    private boolean hasFullScreenIntent(@NonNull NotificationEntry entry) {
         if (entry.getSbn().getNotification() == null) {
             return false;
         }
         return entry.getSbn().getNotification().fullScreenIntent != null;
     }
 
-    protected void setEntryPinned(
+    private void setEntryPinned(
             @NonNull HeadsUpManagerImpl.HeadsUpEntry headsUpEntry, PinnedStatus pinnedStatus,
             String reason) {
-        mLogger.logSetEntryPinned(headsUpEntry.mEntry, pinnedStatus, reason);
-        NotificationEntry entry = headsUpEntry.mEntry;
+        NotificationEntry entry = headsUpEntry.requireEntry();
+        mLogger.logSetEntryPinned(entry, pinnedStatus, reason);
         boolean isPinned = pinnedStatus.isPinned();
         if (!isPinned) {
             headsUpEntry.mWasUnpinned = true;
@@ -574,7 +568,7 @@
         if (headsUpEntry.getPinnedStatus().getValue() != pinnedStatus) {
             headsUpEntry.setRowPinnedStatus(pinnedStatus);
             updatePinnedMode();
-            if (isPinned && entry.getSbn() != null) {
+            if (isPinned) {
                mUiEventLogger.logWithInstanceId(
                         NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(),
                         entry.getSbn().getPackageName(), entry.getSbn().getInstanceId());
@@ -595,8 +589,8 @@
      * @param headsUpEntry entry added
      */
     @VisibleForTesting
-    void onEntryAdded(HeadsUpEntry headsUpEntry, PinnedStatus requestedPinnedStatus) {
-        NotificationEntry entry = headsUpEntry.mEntry;
+     void onEntryAdded(HeadsUpEntry headsUpEntry, PinnedStatus requestedPinnedStatus) {
+        NotificationEntry entry = headsUpEntry.requireEntry();
         entry.setHeadsUp(true);
 
         PinnedStatus pinnedStatus = getNewPinnedStatusForEntry(headsUpEntry, requestedPinnedStatus);
@@ -635,7 +629,7 @@
      * Remove a notification from the alerting entries.
      * @param key key of notification to remove
      */
-    protected final void removeEntry(@NonNull String key, String reason) {
+    private void removeEntry(@NonNull String key, String reason) {
         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
         boolean isWaiting;
         if (headsUpEntry == null) {
@@ -652,10 +646,10 @@
             if (finalHeadsUpEntry == null) {
                 return;
             }
-            NotificationEntry entry = finalHeadsUpEntry.mEntry;
+            NotificationEntry entry = finalHeadsUpEntry.requireEntry();
 
             // If the notification is animating, we will remove it at the end of the animation.
-            if (entry != null && entry.isExpandAnimationRunning()) {
+            if (entry.isExpandAnimationRunning()) {
                 return;
             }
             entry.demoteStickyHun();
@@ -677,8 +671,9 @@
      * @param headsUpEntry entry removed
      * @param reason why onEntryRemoved was called
      */
-    protected void onEntryRemoved(HeadsUpEntry headsUpEntry, String reason) {
-        NotificationEntry entry = headsUpEntry.mEntry;
+    @VisibleForTesting
+    void onEntryRemoved(@NonNull HeadsUpEntry headsUpEntry, String reason) {
+        NotificationEntry entry = headsUpEntry.requireEntry();
         entry.setHeadsUp(false);
         setEntryPinned(headsUpEntry, PinnedStatus.NotPinned, "onEntryRemoved");
         EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */);
@@ -700,15 +695,14 @@
             // mEntriesToRemoveWhenReorderingAllowed, we should not remove from this list (and cause
             // ArrayIndexOutOfBoundsException). We don't need to in this case anyway, because we
             // clear mEntriesToRemoveWhenReorderingAllowed after removing these entries.
-            if (!reason.equals(REASON_REORDER_ALLOWED)
-                    && mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)) {
+            if (!reason.equals(REASON_REORDER_ALLOWED)) {
                 mEntriesToRemoveWhenReorderingAllowed.remove(notifEntry);
             }
         }
     }
 
     private void updateTopHeadsUpFlow() {
-        mTopHeadsUpRow.setValue((HeadsUpRowRepository) getTopHeadsUpEntry());
+        mTopHeadsUpRow.setValue(getTopHeadsUpEntry());
     }
 
     private void updateHeadsUpFlow() {
@@ -753,18 +747,7 @@
         }
     }
 
-    /**
-     * Manager-specific logic, that should occur, when the entry is updated, and its posted time has
-     * changed.
-     *
-     * @param headsUpEntry entry updated
-     */
-    protected void onEntryUpdated(HeadsUpEntry headsUpEntry) {
-        // no need to update the list here
-        updateTopHeadsUpFlow();
-    }
-
-    protected void updatePinnedMode() {
+    private void updatePinnedMode() {
         boolean hasPinnedNotification = hasPinnedNotificationInternal();
         mPinnedNotificationStatus = pinnedNotificationStatusInternal();
         if (hasPinnedNotification == mHasPinnedNotification) {
@@ -806,7 +789,7 @@
         keySet.addAll(mAvalancheController.getWaitingKeys());
         for (String key : keySet) {
             HeadsUpEntry entry = getHeadsUpEntry(key);
-            if (entry.mEntry == null) {
+            if (entry == null || entry.mEntry == null) {
                 continue;
             }
             String packageName = entry.mEntry.getSbn().getPackageName();
@@ -828,7 +811,8 @@
     }
 
     @Nullable
-    protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) {
+    @VisibleForTesting
+    HeadsUpEntry getHeadsUpEntry(@NonNull String key) {
         if (mHeadsUpEntryMap.containsKey(key)) {
             return mHeadsUpEntryMap.get(key);
         }
@@ -845,7 +829,7 @@
     }
 
     @Nullable
-    protected HeadsUpEntry getTopHeadsUpEntry() {
+    private HeadsUpEntry getTopHeadsUpEntry() {
         if (mHeadsUpEntryMap.isEmpty()) {
             return null;
         }
@@ -941,7 +925,7 @@
         dumpInternal(pw, args);
     }
 
-    protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) {
+    private void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.print("  mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
         pw.print("  mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
         pw.print("  now="); pw.println(mSystemClock.elapsedRealtime());
@@ -978,7 +962,7 @@
     private boolean hasPinnedNotificationInternal() {
         for (String key : mHeadsUpEntryMap.keySet()) {
             HeadsUpEntry entry = getHeadsUpEntry(key);
-            if (entry.mEntry != null && entry.mEntry.isRowPinned()) {
+            if (entry != null && entry.mEntry != null && entry.mEntry.isRowPinned()) {
                 return true;
             }
         }
@@ -1003,6 +987,10 @@
     public void unpinAll(boolean userUnPinned) {
         for (String key : mHeadsUpEntryMap.keySet()) {
             HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
+            if (headsUpEntry == null) {
+                Log.wtf(TAG, "Couldn't find entry " + key + " in unpinAll");
+                continue;
+            }
             mLogger.logUnpinEntryRequest(key);
             Runnable runnable = () -> {
                 mLogger.logUnpinEntry(key);
@@ -1013,10 +1001,10 @@
 
                 // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay
                 // on the screen.
-                if (userUnPinned && headsUpEntry.mEntry != null) {
-                    if (headsUpEntry.mEntry != null && headsUpEntry.mEntry.mustStayOnScreen()) {
-                        headsUpEntry.mEntry.setHeadsUpIsVisible();
-                    }
+                if (userUnPinned
+                        && headsUpEntry.mEntry != null
+                        && headsUpEntry.mEntry.mustStayOnScreen()) {
+                    headsUpEntry.mEntry.setHeadsUpIsVisible();
                 }
             };
             mAvalancheController.delete(headsUpEntry, runnable, "unpinAll");
@@ -1037,7 +1025,7 @@
             } else {
                 headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)");
             }
-            onEntryUpdated(headsUpEntry);
+            updateTopHeadsUpFlow();
         }
     }
 
@@ -1113,7 +1101,7 @@
      *
      * @param entry the entry that might be indirectly removed by the user's action
      *
-     * @see HeadsUpCoordinator#mActionPressListener
+     * @see HeadsUpCoordinator.mActionPressListener
      * @see #canRemoveImmediately(String)
      */
     public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) {
@@ -1152,8 +1140,8 @@
     }
 
     /**
-     * @param key
-     * @return true if the entry is (pinned and expanded) or (has an active remote input)
+     * @return true if the entry with the given key is (pinned and expanded) or (has an active
+     * remote input)
      */
     @Override
     public boolean isSticky(String key) {
@@ -1165,7 +1153,8 @@
     }
 
     @NonNull
-    protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
+    @VisibleForTesting
+    HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
         if (NotificationThrottleHun.isEnabled()) {
             return new HeadsUpEntry(entry);
         } else {
@@ -1189,7 +1178,7 @@
     }
 
     @VisibleForTesting
-    public final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> {
+    final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> {
         if (NotificationThrottleHun.isEnabled()) {
             mAvalancheController.setEnableAtRuntime(true);
             if (mEntriesToRemoveWhenReorderingAllowed.isEmpty()) {
@@ -1266,14 +1255,15 @@
         public boolean mRemoteInputActive;
         public boolean mUserActionMayIndirectlyRemove;
 
-        protected boolean mExpanded;
-        protected boolean mWasUnpinned;
+        private boolean mExpanded;
+        @VisibleForTesting
+        boolean mWasUnpinned;
 
         @Nullable public NotificationEntry mEntry;
         public long mPostTime;
         public long mEarliestRemovalTime;
 
-        @Nullable protected Runnable mRemoveRunnable;
+        @Nullable private Runnable mRemoveRunnable;
 
         @Nullable private Runnable mCancelRemoveRunnable;
 
@@ -1325,7 +1315,7 @@
             setEntry(entry, createRemoveRunnable(entry));
         }
 
-        protected void setEntry(
+        private void setEntry(
                 @NonNull final NotificationEntry entry,
                 @Nullable Runnable removeRunnable) {
             mEntry = entry;
@@ -1342,7 +1332,8 @@
             }
         }
 
-        protected void setRowPinnedStatus(PinnedStatus pinnedStatus) {
+        @VisibleForTesting
+        void setRowPinnedStatus(PinnedStatus pinnedStatus) {
             if (mEntry != null) mEntry.setRowPinnedStatus(pinnedStatus);
             mPinnedStatus.setValue(pinnedStatus);
         }
@@ -1350,7 +1341,7 @@
         /**
          * An interface that returns the amount of time left this HUN should show.
          */
-        interface FinishTimeUpdater {
+        private interface FinishTimeUpdater {
             long updateAndGetTimeRemaining();
         }
 
@@ -1370,6 +1361,10 @@
         public void updateEntry(boolean updatePostTime, boolean updateEarliestRemovalTime,
                 @Nullable String reason) {
             Runnable runnable = () -> {
+                if (mEntry == null) {
+                    Log.wtf(TAG, "#updateEntry called with null mEntry; returning early");
+                    return;
+                }
                 mLogger.logUpdateEntry(mEntry, updatePostTime, reason);
 
                 final long now = mSystemClock.elapsedRealtime();
@@ -1391,23 +1386,18 @@
             FinishTimeUpdater finishTimeCalculator = () -> {
                 final long finishTime = calculateFinishTime();
                 final long now = mSystemClock.elapsedRealtime();
-                final long timeLeft = NotificationThrottleHun.isEnabled()
+                return NotificationThrottleHun.isEnabled()
                         ? Math.max(finishTime, mEarliestRemovalTime) - now
                         : Math.max(finishTime - now, mMinimumDisplayTime);
-                return timeLeft;
             };
             scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)");
 
             // Notify the manager, that the posted time has changed.
-            onEntryUpdated(this);
+            updateTopHeadsUpFlow();
 
-            if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
-                mEntriesToRemoveAfterExpand.remove(mEntry);
-            }
+            mEntriesToRemoveAfterExpand.remove(mEntry);
             if (!NotificationThrottleHun.isEnabled()) {
-                if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
-                    mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
-                }
+                mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
             }
         }
 
@@ -1528,8 +1518,7 @@
         @Override
         public boolean equals(@Nullable Object o) {
             if (this == o) return true;
-            if (o == null || !(o instanceof HeadsUpEntry)) return false;
-            HeadsUpEntry otherHeadsUpEntry = (HeadsUpEntry) o;
+            if (!(o instanceof HeadsUpEntry otherHeadsUpEntry)) return false;
             if (mEntry != null && otherHeadsUpEntry.mEntry != null) {
                 return mEntry.getKey().equals(otherHeadsUpEntry.mEntry.getKey());
             }
@@ -1593,10 +1582,13 @@
             }
         }
 
-        public void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator,
+        private void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator,
                 @NonNull String reason) {
-
-            mLogger.logAutoRemoveRequest(this.mEntry, reason);
+            if (mEntry == null) {
+                Log.wtf(TAG, "#scheduleAutoRemovalCallback with null mEntry; returning early");
+                return;
+            }
+            mLogger.logAutoRemoveRequest(mEntry, reason);
             Runnable runnable = () -> {
                 long delayMs = finishTimeCalculator.updateAndGetTimeRemaining();
 
@@ -1636,16 +1628,14 @@
         public void removeAsSoonAsPossible() {
             if (mRemoveRunnable != null) {
 
-                FinishTimeUpdater finishTimeCalculator = () -> {
-                    final long timeLeft = mEarliestRemovalTime - mSystemClock.elapsedRealtime();
-                    return timeLeft;
-                };
+                FinishTimeUpdater finishTimeCalculator = () ->
+                        mEarliestRemovalTime - mSystemClock.elapsedRealtime();
                 scheduleAutoRemovalCallback(finishTimeCalculator, "removeAsSoonAsPossible");
             }
         }
 
         /** Creates a runnable to remove this notification from the alerting entries. */
-        protected Runnable createRemoveRunnable(NotificationEntry entry) {
+        private Runnable createRemoveRunnable(NotificationEntry entry) {
             return () -> {
                 if (!NotificationThrottleHun.isEnabled()
                         && !mVisualStabilityProvider.isReorderingAllowed()
@@ -1669,7 +1659,7 @@
          * Calculate what the post time of a notification is at some current time.
          * @return the post time
          */
-        protected long calculatePostTime() {
+        private long calculatePostTime() {
             // The actual post time will be just after the heads-up really slided in
             return mSystemClock.elapsedRealtime() + mTouchAcceptanceDelay;
         }
@@ -1678,7 +1668,7 @@
          * @return When the notification should auto-dismiss itself, based on
          * {@link SystemClock#elapsedRealtime()}
          */
-        protected long calculateFinishTime() {
+        private long calculateFinishTime() {
             int requestedTimeOutMs;
             if (isStickyForSomeTime()) {
                 requestedTimeOutMs = mStickyForSomeTimeAutoDismissTime;
@@ -1692,9 +1682,8 @@
         /**
          * Get user-preferred or default timeout duration. The larger one will be returned.
          * @return milliseconds before auto-dismiss
-         * @param requestedTimeout
          */
-        protected int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) {
+        private int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) {
             return mAccessibilityMgr.getRecommendedTimeoutMillis(
                     requestedTimeout,
                     AccessibilityManager.FLAG_CONTENT_CONTROLS
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt
index 1ccc45b..e3ca7c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt
@@ -156,12 +156,12 @@
         )
     }
 
-    fun logAutoRemoveCanceled(entry: NotificationEntry, reason: String?) {
+    fun logAutoRemoveCanceled(entry: NotificationEntry?, reason: String?) {
         buffer.log(
             TAG,
             INFO,
             {
-                str1 = entry.logKey
+                str1 = entry?.logKey
                 str2 = reason ?: "unknown"
             },
             { "cancel auto remove of $str1 reason: $str2" },
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt
index fc432ba..839028e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt
@@ -16,62 +16,9 @@
 
 package com.android.systemui.statusbar.notification.icon.ui.viewbinder
 
-import androidx.lifecycle.lifecycleScope
-import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.app.tracing.traceSection
-import com.android.systemui.common.ui.ConfigurationState
-import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
-import com.android.systemui.statusbar.phone.NotificationIconContainer
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.statusbar.ui.SystemBarUtilsState
 import javax.inject.Inject
-import kotlinx.coroutines.DisposableHandle
-
-/** Binds a [NotificationIconContainer] to a [NotificationIconContainerAlwaysOnDisplayViewModel]. */
-class NotificationIconContainerAlwaysOnDisplayViewBinder
-@Inject
-constructor(
-    private val viewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
-    private val keyguardRootViewModel: KeyguardRootViewModel,
-    @ShadeDisplayAware private val configuration: ConfigurationState,
-    private val failureTracker: StatusBarIconViewBindingFailureTracker,
-    private val screenOffAnimationController: ScreenOffAnimationController,
-    private val systemBarUtilsState: SystemBarUtilsState,
-    private val viewStore: AlwaysOnDisplayNotificationIconViewStore,
-) {
-    fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle {
-        return traceSection("NICAlwaysOnDisplay#bindWhileAttached") {
-            view.repeatWhenAttached {
-                lifecycleScope.launch {
-                    launch {
-                        NotificationIconContainerViewBinder.bind(
-                            view = view,
-                            viewModel = viewModel,
-                            configuration = configuration,
-                            systemBarUtilsState = systemBarUtilsState,
-                            failureTracker = failureTracker,
-                            viewStore = viewStore,
-                        )
-                    }
-                    launch {
-                        KeyguardRootViewBinder.bindAodNotifIconVisibility(
-                            view = view,
-                            isVisible = keyguardRootViewModel.isNotifIconContainerVisible,
-                            configuration = configuration,
-                            screenOffAnimationController = screenOffAnimationController,
-                        )
-                    }
-                }
-            }
-        }
-    }
-}
 
 /** [IconViewStore] for the always-on display. */
 class AlwaysOnDisplayNotificationIconViewStore
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
index e122ca8..863c665 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
@@ -71,6 +71,7 @@
         contentBuilder.appName = notification.loadHeaderAppName(context)
         contentBuilder.subText = notification.subText()
         contentBuilder.time = notification.extractWhen()
+        contentBuilder.shortCriticalText = notification.shortCriticalText()
         contentBuilder.lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs
         contentBuilder.profileBadgeResId = null // TODO
         contentBuilder.title = notification.title()
@@ -97,6 +98,13 @@
 
 private fun Notification.subText(): String? = extras?.getString(EXTRA_SUB_TEXT)
 
+private fun Notification.shortCriticalText(): String? {
+    if (!android.app.Flags.apiRichOngoing()) {
+        return null
+    }
+    return this.shortCriticalText
+}
+
 private fun Notification.chronometerCountDown(): Boolean =
     extras?.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, /* defaultValue= */ false) ?: false
 
@@ -107,7 +115,7 @@
     val countDown = chronometerCountDown()
 
     return when {
-        showsTime -> When(time, When.Mode.Absolute)
+        showsTime -> When(time, When.Mode.BasicTime)
         showsChronometer -> When(time, if (countDown) When.Mode.CountDown else When.Mode.CountUp)
         else -> null
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
index 0af4043..fe2dabe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
@@ -34,6 +34,11 @@
     val skeletonSmallIcon: Icon?, // TODO(b/377568176): Make into an IconModel.
     val appName: CharSequence?,
     val subText: CharSequence?,
+    val shortCriticalText: String?,
+    /**
+     * The timestamp associated with the notification. Null if the timestamp should not be
+     * displayed.
+     */
     val time: When?,
     val lastAudiblyAlertedMs: Long,
     @DrawableRes val profileBadgeResId: Int?,
@@ -57,6 +62,7 @@
         var appName: CharSequence? = null
         var subText: CharSequence? = null
         var time: When? = null
+        var shortCriticalText: String? = null
         var lastAudiblyAlertedMs: Long = 0L
         @DrawableRes var profileBadgeResId: Int? = null
         var title: CharSequence? = null
@@ -80,6 +86,7 @@
                 skeletonSmallIcon = skeletonSmallIcon,
                 appName = appName,
                 subText = subText,
+                shortCriticalText = shortCriticalText,
                 time = time,
                 lastAudiblyAlertedMs = lastAudiblyAlertedMs,
                 profileBadgeResId = profileBadgeResId,
@@ -100,8 +107,11 @@
     data class When(val time: Long, val mode: Mode) {
         /** The mode used to display a notification's `when` value. */
         enum class Mode {
-            Absolute,
+            /** No custom mode requested by the notification. */
+            BasicTime,
+            /** Show the notification's time as a chronometer that counts down to [time]. */
             CountDown,
+            /** Show the notification's time as a chronometer that counts up from [time]. */
             CountUp,
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index f8f29ff..b81c71e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -563,7 +563,7 @@
             lockscreenToGoneTransitionViewModel.notificationAlpha(viewState),
             lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
             lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
-            alternateBouncerToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
+            alternateBouncerToPrimaryBouncerTransitionViewModel.notificationAlpha,
             occludedToAodTransitionViewModel.lockscreenAlpha,
             occludedToGoneTransitionViewModel.notificationAlpha(viewState),
             occludedToLockscreenTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt
index fe5a02b..153dd99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt
@@ -67,6 +67,7 @@
         broadcastDispatcher,
         dialogTransitionAnimator,
         delegate,
+        true, /* shouldAcsdDismissDialog */
     ),
     LifecycleOwner,
     SavedStateRegistryOwner,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
index 858cac1..9c7af18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
@@ -65,6 +65,13 @@
         listeners.filterForEach({ this.listeners.contains(it) }) { it.onThemeChanged() }
     }
 
+    override fun dispatchOnMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration) {
+        val listeners = synchronized(this.listeners) { ArrayList(this.listeners) }
+        listeners.filterForEach({ this.listeners.contains(it) }) {
+            it.onMovedToDisplay(newDisplayId, newConfiguration)
+        }
+    }
+
     override fun onConfigurationChanged(newConfig: Configuration) {
         // Avoid concurrent modification exception
         val listeners = synchronized(this.listeners) { ArrayList(this.listeners) }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt
index 3fd46fc..537e3e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt
@@ -28,4 +28,13 @@
 interface ConfigurationForwarder {
     /** Should be called when a new configuration is received. */
     fun onConfigurationChanged(newConfiguration: Configuration)
+
+    /**
+     * Should be called when the view associated to this configuration forwarded moved to another
+     * display, usually as a consequence of [View.onMovedToDisplay].
+     *
+     * For the default configuration forwarder (associated with the global configuration) this is
+     * never expected to be called.
+     */
+    fun dispatchOnMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
index eadb7f5..2368824 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
@@ -22,14 +22,7 @@
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.annotation.StringRes
-import com.android.keyguard.LockIconViewController
-import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder
-import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
-import com.android.systemui.statusbar.VibratorHelper
 
 /**
  * Renders the bottom area of the lock-screen. Concerned primarily with the quick affordance UI
@@ -51,124 +44,7 @@
         defStyleAttr,
         defStyleRes,
     ) {
-
-    @Deprecated("Deprecated as part of b/278057014")
-    interface MessageDisplayer {
-        fun display(@StringRes stringResourceId: Int)
-    }
-
-    private var ambientIndicationArea: View? = null
-    private var keyguardIndicationArea: View? = null
-    private var binding: KeyguardBottomAreaViewBinder.Binding? = null
-    private var lockIconViewController: LockIconViewController? = null
-    private var isLockscreenLandscapeEnabled: Boolean = false
-
-    /** Initializes the view. */
-    @Deprecated("Deprecated as part of b/278057014")
-    fun init(
-        viewModel: KeyguardBottomAreaViewModel,
-        falsingManager: FalsingManager? = null,
-        lockIconViewController: LockIconViewController? = null,
-        messageDisplayer: MessageDisplayer? = null,
-        vibratorHelper: VibratorHelper? = null,
-        activityStarter: ActivityStarter? = null,
-    ) {
-        binding?.destroy()
-        binding =
-            bind(
-                this,
-                viewModel,
-                falsingManager,
-                vibratorHelper,
-                activityStarter,
-            ) {
-                messageDisplayer?.display(it)
-            }
-        this.lockIconViewController = lockIconViewController
-    }
-
-    /**
-     * Initializes this instance of [KeyguardBottomAreaView] based on the given instance of another
-     * [KeyguardBottomAreaView]
-     */
-    @Deprecated("Deprecated as part of b/278057014")
-    fun initFrom(oldBottomArea: KeyguardBottomAreaView) {
-        // if it exists, continue to use the original ambient indication container
-        // instead of the newly inflated one
-        ambientIndicationArea?.let { nonNullAmbientIndicationArea ->
-            // remove old ambient indication from its parent
-            val originalAmbientIndicationView =
-                oldBottomArea.requireViewById<View>(R.id.ambient_indication_container)
-            (originalAmbientIndicationView.parent as ViewGroup).removeView(
-                originalAmbientIndicationView
-            )
-
-            // remove current ambient indication from its parent (discard)
-            val ambientIndicationParent = nonNullAmbientIndicationArea.parent as ViewGroup
-            val ambientIndicationIndex =
-                ambientIndicationParent.indexOfChild(nonNullAmbientIndicationArea)
-            ambientIndicationParent.removeView(nonNullAmbientIndicationArea)
-
-            // add the old ambient indication to this view
-            ambientIndicationParent.addView(originalAmbientIndicationView, ambientIndicationIndex)
-            ambientIndicationArea = originalAmbientIndicationView
-        }
-    }
-
-    fun setIsLockscreenLandscapeEnabled(isLockscreenLandscapeEnabled: Boolean) {
-        this.isLockscreenLandscapeEnabled = isLockscreenLandscapeEnabled
-    }
-
-    override fun onFinishInflate() {
-        super.onFinishInflate()
-        ambientIndicationArea = findViewById(R.id.ambient_indication_container)
-        keyguardIndicationArea = findViewById(R.id.keyguard_indication_area)
-    }
-
-    override fun onConfigurationChanged(newConfig: Configuration) {
-        super.onConfigurationChanged(newConfig)
-        binding?.onConfigurationChanged()
-
-        if (isLockscreenLandscapeEnabled) {
-            updateIndicationAreaBottomMargin()
-        }
-    }
-
-    private fun updateIndicationAreaBottomMargin() {
-        keyguardIndicationArea?.let {
-            val params = it.layoutParams as FrameLayout.LayoutParams
-            params.bottomMargin =
-                resources.getDimensionPixelSize(R.dimen.keyguard_indication_margin_bottom)
-            it.layoutParams = params
-        }
-    }
-
     override fun hasOverlappingRendering(): Boolean {
         return false
     }
-
-    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
-        super.onLayout(changed, left, top, right, bottom)
-        findViewById<View>(R.id.ambient_indication_container)?.let {
-            val (ambientLeft, ambientTop) = it.locationOnScreen
-            if (binding?.shouldConstrainToTopOfLockIcon() == true) {
-                // make top of ambient indication view the bottom of the lock icon
-                it.layout(
-                    ambientLeft,
-                    lockIconViewController?.getBottom()?.toInt() ?: 0,
-                    right - ambientLeft,
-                    ambientTop + it.measuredHeight
-                )
-            } else {
-                // make bottom of ambient indication view the top of the lock icon
-                val lockLocationTop = lockIconViewController?.getTop() ?: 0
-                it.layout(
-                    ambientLeft,
-                    lockLocationTop.toInt() - it.measuredHeight,
-                    right - ambientLeft,
-                    lockLocationTop.toInt()
-                )
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt
deleted file mode 100644
index 4aece3d..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone
-
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
-import com.android.systemui.Flags.smartspaceRelocateToBottom
-import android.view.View
-import android.view.ViewGroup
-import android.widget.LinearLayout
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
-import com.android.systemui.util.ViewController
-import javax.inject.Inject
-
-class KeyguardBottomAreaViewController
-    @Inject constructor(
-            view: KeyguardBottomAreaView,
-            private val smartspaceController: LockscreenSmartspaceController,
-            featureFlags: FeatureFlagsClassic
-) : ViewController<KeyguardBottomAreaView> (view) {
-
-    private var smartspaceView: View? = null
-
-    init {
-        view.setIsLockscreenLandscapeEnabled(
-                featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE))
-    }
-
-    override fun onViewAttached() {
-        if (!smartspaceRelocateToBottom() || !smartspaceController.isEnabled) {
-            return
-        }
-
-        val ambientIndicationArea = mView.findViewById<View>(R.id.ambient_indication_container)
-        ambientIndicationArea?.visibility = View.GONE
-
-        addSmartspaceView()
-    }
-
-    override fun onViewDetached() {
-    }
-
-    fun getView(): KeyguardBottomAreaView {
-        // TODO: remove this method.
-        return mView
-    }
-
-    private fun addSmartspaceView() {
-        if (!smartspaceRelocateToBottom()) {
-            return
-        }
-
-        val smartspaceContainer = mView.findViewById<View>(R.id.smartspace_container)
-        smartspaceContainer!!.visibility = View.VISIBLE
-
-        smartspaceView = smartspaceController.buildAndConnectView(smartspaceContainer as ViewGroup)
-        val lp = LinearLayout.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
-        (smartspaceContainer as ViewGroup).addView(smartspaceView, 0, lp)
-        val startPadding = context.resources.getDimensionPixelSize(
-                R.dimen.below_clock_padding_start)
-        val endPadding = context.resources.getDimensionPixelSize(
-                R.dimen.below_clock_padding_end)
-        smartspaceView?.setPaddingRelative(startPadding, 0, endPadding, 0)
-//        mKeyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 12684fa..0fac644 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -316,7 +316,8 @@
                         .isLockscreenPublicMode(mLockscreenUserManager.getCurrentUserId());
                 boolean userPublic = devicePublic
                         || mLockscreenUserManager.isLockscreenPublicMode(sbn.getUserId());
-                boolean needsRedaction = mLockscreenUserManager.needsRedaction(entry);
+                boolean needsRedaction = mLockscreenUserManager.getRedactionType(entry)
+                        != NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
                 if (userPublic && needsRedaction) {
                     // TODO(b/135046837): we can probably relax this with dynamic privacy
                     return true;
@@ -369,7 +370,8 @@
                         return false;
                     }
 
-                    if (!mLockscreenUserManager.needsRedaction(entry)) {
+                    if (mLockscreenUserManager.getRedactionType(entry)
+                            == NotificationLockscreenUserManager.REDACTION_TYPE_NONE) {
                         return false;
                     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 0ad1042a..03324d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -145,7 +145,7 @@
          */
         public SystemUIDialog create() {
             return create(new DialogDelegate<>() {
-            }, mContext, DEFAULT_THEME);
+            }, mContext, DEFAULT_THEME, true /* shouldAcsdDismissDialog */);
         }
 
         /**
@@ -155,7 +155,7 @@
          */
         public SystemUIDialog create(Context context) {
             return create(new DialogDelegate<>() {
-            }, context, DEFAULT_THEME);
+            }, context, DEFAULT_THEME, true /* shouldAcsdDismissDialog */);
         }
 
         /**
@@ -168,8 +168,21 @@
             return create(delegate, context, DEFAULT_THEME);
         }
 
+        /**
+         * Creates a new instance of {@link SystemUIDialog} with {@code delegate} as the {@link
+         * Delegate}. When you need to customize the dialog, pass it a delegate.
+         *
+         * This method allows the caller to specify if the dialog should be dismissed in response
+         * to the ACTION_CLOSE_SYSTEM_DIALOGS intent.
+         */
+        public SystemUIDialog create(Delegate delegate, Context context,
+                boolean shouldAcsdDismissDialog) {
+            return create(delegate, context, DEFAULT_THEME, shouldAcsdDismissDialog);
+        }
+
         public SystemUIDialog create(Delegate delegate, Context context, @StyleRes int theme) {
-            return create((DialogDelegate<SystemUIDialog>) delegate, context, theme);
+            return create((DialogDelegate<SystemUIDialog>) delegate, context, theme,
+                    true /* shouldAcsdDismissDialog */);
         }
 
         public SystemUIDialog create(Delegate delegate) {
@@ -177,7 +190,7 @@
         }
 
         private SystemUIDialog create(DialogDelegate<SystemUIDialog> dialogDelegate,
-                Context context, @StyleRes int theme) {
+                Context context, @StyleRes int theme, boolean shouldAcsdDismissDialog) {
             return new SystemUIDialog(
                     context,
                     theme,
@@ -186,7 +199,8 @@
                     mSysUiState,
                     mBroadcastDispatcher,
                     mDialogTransitionAnimator,
-                    dialogDelegate);
+                    dialogDelegate,
+                    shouldAcsdDismissDialog);
         }
     }
 
@@ -207,7 +221,8 @@
                 broadcastDispatcher,
                 dialogTransitionAnimator,
                 new DialogDelegate<>() {
-                });
+                },
+                true /* shouldAcsdDismissDialog */);
     }
 
     public SystemUIDialog(
@@ -227,7 +242,8 @@
                 sysUiState,
                 broadcastDispatcher,
                 dialogTransitionAnimator,
-                (DialogDelegate<SystemUIDialog>) delegate);
+                (DialogDelegate<SystemUIDialog>) delegate,
+                true /* shouldAcsdDismissDialog */);
     }
 
     public SystemUIDialog(
@@ -238,7 +254,8 @@
             SysUiState sysUiState,
             BroadcastDispatcher broadcastDispatcher,
             DialogTransitionAnimator dialogTransitionAnimator,
-            DialogDelegate<SystemUIDialog> delegate) {
+            DialogDelegate<SystemUIDialog> delegate,
+            boolean shouldAcsdDismissDialog) {
         super(context, theme);
         mContext = context;
         mDelegate = delegate;
@@ -249,7 +266,7 @@
         getWindow().setAttributes(attrs);
 
         mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this, broadcastDispatcher,
-                dialogTransitionAnimator) : null;
+                dialogTransitionAnimator, shouldAcsdDismissDialog) : null;
         mDialogManager = dialogManager;
         mSysUiState = sysUiState;
     }
@@ -523,7 +540,8 @@
         // TODO(b/219008720): Remove those calls to Dependency.get.
         DismissReceiver dismissReceiver = new DismissReceiver(dialog,
                 Dependency.get(BroadcastDispatcher.class),
-                Dependency.get(DialogTransitionAnimator.class));
+                Dependency.get(DialogTransitionAnimator.class),
+                true /* shouldAcsdDismissDialog */);
         dialog.setOnDismissListener(d -> {
             dismissReceiver.unregister();
             if (dismissAction != null) dismissAction.run();
@@ -595,12 +613,7 @@
     }
 
     private static class DismissReceiver extends BroadcastReceiver {
-        private static final IntentFilter INTENT_FILTER = new IntentFilter();
-
-        static {
-            INTENT_FILTER.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
-            INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF);
-        }
+        private final IntentFilter mIntentFilter = new IntentFilter();
 
         private final Dialog mDialog;
         private boolean mRegistered;
@@ -608,14 +621,20 @@
         private final DialogTransitionAnimator mDialogTransitionAnimator;
 
         DismissReceiver(Dialog dialog, BroadcastDispatcher broadcastDispatcher,
-                DialogTransitionAnimator dialogTransitionAnimator) {
+                DialogTransitionAnimator dialogTransitionAnimator,
+                boolean shouldAcsdDismissDialog) {
             mDialog = dialog;
             mBroadcastDispatcher = broadcastDispatcher;
             mDialogTransitionAnimator = dialogTransitionAnimator;
+
+            mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+            if (shouldAcsdDismissDialog) {
+                mIntentFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+            }
         }
 
         void register() {
-            mBroadcastDispatcher.registerReceiver(this, INTENT_FILTER, null, UserHandle.CURRENT);
+            mBroadcastDispatcher.registerReceiver(this, mIntentFilter, null, UserHandle.CURRENT);
             mRegistered = true;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt
index b7ccfa0..2f7b243 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt
@@ -16,12 +16,15 @@
 
 package com.android.systemui.statusbar.phone.ongoingcall.domain.interactor
 
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.CoreStartable
 import com.android.systemui.activity.data.repository.ActivityManagerRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
+import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
@@ -31,11 +34,15 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 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.filterIsInstance
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
@@ -55,12 +62,19 @@
     private val activityManagerRepository: ActivityManagerRepository,
     private val statusBarModeRepositoryStore: StatusBarModeRepositoryStore,
     private val statusBarWindowControllerStore: StatusBarWindowControllerStore,
+    private val swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler,
     activeNotificationsInteractor: ActiveNotificationsInteractor,
     @OngoingCallLog private val logBuffer: LogBuffer,
-) {
+) : CoreStartable {
     private val logger = Logger(logBuffer, TAG)
 
     /**
+     * Tracks whether the call chip has been swiped away.
+     */
+    private val _isChipSwipedAway = MutableStateFlow(false)
+    val isChipSwipedAway: StateFlow<Boolean> = _isChipSwipedAway.asStateFlow()
+
+    /**
      * The current state of ongoing calls.
      */
     val ongoingCallState: StateFlow<OngoingCallModel> =
@@ -70,15 +84,29 @@
                     notification = notification
                 )
             }
-            .onEach { state ->
-                setStatusBarRequiredForOngoingCall(state)
-            }
             .stateIn(
                 scope = scope,
                 started = SharingStarted.WhileSubscribed(),
                 initialValue = OngoingCallModel.NoCall
             )
 
+    @VisibleForTesting
+    val isStatusBarRequiredForOngoingCall = combine(
+        ongoingCallState,
+        isChipSwipedAway
+    ) { callState, chipSwipedAway ->
+        callState is OngoingCallModel.InCall && !chipSwipedAway
+    }
+
+    @VisibleForTesting
+    val isGestureListeningEnabled = combine(
+        ongoingCallState,
+        statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode,
+        isChipSwipedAway
+    ) { callState, isFullscreen, chipSwipedAway ->
+        callState is OngoingCallModel.InCall && !chipSwipedAway && isFullscreen
+    }
+
     private fun createOngoingCallStateFlow(
         notification: ActiveNotificationModel?
     ): Flow<OngoingCallModel> {
@@ -99,6 +127,31 @@
         }
     }
 
+    override fun start() {
+        ongoingCallState
+            .filterIsInstance<OngoingCallModel.NoCall>()
+            .onEach {
+                _isChipSwipedAway.value = false
+            }.launchIn(scope)
+
+        isStatusBarRequiredForOngoingCall.onEach { statusBarRequired ->
+            setStatusBarRequiredForOngoingCall(statusBarRequired)
+        }.launchIn(scope)
+
+        isGestureListeningEnabled.onEach { isEnabled ->
+            updateGestureListening(isEnabled)
+        }.launchIn(scope)
+    }
+
+    /**
+     * Callback that must run when the status bar is swiped while gesture listening is active.
+     */
+    @VisibleForTesting
+    fun onStatusBarSwiped() {
+        logger.d("Status bar chip swiped away")
+        _isChipSwipedAway.value = true
+    }
+
     private fun deriveOngoingCallState(
         model: ActiveNotificationModel,
         isVisible: Boolean
@@ -126,8 +179,7 @@
         }
     }
 
-    private fun setStatusBarRequiredForOngoingCall(state: OngoingCallModel) {
-        val statusBarRequired = state is OngoingCallModel.InCall
+    private fun setStatusBarRequiredForOngoingCall(statusBarRequired: Boolean) {
         // TODO(b/382808183): Create a single repository that can be utilized in
         //  `statusBarModeRepositoryStore` and `statusBarWindowControllerStore` so we do not need
         //  two separate calls to force the status bar to stay visible.
@@ -138,6 +190,16 @@
             .setOngoingProcessRequiresStatusBarVisible(statusBarRequired)
     }
 
+    private fun updateGestureListening(isEnabled: Boolean) {
+        if (isEnabled) {
+            swipeStatusBarAwayGestureHandler.addOnGestureDetectedCallback(TAG) { _ ->
+                onStatusBarSwiped()
+            }
+        } else {
+            swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
+        }
+    }
+
     companion object {
         private val TAG = "OngoingCall"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index a36ef56..f11ebc0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -220,7 +220,7 @@
 
         if (satelliteManager != null) {
             // Outer scope launch allows us to delay until MIN_UPTIME
-            scope.launch {
+            scope.launch(context = bgDispatcher) {
                 // First, check that satellite is supported on this device
                 satelliteSupport.value = checkSatelliteSupportAfterMinUptime(satelliteManager)
                 logBuffer.i(
@@ -229,7 +229,9 @@
                 )
 
                 // Second, register a listener to let us know if there are changes to support
-                scope.launch { listenForChangesToSatelliteSupport(satelliteManager) }
+                scope.launch(context = bgDispatcher) {
+                    listenForChangesToSatelliteSupport(satelliteManager)
+                }
             }
         } else {
             logBuffer.i { "Satellite manager is null" }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
index 1bb4e8c..c77f6c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
@@ -45,5 +45,6 @@
         default void onLocaleListChanged() {}
         default void onLayoutDirectionChanged(boolean isLayoutRtl) {}
         default void onOrientationChanged(int orientation) {}
+        default void onMovedToDisplay(int newDisplayId, Configuration newConfiguration) {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index bfdae62..e1cc11a 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -45,6 +45,8 @@
                     bodyResId = R.string.touchpad_back_gesture_guidance,
                     titleSuccessResId = R.string.touchpad_back_gesture_success_title,
                     bodySuccessResId = R.string.touchpad_back_gesture_success_body,
+                    titleErrorResId = R.string.gesture_error_title,
+                    bodyErrorResId = R.string.touchpad_back_gesture_error_body,
                 ),
             animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_back_edu),
         )
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
index 2332c00..ed84f9c 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
@@ -54,6 +54,8 @@
         val progressStartMarker: String,
         val progressEndMarker: String,
     ) : GestureUiState
+
+    data object Error : GestureUiState
 }
 
 fun GestureState.toGestureUiState(
@@ -66,19 +68,31 @@
         is GestureState.InProgress ->
             GestureUiState.InProgress(this.progress, progressStartMarker, progressEndMarker)
         is GestureState.Finished -> GestureUiState.Finished(successAnimation)
+        GestureState.Error -> GestureUiState.Error
     }
 }
 
-fun GestureUiState.toTutorialActionState(): TutorialActionState {
+fun GestureUiState.toTutorialActionState(previousState: TutorialActionState): TutorialActionState {
     return when (this) {
         NotStarted -> TutorialActionState.NotStarted
-        is GestureUiState.InProgress ->
-            TutorialActionState.InProgress(
-                progress = progress,
-                startMarker = progressStartMarker,
-                endMarker = progressEndMarker,
-            )
+        is GestureUiState.InProgress -> {
+            val inProgress =
+                TutorialActionState.InProgress(
+                    progress = progress,
+                    startMarker = progressStartMarker,
+                    endMarker = progressEndMarker,
+                )
+            if (
+                previousState is TutorialActionState.InProgressAfterError ||
+                    previousState is TutorialActionState.Error
+            ) {
+                return TutorialActionState.InProgressAfterError(inProgress)
+            } else {
+                return inProgress
+            }
+        }
         is Finished -> TutorialActionState.Finished(successAnimation)
+        GestureUiState.Error -> TutorialActionState.Error
     }
 }
 
@@ -102,11 +116,11 @@
         easterEggTriggered,
         resetEasterEggFlag = { easterEggTriggered = false },
     ) {
-        ActionTutorialContent(
-            gestureState.toTutorialActionState(),
-            onDoneButtonClicked,
-            screenConfig,
-        )
+        var lastState: TutorialActionState by remember {
+            mutableStateOf(TutorialActionState.NotStarted)
+        }
+        lastState = gestureState.toTutorialActionState(lastState)
+        ActionTutorialContent(lastState, onDoneButtonClicked, screenConfig)
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index 45fdd21..26604ca 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -42,6 +42,8 @@
                     bodyResId = R.string.touchpad_home_gesture_guidance,
                     titleSuccessResId = R.string.touchpad_home_gesture_success_title,
                     bodySuccessResId = R.string.touchpad_home_gesture_success_body,
+                    titleErrorResId = R.string.gesture_error_title,
+                    bodyErrorResId = R.string.touchpad_home_gesture_error_body,
                 ),
             animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_home_edu),
         )
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
index 680b670..6400aca 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
@@ -42,6 +42,8 @@
                     bodyResId = R.string.touchpad_recent_apps_gesture_guidance,
                     titleSuccessResId = R.string.touchpad_recent_apps_gesture_success_title,
                     bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body,
+                    titleErrorResId = R.string.gesture_error_title,
+                    bodyErrorResId = R.string.touchpad_recent_gesture_error_body,
                 ),
             animations =
                 TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_recent_apps_edu),
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
index f27ddb5..7bc7e81 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
@@ -23,6 +23,8 @@
 
     data class InProgress(val progress: Float = 0f, val direction: GestureDirection? = null) :
         GestureState
+
+    data object Error : GestureState
 }
 
 enum class GestureDirection {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
index 24f5d1f..69886e4 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
@@ -28,7 +28,7 @@
             if (isFinished(gestureState)) {
                 gestureStateChangedCallback(GestureState.Finished)
             } else {
-                gestureStateChangedCallback(GestureState.NotStarted)
+                gestureStateChangedCallback(GestureState.Error)
             }
         }
         is Moving -> {
diff --git a/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt b/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt
index ca0876c..27ada23 100644
--- a/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt
@@ -22,5 +22,5 @@
 
 @Module
 interface EventLogModule {
-    @SysUISingleton @Binds fun bindEventLog(eventLogImpl: EventLogImpl?): EventLog?
+    @SysUISingleton @Binds fun bindEventLog(eventLogImpl: EventLogImpl): EventLog
 }
diff --git a/packages/SystemUI/src/com/android/systemui/window/flag/WindowBlurFlag.kt b/packages/SystemUI/src/com/android/systemui/window/flag/WindowBlurFlag.kt
new file mode 100644
index 0000000..8b6c860
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/window/flag/WindowBlurFlag.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.window.flag
+
+import com.android.systemui.Flags
+
+/**
+ * Flag that controls whether the background surface is blurred or not while on the
+ * lockscreen/shade/bouncer. This makes the background of scrim, bouncer and few other opaque
+ * surfaces transparent so that we can see the blur effect on the background surface (wallpaper).
+ */
+object WindowBlurFlag {
+    /** Whether the blur is enabled or not */
+    @JvmStatic
+    val isEnabled
+        // Add flags here that require scrims/background surfaces to be transparent.
+        get() = Flags.notificationShadeBlur() || Flags.bouncerUiRevamp()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
index f5616d4..7fb74b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
@@ -51,11 +51,11 @@
 
     private companion object {
         val TEST_COMPONENT = ComponentName("TestPackageName", "TestClassName")
+        val TEST_STRUCTURE: CharSequence = "TestStructure"
         val TEST_CONTROL =
             mock(Control::class.java, Answers.RETURNS_MOCKS)!!.apply {
                 whenever(structure).thenReturn(TEST_STRUCTURE)
             }
-        val TEST_STRUCTURE: CharSequence = "TestStructure"
         val TEST_APP: CharSequence = "TestApp"
 
         private fun View.waitForPost() {
@@ -158,7 +158,7 @@
                 assertThat(getBooleanExtra(ControlsEditingActivity.EXTRA_FROM_FAVORITING, false))
                     .isTrue()
                 assertThat(getCharSequenceExtra(ControlsEditingActivity.EXTRA_STRUCTURE))
-                    .isEqualTo("")
+                    .isEqualTo(TEST_STRUCTURE)
             }
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index 24bca70..fb70846 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -41,6 +41,7 @@
 import android.os.UserManager;
 import android.provider.Settings;
 import android.testing.TestableLooper;
+import android.view.Display;
 import android.view.GestureDetector;
 import android.view.IWindowManager;
 import android.view.KeyEvent;
@@ -63,6 +64,7 @@
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.display.data.repository.FakeDisplayWindowPropertiesRepository;
 import com.android.systemui.globalactions.domain.interactor.GlobalActionsInteractor;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.plugins.ActivityStarter;
@@ -159,6 +161,8 @@
                 getContext().getResources().getConfiguration());
         when(mStatusBarWindowControllerStore.getDefaultDisplay())
                 .thenReturn(mStatusBarWindowController);
+        when(mStatusBarWindowControllerStore.forDisplay(anyInt()))
+                .thenReturn(mStatusBarWindowController);
         mGlobalSettings = new FakeGlobalSettings();
         mSecureSettings = new FakeSettings();
         mInteractor = mKosmos.getGlobalActionsInteractor();
@@ -198,7 +202,9 @@
                 mDialogTransitionAnimator,
                 mSelectedUserInteractor,
                 mLogoutInteractor,
-                mInteractor);
+                mInteractor,
+                () -> new FakeDisplayWindowPropertiesRepository(mContext)
+        );
         mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting();
 
         ColorExtractor.GradientColors backdropColors = new ColorExtractor.GradientColors();
@@ -609,13 +615,15 @@
 
         // When entering power menu from lockscreen, with smart lock enabled
         when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
-        mGlobalActionsDialogLite.showOrHideDialog(true, true, null /* view */);
+        mGlobalActionsDialogLite.showOrHideDialog(true, true, null /* view */,
+                Display.DEFAULT_DISPLAY);
 
         // Then smart lock will be disabled
         verify(mLockPatternUtils).requireCredentialEntry(eq(expectedUser));
 
         // hide dialog again
-        mGlobalActionsDialogLite.showOrHideDialog(true, true, null /* view */);
+        mGlobalActionsDialogLite.showOrHideDialog(true, true, null /* view */,
+                Display.DEFAULT_DISPLAY);
     }
 
     @Test
@@ -729,13 +737,13 @@
         doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
 
         // Show dialog with keyguard showing
-        mGlobalActionsDialogLite.showOrHideDialog(true, true, null);
+        mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY);
 
         assertOneItemOfType(mGlobalActionsDialogLite.mItems,
                 GlobalActionsDialogLite.SystemUpdateAction.class);
 
         // Hide dialog
-        mGlobalActionsDialogLite.showOrHideDialog(true, true, null);
+        mGlobalActionsDialogLite.showOrHideDialog(true, true, null, Display.DEFAULT_DISPLAY);
     }
 
     @Test
@@ -754,13 +762,13 @@
         doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
 
         // Show dialog with keyguard showing
-        mGlobalActionsDialogLite.showOrHideDialog(false, false, null);
+        mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY);
 
         assertNoItemsOfType(mGlobalActionsDialogLite.mItems,
                 GlobalActionsDialogLite.SystemUpdateAction.class);
 
         // Hide dialog
-        mGlobalActionsDialogLite.showOrHideDialog(false, false, null);
+        mGlobalActionsDialogLite.showOrHideDialog(false, false, null, Display.DEFAULT_DISPLAY);
     }
 
     private UserInfo mockCurrentUser(int flags) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
index cea51a8..222a7fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
@@ -23,7 +23,6 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.flags.FakeFeatureFlags
@@ -66,8 +65,6 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
-        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-
         featureFlags =
             FakeFeatureFlagsClassic().apply { set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) }
         underTest =
@@ -88,16 +85,7 @@
     }
 
     @Test
-    fun addViewsConditionally_migrateFlagOn() {
-        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-        val constraintLayout = ConstraintLayout(context, null)
-        underTest.addViews(constraintLayout)
-        assertThat(constraintLayout.childCount).isGreaterThan(0)
-    }
-
-    @Test
-    fun addViewsConditionally_migrateAndRefactorFlagsOn() {
-        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
+    fun addViewsConditionally() {
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
         assertThat(constraintLayout.childCount).isGreaterThan(0)
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
deleted file mode 100644
index 7e85dd5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ /dev/null
@@ -1,771 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import android.app.admin.DevicePolicyManager
-import android.content.Intent
-import android.os.UserHandle
-import android.platform.test.flag.junit.FlagsParameterization
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.internal.widget.LockPatternUtils
-import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.animation.Expandable
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
-import com.android.systemui.dock.DockManagerFake
-import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.flags.andSceneContainer
-import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
-import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
-import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-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.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
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger
-import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.sceneInteractor
-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
-import com.android.systemui.testKosmos
-import com.android.systemui.util.FakeSharedPreferences
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.fakeSettings
-import com.google.common.truth.Truth.assertThat
-import kotlin.math.max
-import kotlin.math.min
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.MockitoAnnotations
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
-
-@SmallTest
-@RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
-
-    private val kosmos = testKosmos()
-    private val testDispatcher = kosmos.testDispatcher
-    private val testScope = kosmos.testScope
-    private val settings = kosmos.fakeSettings
-
-    @Mock private lateinit var expandable: Expandable
-    @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
-    @Mock private lateinit var lockPatternUtils: LockPatternUtils
-    @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var userTracker: UserTracker
-    @Mock private lateinit var activityStarter: ActivityStarter
-    @Mock private lateinit var launchAnimator: DialogTransitionAnimator
-    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
-    @Mock private lateinit var logger: KeyguardQuickAffordancesLogger
-    @Mock private lateinit var metricsLogger: KeyguardQuickAffordancesMetricsLogger
-    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock private lateinit var accessibilityManager: AccessibilityManagerWrapper
-
-    private lateinit var underTest: KeyguardBottomAreaViewModel
-
-    private lateinit var repository: FakeKeyguardRepository
-    private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig
-    private lateinit var quickAccessWalletAffordanceConfig: FakeKeyguardQuickAffordanceConfig
-    private lateinit var qrCodeScannerAffordanceConfig: FakeKeyguardQuickAffordanceConfig
-    private lateinit var dockManager: DockManagerFake
-    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
-
-    init {
-        mSetFlagsRule.setFlagsParameterization(flags)
-    }
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        overrideResource(R.bool.custom_lockscreen_shortcuts_enabled, true)
-        overrideResource(
-            R.array.config_keyguardQuickAffordanceDefaults,
-            arrayOf(
-                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START +
-                    ":" +
-                    BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
-                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END +
-                    ":" +
-                    BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
-            )
-        )
-
-        whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
-            .thenReturn(RETURNED_BURN_IN_OFFSET)
-
-        homeControlsQuickAffordanceConfig =
-            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
-        quickAccessWalletAffordanceConfig =
-            FakeKeyguardQuickAffordanceConfig(
-                BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
-            )
-        qrCodeScannerAffordanceConfig =
-            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
-        dockManager = DockManagerFake()
-        biometricSettingsRepository = FakeBiometricSettingsRepository()
-        val featureFlags =
-            FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) }
-
-        val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
-        val keyguardInteractor = withDeps.keyguardInteractor
-        repository = withDeps.repository
-
-        whenever(userTracker.userHandle).thenReturn(mock())
-        whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
-            .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
-        val localUserSelectionManager =
-            KeyguardQuickAffordanceLocalUserSelectionManager(
-                context = context,
-                userFileManager =
-                    mock<UserFileManager>().apply {
-                        whenever(
-                                getSharedPreferences(
-                                    anyString(),
-                                    anyInt(),
-                                    anyInt(),
-                                )
-                            )
-                            .thenReturn(FakeSharedPreferences())
-                    },
-                userTracker = userTracker,
-                broadcastDispatcher = fakeBroadcastDispatcher,
-            )
-        val remoteUserSelectionManager =
-            KeyguardQuickAffordanceRemoteUserSelectionManager(
-                scope = testScope.backgroundScope,
-                userTracker = userTracker,
-                clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
-                userHandle = UserHandle.SYSTEM,
-            )
-        val quickAffordanceRepository =
-            KeyguardQuickAffordanceRepository(
-                appContext = context,
-                scope = testScope.backgroundScope,
-                localUserSelectionManager = localUserSelectionManager,
-                remoteUserSelectionManager = remoteUserSelectionManager,
-                userTracker = userTracker,
-                legacySettingSyncer =
-                    KeyguardQuickAffordanceLegacySettingSyncer(
-                        scope = testScope.backgroundScope,
-                        backgroundDispatcher = testDispatcher,
-                        secureSettings = settings,
-                        selectionsManager = localUserSelectionManager,
-                    ),
-                configs =
-                    setOf(
-                        homeControlsQuickAffordanceConfig,
-                        quickAccessWalletAffordanceConfig,
-                        qrCodeScannerAffordanceConfig,
-                    ),
-                dumpManager = mock(),
-                userHandle = UserHandle.SYSTEM,
-            )
-        val keyguardTouchHandlingInteractor =
-            KeyguardTouchHandlingInteractor(
-                context = mContext,
-                scope = testScope.backgroundScope,
-                transitionInteractor = kosmos.keyguardTransitionInteractor,
-                repository = repository,
-                logger = UiEventLoggerFake(),
-                featureFlags = featureFlags,
-                broadcastDispatcher = broadcastDispatcher,
-                accessibilityManager = accessibilityManager,
-                pulsingGestureListener = kosmos.pulsingGestureListener,
-                faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor,
-            )
-        underTest =
-            KeyguardBottomAreaViewModel(
-                keyguardInteractor = keyguardInteractor,
-                quickAffordanceInteractor =
-                    KeyguardQuickAffordanceInteractor(
-                        keyguardInteractor = keyguardInteractor,
-                        shadeInteractor = kosmos.shadeInteractor,
-                        lockPatternUtils = lockPatternUtils,
-                        keyguardStateController = keyguardStateController,
-                        userTracker = userTracker,
-                        activityStarter = activityStarter,
-                        featureFlags = featureFlags,
-                        repository = { quickAffordanceRepository },
-                        launchAnimator = launchAnimator,
-                        logger = logger,
-                        metricsLogger = metricsLogger,
-                        devicePolicyManager = devicePolicyManager,
-                        dockManager = dockManager,
-                        biometricSettingsRepository = biometricSettingsRepository,
-                        backgroundDispatcher = testDispatcher,
-                        appContext = mContext,
-                        sceneInteractor = { kosmos.sceneInteractor },
-                    ),
-                bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
-                burnInHelperWrapper = burnInHelperWrapper,
-                keyguardTouchHandlingViewModel =
-                    KeyguardTouchHandlingViewModel(
-                        interactor = keyguardTouchHandlingInteractor,
-                    ),
-                settingsMenuViewModel =
-                    KeyguardSettingsMenuViewModel(
-                        interactor = keyguardTouchHandlingInteractor,
-                    ),
-            )
-    }
-
-    @Test
-    fun startButton_present_visibleModel_startsActivityOnClick() =
-        testScope.runTest {
-            repository.setKeyguardShowing(true)
-            val latest = collectLastValue(underTest.startButton)
-
-            val testConfig =
-                TestConfig(
-                    isVisible = true,
-                    isClickable = true,
-                    isActivated = true,
-                    icon = mock(),
-                    canShowWhileLocked = false,
-                    intent = Intent("action"),
-                    slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                )
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                    testConfig = testConfig,
-                )
-
-            assertQuickAffordanceViewModel(
-                viewModel = latest(),
-                testConfig = testConfig,
-                configKey = configKey,
-            )
-        }
-
-    @Test
-    fun startButton_hiddenWhenDevicePolicyDisablesAllKeyguardFeatures() =
-        testScope.runTest {
-            whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
-                .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL)
-            repository.setKeyguardShowing(true)
-            val latest by collectLastValue(underTest.startButton)
-
-            val testConfig =
-                TestConfig(
-                    isVisible = true,
-                    isClickable = true,
-                    isActivated = true,
-                    icon = mock(),
-                    canShowWhileLocked = false,
-                    intent = Intent("action"),
-                    slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                )
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                    testConfig = testConfig,
-                )
-
-            assertQuickAffordanceViewModel(
-                viewModel = latest,
-                testConfig =
-                    TestConfig(
-                        isVisible = false,
-                        slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                    ),
-                configKey = configKey,
-            )
-        }
-
-    @Test
-    fun startButton_inPreviewMode_visibleEvenWhenKeyguardNotShowing() =
-        testScope.runTest {
-            underTest.enablePreviewMode(
-                initiallySelectedSlotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                shouldHighlightSelectedAffordance = true,
-            )
-            repository.setKeyguardShowing(false)
-            val latest = collectLastValue(underTest.startButton)
-
-            val icon: Icon = mock()
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                    testConfig =
-                        TestConfig(
-                            isVisible = true,
-                            isClickable = true,
-                            isActivated = true,
-                            icon = icon,
-                            canShowWhileLocked = false,
-                            intent = Intent("action"),
-                            slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                        ),
-                )
-
-            assertQuickAffordanceViewModel(
-                viewModel = latest(),
-                testConfig =
-                    TestConfig(
-                        isVisible = true,
-                        isClickable = false,
-                        isActivated = false,
-                        icon = icon,
-                        canShowWhileLocked = false,
-                        intent = Intent("action"),
-                        isSelected = true,
-                        slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                    ),
-                configKey = configKey,
-            )
-            assertThat(latest()?.isSelected).isTrue()
-        }
-
-    @Test
-    fun endButton_inHiglightedPreviewMode_dimmedWhenOtherIsSelected() =
-        testScope.runTest {
-            underTest.enablePreviewMode(
-                initiallySelectedSlotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                shouldHighlightSelectedAffordance = true,
-            )
-            repository.setKeyguardShowing(false)
-            val startButton = collectLastValue(underTest.startButton)
-            val endButton = collectLastValue(underTest.endButton)
-
-            val icon: Icon = mock()
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                testConfig =
-                    TestConfig(
-                        isVisible = true,
-                        isClickable = true,
-                        isActivated = true,
-                        icon = icon,
-                        canShowWhileLocked = false,
-                        intent = Intent("action"),
-                        slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                    ),
-            )
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_END,
-                    testConfig =
-                        TestConfig(
-                            isVisible = true,
-                            isClickable = true,
-                            isActivated = true,
-                            icon = icon,
-                            canShowWhileLocked = false,
-                            intent = Intent("action"),
-                            slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(),
-                        ),
-                )
-
-            assertQuickAffordanceViewModel(
-                viewModel = endButton(),
-                testConfig =
-                    TestConfig(
-                        isVisible = true,
-                        isClickable = false,
-                        isActivated = false,
-                        icon = icon,
-                        canShowWhileLocked = false,
-                        intent = Intent("action"),
-                        isDimmed = true,
-                        slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(),
-                    ),
-                configKey = configKey,
-            )
-        }
-
-    @Test
-    fun endButton_present_visibleModel_doNothingOnClick() =
-        testScope.runTest {
-            repository.setKeyguardShowing(true)
-            val latest = collectLastValue(underTest.endButton)
-
-            val config =
-                TestConfig(
-                    isVisible = true,
-                    isClickable = true,
-                    icon = mock(),
-                    canShowWhileLocked = false,
-                    intent =
-                        null, // This will cause it to tell the system that the click was handled.
-                    slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId(),
-                )
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_END,
-                    testConfig = config,
-                )
-
-            assertQuickAffordanceViewModel(
-                viewModel = latest(),
-                testConfig = config,
-                configKey = configKey,
-            )
-        }
-
-    @Test
-    fun startButton_notPresent_modelIsHidden() =
-        testScope.runTest {
-            val latest = collectLastValue(underTest.startButton)
-
-            val config =
-                TestConfig(
-                    isVisible = false,
-                    slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                )
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                    testConfig = config,
-                )
-
-            assertQuickAffordanceViewModel(
-                viewModel = latest(),
-                testConfig = config,
-                configKey = configKey,
-            )
-        }
-
-    @Test
-    fun animateButtonReveal() =
-        testScope.runTest {
-            repository.setKeyguardShowing(true)
-            val testConfig =
-                TestConfig(
-                    isVisible = true,
-                    isClickable = true,
-                    icon = mock(),
-                    canShowWhileLocked = false,
-                    intent = Intent("action"),
-                    slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                )
-
-            setUpQuickAffordanceModel(
-                position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                testConfig = testConfig,
-            )
-
-            val value = collectLastValue(underTest.startButton.map { it.animateReveal })
-
-            assertThat(value()).isFalse()
-            repository.setAnimateDozingTransitions(true)
-            assertThat(value()).isTrue()
-            repository.setAnimateDozingTransitions(false)
-            assertThat(value()).isFalse()
-        }
-
-    @Test
-    fun isOverlayContainerVisible() =
-        testScope.runTest {
-            val value = collectLastValue(underTest.isOverlayContainerVisible)
-
-            assertThat(value()).isTrue()
-            repository.setIsDozing(true)
-            assertThat(value()).isFalse()
-            repository.setIsDozing(false)
-            assertThat(value()).isTrue()
-        }
-
-    @Test
-    fun alpha() =
-        testScope.runTest {
-            val value = collectLastValue(underTest.alpha)
-
-            assertThat(value()).isEqualTo(1f)
-            repository.setBottomAreaAlpha(0.1f)
-            assertThat(value()).isEqualTo(0.1f)
-            repository.setBottomAreaAlpha(0.5f)
-            assertThat(value()).isEqualTo(0.5f)
-            repository.setBottomAreaAlpha(0.2f)
-            assertThat(value()).isEqualTo(0.2f)
-            repository.setBottomAreaAlpha(0f)
-            assertThat(value()).isEqualTo(0f)
-        }
-
-    @Test
-    fun alpha_inPreviewMode_doesNotChange() =
-        testScope.runTest {
-            underTest.enablePreviewMode(
-                initiallySelectedSlotId = null,
-                shouldHighlightSelectedAffordance = false,
-            )
-            val value = collectLastValue(underTest.alpha)
-
-            assertThat(value()).isEqualTo(1f)
-            repository.setBottomAreaAlpha(0.1f)
-            assertThat(value()).isEqualTo(1f)
-            repository.setBottomAreaAlpha(0.5f)
-            assertThat(value()).isEqualTo(1f)
-            repository.setBottomAreaAlpha(0.2f)
-            assertThat(value()).isEqualTo(1f)
-            repository.setBottomAreaAlpha(0f)
-            assertThat(value()).isEqualTo(1f)
-        }
-
-    @Test
-    fun isClickable_trueWhenAlphaAtThreshold() =
-        testScope.runTest {
-            repository.setKeyguardShowing(true)
-            repository.setBottomAreaAlpha(
-                KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD
-            )
-
-            val testConfig =
-                TestConfig(
-                    isVisible = true,
-                    isClickable = true,
-                    icon = mock(),
-                    canShowWhileLocked = false,
-                    intent = Intent("action"),
-                    slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                )
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                    testConfig = testConfig,
-                )
-
-            val latest = collectLastValue(underTest.startButton)
-
-            assertQuickAffordanceViewModel(
-                viewModel = latest(),
-                testConfig = testConfig,
-                configKey = configKey,
-            )
-        }
-
-    @Test
-    fun isClickable_trueWhenAlphaAboveThreshold() =
-        testScope.runTest {
-            repository.setKeyguardShowing(true)
-            val latest = collectLastValue(underTest.startButton)
-            repository.setBottomAreaAlpha(
-                min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f),
-            )
-
-            val testConfig =
-                TestConfig(
-                    isVisible = true,
-                    isClickable = true,
-                    icon = mock(),
-                    canShowWhileLocked = false,
-                    intent = Intent("action"),
-                    slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                )
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                    testConfig = testConfig,
-                )
-
-            assertQuickAffordanceViewModel(
-                viewModel = latest(),
-                testConfig = testConfig,
-                configKey = configKey,
-            )
-        }
-
-    @Test
-    fun isClickable_falseWhenAlphaBelowThreshold() =
-        testScope.runTest {
-            repository.setKeyguardShowing(true)
-            val latest = collectLastValue(underTest.startButton)
-            repository.setBottomAreaAlpha(
-                max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f),
-            )
-
-            val testConfig =
-                TestConfig(
-                    isVisible = true,
-                    isClickable = false,
-                    icon = mock(),
-                    canShowWhileLocked = false,
-                    intent = Intent("action"),
-                    slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                )
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                    testConfig = testConfig,
-                )
-
-            assertQuickAffordanceViewModel(
-                viewModel = latest(),
-                testConfig = testConfig,
-                configKey = configKey,
-            )
-        }
-
-    @Test
-    fun isClickable_falseWhenAlphaAtZero() =
-        testScope.runTest {
-            repository.setKeyguardShowing(true)
-            val latest = collectLastValue(underTest.startButton)
-            repository.setBottomAreaAlpha(0f)
-
-            val testConfig =
-                TestConfig(
-                    isVisible = true,
-                    isClickable = false,
-                    icon = mock(),
-                    canShowWhileLocked = false,
-                    intent = Intent("action"),
-                    slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId(),
-                )
-            val configKey =
-                setUpQuickAffordanceModel(
-                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
-                    testConfig = testConfig,
-                )
-
-            assertQuickAffordanceViewModel(
-                viewModel = latest(),
-                testConfig = testConfig,
-                configKey = configKey,
-            )
-        }
-
-    private suspend fun setUpQuickAffordanceModel(
-        position: KeyguardQuickAffordancePosition,
-        testConfig: TestConfig,
-    ): String {
-        val config =
-            when (position) {
-                KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig
-                KeyguardQuickAffordancePosition.BOTTOM_END -> quickAccessWalletAffordanceConfig
-            }
-
-        val lockScreenState =
-            if (testConfig.isVisible) {
-                if (testConfig.intent != null) {
-                    config.onTriggeredResult =
-                        KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
-                            intent = testConfig.intent,
-                            canShowWhileLocked = testConfig.canShowWhileLocked,
-                        )
-                }
-                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
-                    icon = testConfig.icon ?: error("Icon is unexpectedly null!"),
-                    activationState =
-                        when (testConfig.isActivated) {
-                            true -> ActivationState.Active
-                            false -> ActivationState.Inactive
-                        }
-                )
-            } else {
-                KeyguardQuickAffordanceConfig.LockScreenState.Hidden
-            }
-        config.setState(lockScreenState)
-
-        return "${position.toSlotId()}::${config.key}"
-    }
-
-    private fun assertQuickAffordanceViewModel(
-        viewModel: KeyguardQuickAffordanceViewModel?,
-        testConfig: TestConfig,
-        configKey: String,
-    ) {
-        checkNotNull(viewModel)
-        assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
-        assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
-        assertThat(viewModel.isActivated).isEqualTo(testConfig.isActivated)
-        assertThat(viewModel.isSelected).isEqualTo(testConfig.isSelected)
-        assertThat(viewModel.isDimmed).isEqualTo(testConfig.isDimmed)
-        assertThat(viewModel.slotId).isEqualTo(testConfig.slotId)
-        if (testConfig.isVisible) {
-            assertThat(viewModel.icon).isEqualTo(testConfig.icon)
-            viewModel.onClicked.invoke(
-                KeyguardQuickAffordanceViewModel.OnClickedParameters(
-                    configKey = configKey,
-                    expandable = expandable,
-                    slotId = viewModel.slotId,
-                )
-            )
-            if (testConfig.intent != null) {
-                assertThat(Mockito.mockingDetails(activityStarter).invocations).hasSize(1)
-            } else {
-                verifyNoMoreInteractions(activityStarter)
-            }
-        } else {
-            assertThat(viewModel.isVisible).isFalse()
-        }
-    }
-
-    private data class TestConfig(
-        val isVisible: Boolean,
-        val isClickable: Boolean = false,
-        val isActivated: Boolean = false,
-        val icon: Icon? = null,
-        val canShowWhileLocked: Boolean = false,
-        val intent: Intent? = null,
-        val isSelected: Boolean = false,
-        val isDimmed: Boolean = false,
-        val slotId: String = ""
-    ) {
-        init {
-            check(!isVisible || icon != null) { "Must supply non-null icon if visible!" }
-        }
-    }
-
-    companion object {
-        private const val DEFAULT_BURN_IN_OFFSET = 5
-        private const val RETURNED_BURN_IN_OFFSET = 3
-
-        @JvmStatic
-        @Parameters(name = "{0}")
-        fun getParams(): List<FlagsParameterization> {
-            return FlagsParameterization.allCombinationsOf().andSceneContainer()
-        }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index cb2c8fc..3364528 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -25,7 +25,6 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
-import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
@@ -191,8 +190,6 @@
         dockManager = DockManagerFake()
         biometricSettingsRepository = FakeBiometricSettingsRepository()
 
-        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-
         val featureFlags =
             FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index 68a5d93..9543032 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -247,7 +247,7 @@
             mContext,
             0,
             intent.setPackage(mContext.packageName),
-            PendingIntent.FLAG_MUTABLE
+            PendingIntent.FLAG_MUTABLE,
         )
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
@@ -294,7 +294,7 @@
                 override fun loadAnimator(
                     animId: Int,
                     otionInterpolator: Interpolator,
-                    vararg targets: View
+                    vararg targets: View,
                 ): AnimatorSet {
                     return mockAnimator
                 }
@@ -323,7 +323,7 @@
                 packageName = PACKAGE,
                 instanceId = instanceId,
                 recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction),
-                cardAction = smartspaceAction
+                cardAction = smartspaceAction,
             )
     }
 
@@ -370,7 +370,7 @@
                 packageName = PACKAGE,
                 token = session.sessionToken,
                 device = device,
-                instanceId = instanceId
+                instanceId = instanceId,
             )
     }
 
@@ -416,7 +416,7 @@
                         action1.id,
                         action2.id,
                         action3.id,
-                        action4.id
+                        action4.id,
                     )
             }
 
@@ -536,7 +536,7 @@
                 playOrPause = MediaAction(icon, Runnable {}, "play", bg),
                 nextOrCustom = MediaAction(icon, Runnable {}, "next", bg),
                 custom0 = MediaAction(icon, null, "custom 0", bg),
-                custom1 = MediaAction(icon, null, "custom 1", bg)
+                custom1 = MediaAction(icon, null, "custom 1", bg),
             )
         val state = mediaData.copy(semanticActions = semanticActions)
         player.attachPlayer(viewHolder)
@@ -590,7 +590,7 @@
                 custom0 = MediaAction(icon, null, "custom 0", bg),
                 custom1 = MediaAction(icon, null, "custom 1", bg),
                 false,
-                true
+                true,
             )
         val state = mediaData.copy(semanticActions = semanticActions)
 
@@ -622,7 +622,7 @@
                 custom0 = MediaAction(icon, null, "custom 0", bg),
                 custom1 = MediaAction(icon, null, "custom 1", bg),
                 true,
-                false
+                false,
             )
         val state = mediaData.copy(semanticActions = semanticActions)
 
@@ -760,7 +760,7 @@
         val semanticActions =
             MediaButton(
                 playOrPause = MediaAction(icon, Runnable {}, "play", null),
-                nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
+                nextOrCustom = MediaAction(icon, Runnable {}, "next", null),
             )
         val state = mediaData.copy(semanticActions = semanticActions)
 
@@ -850,7 +850,7 @@
         val semanticActions =
             MediaButton(
                 prevOrCustom = MediaAction(icon, {}, "prev", null),
-                nextOrCustom = MediaAction(icon, {}, "next", null)
+                nextOrCustom = MediaAction(icon, {}, "next", null),
             )
         val state = mediaData.copy(semanticActions = semanticActions)
 
@@ -921,7 +921,7 @@
         val semanticActions =
             MediaButton(
                 prevOrCustom = MediaAction(icon, {}, "prev", null),
-                nextOrCustom = MediaAction(icon, {}, "next", null)
+                nextOrCustom = MediaAction(icon, {}, "next", null),
             )
         val state = mediaData.copy(semanticActions = semanticActions)
         player.attachPlayer(viewHolder)
@@ -944,7 +944,7 @@
         val semanticActions =
             MediaButton(
                 prevOrCustom = MediaAction(icon, {}, "prev", null),
-                nextOrCustom = MediaAction(icon, {}, "next", null)
+                nextOrCustom = MediaAction(icon, {}, "next", null),
             )
         val state = mediaData.copy(semanticActions = semanticActions)
 
@@ -966,6 +966,29 @@
     }
 
     @Test
+    fun setIsScrubbing_reservedButtonSpaces_scrubbingTimesShown() {
+        val semanticActions =
+            MediaButton(
+                prevOrCustom = null,
+                nextOrCustom = null,
+                reserveNext = true,
+                reservePrev = true,
+            )
+        val state = mediaData.copy(semanticActions = semanticActions)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        reset(expandedSet)
+
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        verify(expandedSet).setVisibility(R.id.actionPrev, View.GONE)
+        verify(expandedSet).setVisibility(R.id.actionNext, View.GONE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.VISIBLE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.VISIBLE)
+    }
+
+    @Test
     fun bind_resumeState_withProgress() {
         val progress = 0.5
         val state = mediaData.copy(resumption = true, resumeProgress = progress)
@@ -1009,13 +1032,13 @@
                 MediaNotificationAction(true, actionIntent = pendingIntent, icon, "play"),
                 MediaNotificationAction(true, actionIntent = null, icon, "next"),
                 MediaNotificationAction(true, actionIntent = null, icon, "custom 0"),
-                MediaNotificationAction(true, actionIntent = pendingIntent, icon, "custom 1")
+                MediaNotificationAction(true, actionIntent = pendingIntent, icon, "custom 1"),
             )
         val state =
             mediaData.copy(
                 actions = actions,
                 actionsToShowInCompact = listOf(1, 2),
-                semanticActions = null
+                semanticActions = null,
             )
 
         player.attachPlayer(viewHolder)
@@ -1701,7 +1724,7 @@
                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
-                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4"),
             )
         val data = mediaData.copy(actions = actions)
 
@@ -1720,7 +1743,7 @@
                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
-                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4"),
             )
         val data = mediaData.copy(actions = actions)
 
@@ -1739,7 +1762,7 @@
                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
                 MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
-                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4"),
             )
         val data = mediaData.copy(actions = actions)
 
@@ -2021,7 +2044,7 @@
                             .setSubtitle(subtitle3)
                             .setIcon(icon)
                             .setExtras(Bundle.EMPTY)
-                            .build()
+                            .build(),
                     )
             )
         player.bindRecommendation(data)
@@ -2047,7 +2070,7 @@
                             .setIcon(
                                 Icon.createWithResource(
                                     context,
-                                    com.android.settingslib.R.drawable.ic_1x_mobiledata
+                                    com.android.settingslib.R.drawable.ic_1x_mobiledata,
                                 )
                             )
                             .setExtras(Bundle.EMPTY)
@@ -2084,7 +2107,7 @@
                             .setSubtitle("fake subtitle")
                             .setIcon(icon)
                             .setExtras(Bundle.EMPTY)
-                            .build()
+                            .build(),
                     )
             )
         player.bindRecommendation(data)
@@ -2119,7 +2142,7 @@
                             .setSubtitle("")
                             .setIcon(icon)
                             .setExtras(Bundle.EMPTY)
-                            .build()
+                            .build(),
                     )
             )
         player.bindRecommendation(data)
@@ -2142,7 +2165,7 @@
                             .setIcon(
                                 Icon.createWithResource(
                                     context,
-                                    com.android.settingslib.R.drawable.ic_1x_mobiledata
+                                    com.android.settingslib.R.drawable.ic_1x_mobiledata,
                                 )
                             )
                             .setExtras(Bundle.EMPTY)
@@ -2157,11 +2180,11 @@
                             .setIcon(
                                 Icon.createWithResource(
                                     context,
-                                    com.android.settingslib.R.drawable.ic_3g_mobiledata
+                                    com.android.settingslib.R.drawable.ic_3g_mobiledata,
                                 )
                             )
                             .setExtras(Bundle.EMPTY)
-                            .build()
+                            .build(),
                     )
             )
 
@@ -2185,7 +2208,7 @@
                             .setIcon(
                                 Icon.createWithResource(
                                     context,
-                                    com.android.settingslib.R.drawable.ic_1x_mobiledata
+                                    com.android.settingslib.R.drawable.ic_1x_mobiledata,
                                 )
                             )
                             .setExtras(Bundle.EMPTY)
@@ -2200,11 +2223,11 @@
                             .setIcon(
                                 Icon.createWithResource(
                                     context,
-                                    com.android.settingslib.R.drawable.ic_3g_mobiledata
+                                    com.android.settingslib.R.drawable.ic_3g_mobiledata,
                                 )
                             )
                             .setExtras(Bundle.EMPTY)
-                            .build()
+                            .build(),
                     )
             )
 
@@ -2245,7 +2268,7 @@
                             .setSubtitle("subtitle1")
                             .setIcon(albumArt)
                             .setExtras(Bundle.EMPTY)
-                            .build()
+                            .build(),
                     )
             )
 
@@ -2268,7 +2291,7 @@
             Bundle().apply {
                 putInt(
                     MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
-                    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
+                    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED,
                 )
                 putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.5)
             }
@@ -2290,7 +2313,7 @@
                             .setSubtitle("subtitle1")
                             .setIcon(albumArt)
                             .setExtras(Bundle.EMPTY)
-                            .build()
+                            .build(),
                     )
             )
 
@@ -2328,7 +2351,7 @@
                             .setSubtitle("subtitle1")
                             .setIcon(albumArt)
                             .setExtras(Bundle.EMPTY)
-                            .build()
+                            .build(),
                     )
             )
 
@@ -2381,7 +2404,7 @@
                             .setSubtitle("subtitle1")
                             .setIcon(albumArt)
                             .setExtras(Bundle.EMPTY)
-                            .build()
+                            .build(),
                     )
             )
 
@@ -2444,7 +2467,7 @@
                         icon = null,
                         action = {},
                         contentDescription = "play",
-                        background = null
+                        background = null,
                     )
             )
         val data = mediaData.copy(semanticActions = semanticActions)
@@ -2465,7 +2488,7 @@
                         icon = null,
                         action = {},
                         contentDescription = "play",
-                        background = null
+                        background = null,
                     )
             )
         val data = mediaData.copy(semanticActions = semanticActions)
@@ -2498,7 +2521,7 @@
                         icon = null,
                         action = {},
                         contentDescription = "play",
-                        background = null
+                        background = null,
                     )
             )
         val data = mediaData.copy(semanticActions = semanticActions)
@@ -2530,8 +2553,8 @@
                         icon = null,
                         action = {},
                         contentDescription = "custom0",
-                        background = null
-                    ),
+                        background = null,
+                    )
             )
         val data = mediaData.copy(semanticActions = semanticActions)
         player.attachPlayer(viewHolder)
@@ -2553,8 +2576,8 @@
                         icon = null,
                         action = {},
                         contentDescription = "custom0",
-                        background = null
-                    ),
+                        background = null,
+                    )
             )
         val data = mediaData.copy(semanticActions = semanticActions)
         player.attachPlayer(viewHolder)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
index 90ffaf1..67c5986 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
@@ -236,7 +236,7 @@
 
         context.orCreateTestableResources.addOverride(
             R.array.tile_states_internet,
-            arrayOf(unavailableString, offString, onString)
+            arrayOf(unavailableString, offString, onString),
         )
 
         // State UNAVAILABLE
@@ -341,7 +341,7 @@
         val testA11yLabel = "TEST_LABEL"
         context.orCreateTestableResources.addOverride(
             R.string.accessibility_tile_disabled_by_policy_action_description,
-            testA11yLabel
+            testA11yLabel,
         )
 
         val stateDisabledByPolicy = QSTile.State()
@@ -374,7 +374,7 @@
 
         context.orCreateTestableResources.addOverride(
             R.array.tile_states_internet,
-            arrayOf(unavailableString, offString, onString)
+            arrayOf(unavailableString, offString, onString),
         )
 
         tileView.changeState(state)
@@ -477,6 +477,24 @@
     }
 
     @Test
+    fun onStateChange_fromLongPress_toNoLongPress_whileLongPressRuns_doesNotClearResources() {
+        // GIVEN that the long-press effect has been initialized
+        val state = QSTile.State()
+        state.handlesLongClick = true
+        tileView.changeState(state)
+
+        // WHEN the long-press effect is running
+        kosmos.qsLongPressEffect.setState(QSLongPressEffect.State.RUNNING_FORWARD)
+
+        // WHEN a state changed happens so that the tile no longer handles long-press
+        state.handlesLongClick = false
+        tileView.changeState(state)
+
+        // THEN the long-press effect resources are not cleared
+        assertThat(tileView.areLongPressEffectPropertiesSet).isTrue()
+    }
+
+    @Test
     fun onStateChange_withoutLongPressEffect_fromLongPress_to_noLongPress_neverSetsProperties() {
         // GIVEN a tile where the long-press effect is null
         tileView = FakeTileView(context, false, null)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
index 4d74254c..4870497 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.common.ui.data.repository
 
 import android.content.res.Configuration
+import android.view.Display
 import com.android.systemui.dagger.SysUISingleton
 import dagger.Binds
 import dagger.Module
@@ -25,6 +26,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 
@@ -46,6 +48,10 @@
     override val configurationValues: Flow<Configuration> =
         _configurationChangeValues.asSharedFlow()
 
+    private val _onMovedToDisplay = MutableStateFlow<Int>(Display.DEFAULT_DISPLAY)
+    override val onMovedToDisplay: StateFlow<Int>
+        get() = _onMovedToDisplay
+
     private val _scaleForResolution = MutableStateFlow(1f)
     override val scaleForResolution: Flow<Float> = _scaleForResolution.asStateFlow()
 
@@ -64,6 +70,10 @@
         onAnyConfigurationChange()
     }
 
+    fun onMovedToDisplay(newDisplayId: Int) {
+        _onMovedToDisplay.value = newDisplayId
+    }
+
     fun setScaleForResolution(scale: Float) {
         _scaleForResolution.value = scale
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
index 41402ba..4513cc0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.flags
 
 import android.platform.test.annotations.EnableFlags
-import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
 import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
 import com.android.systemui.Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN
@@ -29,7 +28,6 @@
  * that feature. It is also picked up by [SceneContainerRule] to set non-aconfig prerequisites.
  */
 @EnableFlags(
-    FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
     FLAG_KEYGUARD_WM_STATE_REFACTOR,
     FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
     FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN,
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 4cb8a41..2641070 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
@@ -22,12 +22,14 @@
 import android.hardware.input.fakeInputManager
 import android.view.windowManager
 import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.keyboard.shortcut.data.repository.AppLaunchDataRepository
 import com.android.systemui.keyboard.shortcut.data.repository.CustomInputGesturesRepository
 import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository
 import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository
 import com.android.systemui.keyboard.shortcut.data.repository.InputGestureDataAdapter
 import com.android.systemui.keyboard.shortcut.data.repository.InputGestureMaps
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesUtils
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperInputDeviceRepository
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper
 import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource
@@ -47,6 +49,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.backgroundScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.sysUiState
@@ -99,35 +102,54 @@
     Kosmos.Fixture {
         DefaultShortcutCategoriesRepository(
             applicationCoroutineScope,
-            testDispatcher,
             shortcutHelperSystemShortcutsSource,
             shortcutHelperMultiTaskingShortcutsSource,
             shortcutHelperAppCategoriesShortcutsSource,
             shortcutHelperInputShortcutsSource,
             shortcutHelperCurrentAppShortcutsSource,
-            fakeInputManager.inputManager,
-            shortcutHelperStateRepository,
+            shortcutHelperInputDeviceRepository,
             shortcutCategoriesUtils,
         )
     }
 
 val Kosmos.inputGestureMaps by Kosmos.Fixture { InputGestureMaps(applicationContext) }
 
-val Kosmos.inputGestureDataAdapter by Kosmos.Fixture { InputGestureDataAdapter(userTracker, inputGestureMaps, applicationContext)}
+val Kosmos.inputGestureDataAdapter by
+    Kosmos.Fixture { InputGestureDataAdapter(userTracker, inputGestureMaps, applicationContext) }
 
 val Kosmos.customInputGesturesRepository by
     Kosmos.Fixture { CustomInputGesturesRepository(userTracker, testDispatcher) }
 
+val Kosmos.shortcutHelperInputDeviceRepository by
+    Kosmos.Fixture {
+        ShortcutHelperInputDeviceRepository(
+            shortcutHelperStateRepository,
+            backgroundScope,
+            backgroundCoroutineContext,
+            fakeInputManager.inputManager,
+        )
+    }
+
+val Kosmos.appLaunchDataRepository by
+    Kosmos.Fixture {
+        AppLaunchDataRepository(
+            fakeInputManager.inputManager,
+            backgroundScope,
+            shortcutCategoriesUtils,
+            shortcutHelperInputDeviceRepository,
+        )
+    }
+
 val Kosmos.customShortcutCategoriesRepository by
     Kosmos.Fixture {
         CustomShortcutCategoriesRepository(
-            shortcutHelperStateRepository,
+            shortcutHelperInputDeviceRepository,
             applicationCoroutineScope,
-            testDispatcher,
             shortcutCategoriesUtils,
             inputGestureDataAdapter,
             customInputGesturesRepository,
             fakeInputManager.inputManager,
+            appLaunchDataRepository,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 693ec79..1288d31 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -56,9 +56,6 @@
     override val animateBottomAreaDozingTransitions: StateFlow<Boolean> =
         _animateBottomAreaDozingTransitions
 
-    private val _bottomAreaAlpha = MutableStateFlow(1f)
-    override val bottomAreaAlpha: StateFlow<Float> = _bottomAreaAlpha
-
     private val _isKeyguardShowing = MutableStateFlow(false)
     override val isKeyguardShowing: StateFlow<Boolean> = _isKeyguardShowing
 
@@ -159,11 +156,6 @@
         _animateBottomAreaDozingTransitions.tryEmit(animate)
     }
 
-    @Deprecated("Deprecated as part of b/278057014")
-    override fun setBottomAreaAlpha(alpha: Float) {
-        _bottomAreaAlpha.value = alpha
-    }
-
     fun setKeyguardShowing(isShowing: Boolean) {
         _isKeyguardShowing.value = isShowing
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelKosmos.kt
index 2d1f836..b03624b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelKosmos.kt
@@ -26,5 +26,6 @@
 val Kosmos.alternateBouncerToPrimaryBouncerTransitionViewModel by Fixture {
     AlternateBouncerToPrimaryBouncerTransitionViewModel(
         animationFlow = keyguardTransitionAnimationFlow,
+        shadeDependentFlows = shadeDependentFlows,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerViewModelKosmos.kt
similarity index 60%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerViewModelKosmos.kt
index a3955f7..5e6d605 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerViewModelKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.domain.interactor
+package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
-val Kosmos.keyguardBottomAreaInteractor by Fixture {
-    KeyguardBottomAreaInteractor(
-        repository = keyguardRepository,
-    )
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.aodToPrimaryBouncerTransitionViewModel by Fixture {
+    AodToPrimaryBouncerTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow)
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 3ab686d..e473107 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -51,6 +51,8 @@
             alternateBouncerToLockscreenTransitionViewModel,
         alternateBouncerToOccludedTransitionViewModel =
             alternateBouncerToOccludedTransitionViewModel,
+        alternateBouncerToPrimaryBouncerTransitionViewModel =
+            alternateBouncerToPrimaryBouncerTransitionViewModel,
         aodToGoneTransitionViewModel = aodToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
         aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModelKosmos.kt
similarity index 64%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModelKosmos.kt
index a3955f7..09233af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModelKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.domain.interactor
+package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.keyguardBottomAreaInteractor by Fixture {
-    KeyguardBottomAreaInteractor(
-        repository = keyguardRepository,
+val Kosmos.primaryBouncerToGlanceableHubTransitionViewModel by Fixture {
+    PrimaryBouncerToGlanceableHubTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
index 370afc3..76478cb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
@@ -26,5 +26,6 @@
 val Kosmos.primaryBouncerToLockscreenTransitionViewModel by Fixture {
     PrimaryBouncerToLockscreenTransitionViewModel(
         animationFlow = keyguardTransitionAnimationFlow,
+        shadeDependentFlows = shadeDependentFlows,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt
index d72630d..01e357e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt
@@ -18,6 +18,7 @@
 
 import android.app.admin.devicePolicyManager
 import android.content.applicationContext
+import android.content.mockedContext
 import android.os.fakeExecutorHandler
 import android.os.looper
 import com.android.internal.logging.metricsLogger
@@ -54,9 +55,7 @@
 val Kosmos.fgsManagerController by Fixture { FakeFgsManagerController() }
 
 val Kosmos.footerActionsController by Fixture {
-    FooterActionsController(
-        fgsManagerController = fgsManagerController,
-    )
+    FooterActionsController(fgsManagerController = fgsManagerController)
 }
 
 val Kosmos.qsSecurityFooterUtils by Fixture {
@@ -86,6 +85,7 @@
         userSwitcherRepository = userSwitcherRepository,
         broadcastDispatcher = broadcastDispatcher,
         bgDispatcher = testDispatcher,
+        context = mockedContext,
     )
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
index cde5d4e..9edeb0c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
@@ -69,6 +69,7 @@
     private val scheduler: TestCoroutineScheduler,
 ) {
     private val mockActivityStarter: ActivityStarter = mock<ActivityStarter>()
+
     /** Enable or disable the user switcher in the settings. */
     fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean) {
         settings.putBool(Settings.Global.USER_SWITCHER_ENABLED, enabled)
@@ -110,6 +111,7 @@
         userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(),
         broadcastDispatcher: BroadcastDispatcher = mock(),
         bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler),
+        context: Context = mock(),
     ): FooterActionsInteractor {
         return FooterActionsInteractorImpl(
             activityStarter,
@@ -124,6 +126,7 @@
             userSwitcherRepository,
             broadcastDispatcher,
             bgDispatcher,
+            context,
         )
     }
 
@@ -132,15 +135,12 @@
         securityController: SecurityController = FakeSecurityController(),
         bgDispatcher: CoroutineDispatcher = StandardTestDispatcher(scheduler),
     ): SecurityRepository {
-        return SecurityRepositoryImpl(
-            securityController,
-            bgDispatcher,
-        )
+        return SecurityRepositoryImpl(securityController, bgDispatcher)
     }
 
     /** Create a [SecurityRepository] to be used in tests. */
     fun foregroundServicesRepository(
-        fgsManagerController: FakeFgsManagerController = FakeFgsManagerController(),
+        fgsManagerController: FakeFgsManagerController = FakeFgsManagerController()
     ): ForegroundServicesRepository {
         return ForegroundServicesRepositoryImpl(fgsManagerController)
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt
index 5d146fb..3d45a51 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt
@@ -29,7 +29,7 @@
 /** This factory creates empty mocks. */
 var Kosmos.brightnessSliderControllerFactory by
     Kosmos.Fixture<BrightnessSliderController.Factory> {
-        BrightnessSliderController.Factory(
+        BrightnessSliderController.BrightnessSliderControllerFactory(
             falsingManager,
             uiEventLogger,
             vibratorHelper,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt
index 7488397d..636cb37 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt
@@ -16,20 +16,53 @@
 
 package com.android.systemui.shade.data.repository
 
-import android.view.Display
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.display.AnyExternalShadeDisplayPolicy
+import com.android.systemui.shade.display.DefaultDisplayShadePolicy
 import com.android.systemui.shade.display.ShadeDisplayPolicy
-import com.android.systemui.shade.display.SpecificDisplayIdPolicy
+import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy
+import com.android.systemui.util.settings.fakeGlobalSettings
 
-val Kosmos.defaultShadeDisplayPolicy: ShadeDisplayPolicy by
-    Kosmos.Fixture { SpecificDisplayIdPolicy(Display.DEFAULT_DISPLAY) }
+val Kosmos.defaultShadeDisplayPolicy: DefaultDisplayShadePolicy by
+    Kosmos.Fixture { DefaultDisplayShadePolicy() }
+
+val Kosmos.anyExternalShadeDisplayPolicy: AnyExternalShadeDisplayPolicy by
+    Kosmos.Fixture {
+        AnyExternalShadeDisplayPolicy(
+            bgScope = testScope.backgroundScope,
+            displayRepository = displayRepository,
+        )
+    }
+
+val Kosmos.focusBasedShadeDisplayPolicy: StatusBarTouchShadeDisplayPolicy by
+    Kosmos.Fixture {
+        StatusBarTouchShadeDisplayPolicy(
+            displayRepository = displayRepository,
+            backgroundScope = testScope.backgroundScope,
+            keyguardRepository = keyguardRepository,
+            shadeOnDefaultDisplayWhenLocked = false,
+        )
+    }
 
 val Kosmos.shadeDisplaysRepository: MutableShadeDisplaysRepository by
     Kosmos.Fixture {
         ShadeDisplaysRepositoryImpl(
-            defaultPolicy = defaultShadeDisplayPolicy,
             bgScope = testScope.backgroundScope,
+            globalSettings = fakeGlobalSettings,
+            policies = shadeDisplayPolicies,
+            defaultPolicy = defaultShadeDisplayPolicy,
+        )
+    }
+
+val Kosmos.shadeDisplayPolicies: Set<ShadeDisplayPolicy> by
+    Kosmos.Fixture {
+        setOf(
+            defaultShadeDisplayPolicy,
+            anyExternalShadeDisplayPolicy,
+            focusBasedShadeDisplayPolicy,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt
index 00b788f..c0d2621 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt
@@ -30,7 +30,6 @@
             backgroundScope = applicationCoroutineScope,
             shadeInteractor = shadeInteractorImpl,
             sceneInteractor = sceneInteractor,
-            lockIconViewController = mock(),
             shadeRepository = shadeRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandlerKosmos.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandlerKosmos.kt
index a3955f7..72165c9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandlerKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,14 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.domain.interactor
+package com.android.systemui.statusbar.gesture
 
-import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
+import org.mockito.kotlin.mock
 
-val Kosmos.keyguardBottomAreaInteractor by Fixture {
-    KeyguardBottomAreaInteractor(
-        repository = keyguardRepository,
-    )
-}
+val Kosmos.swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler by
+Kosmos.Fixture {
+    mock<SwipeStatusBarAwayGestureHandler>()
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt
index 9090e02..40d9101 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
+import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore
 
@@ -32,6 +33,7 @@
           activityManagerRepository = activityManagerRepository,
           statusBarModeRepositoryStore = fakeStatusBarModeRepository,
           statusBarWindowControllerStore = fakeStatusBarWindowControllerStore,
+          swipeStatusBarAwayGestureHandler = swipeStatusBarAwayGestureHandler,
           logBuffer = logcatLogBuffer("OngoingCallInteractorKosmos"),
       )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
index 3219127..13673d1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
@@ -27,6 +27,10 @@
         listeners.forEach { it.onConfigChanged(newConfiguration) }
     }
 
+    override fun dispatchOnMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration) {
+        listeners.forEach { it.onMovedToDisplay(newDisplayId, newConfiguration) }
+    }
+
     override fun notifyThemeChanged() {
         listeners.forEach { it.onThemeChanged() }
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java
index 111c40d..9cf25e8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java
@@ -16,6 +16,8 @@
 
 import android.content.res.Configuration;
 
+import androidx.annotation.NonNull;
+
 import com.android.systemui.statusbar.policy.ConfigurationController;
 
 public class FakeConfigurationController
@@ -43,4 +45,10 @@
     public String getNightModeName() {
         return "undefined";
     }
+
+    @Override
+    public void dispatchOnMovedToDisplay(int newDisplayId,
+            @NonNull Configuration newConfiguration) {
+
+    }
 }
diff --git a/services/Android.bp b/services/Android.bp
index 473911f..efd35ce 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -233,8 +233,7 @@
             libs: ["service-ondeviceintelligence.stubs.system_server"],
         },
         release_ondevice_intelligence_platform: {
-            srcs: [":service-ondeviceintelligence-sources"],
-            static_libs: ["modules-utils-backgroundthread"],
+            srcs: [":service-ondeviceintelligence-sources-platform"],
         },
     },
 }
@@ -245,13 +244,21 @@
     name: "system_java_library",
     module_type: "java_library",
     config_namespace: "system_services",
-    bool_variables: ["without_vibrator"],
+    variables: ["without_hal"],
     properties: ["vintf_fragment_modules"],
 }
 
+soong_config_string_variable {
+    name: "without_hal",
+    values: [
+        "vibrator",
+        "devicestate",
+    ],
+}
+
 vintf_fragment {
-    name: "manifest_services.xml",
-    src: "manifest_services.xml",
+    name: "manifest_services_android.frameworks.location.xml",
+    src: "manifest_services_android.frameworks.location.xml",
 }
 
 vintf_fragment {
@@ -259,6 +266,11 @@
     src: "manifest_services_android.frameworks.vibrator.xml",
 }
 
+vintf_fragment {
+    name: "manifest_services_android.frameworks.devicestate.xml",
+    src: "manifest_services_android.frameworks.devicestate.xml",
+}
+
 system_java_library {
     name: "services",
     defaults: [
@@ -329,14 +341,24 @@
     ],
 
     soong_config_variables: {
-        without_vibrator: {
-            vintf_fragment_modules: [
-                "manifest_services.xml",
-            ],
+        without_hal: {
+            vibrator: {
+                vintf_fragment_modules: [
+                    "manifest_services_android.frameworks.location.xml",
+                    "manifest_services_android.frameworks.devicestate.xml",
+                ],
+            },
+            devicestate: {
+                vintf_fragment_modules: [
+                    "manifest_services_android.frameworks.location.xml",
+                    "manifest_services_android.frameworks.vibrator.xml",
+                ],
+            },
             conditions_default: {
                 vintf_fragment_modules: [
-                    "manifest_services.xml",
+                    "manifest_services_android.frameworks.location.xml",
                     "manifest_services_android.frameworks.vibrator.xml",
+                    "manifest_services_android.frameworks.devicestate.xml",
                 ],
             },
         },
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 5c1ad74..37d045b 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1057,17 +1057,20 @@
                             intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE);
                     final int restoredFromSdk =
                             intent.getIntExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, 0);
+                    final int userId =
+                            android.view.accessibility.Flags.restoreA11ySecureSettingsOnHsumDevice()
+                                    ? getSendingUserId() : UserHandle.USER_SYSTEM;
                     switch (which) {
                         case Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES -> {
                             synchronized (mLock) {
                                 restoreEnabledAccessibilityServicesLocked(
-                                        previousValue, newValue, restoredFromSdk);
+                                        previousValue, newValue, restoredFromSdk, userId);
                             }
                         }
                         case ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED -> {
                             synchronized (mLock) {
                                 restoreLegacyDisplayMagnificationNavBarIfNeededLocked(
-                                        newValue, restoredFromSdk);
+                                        newValue, restoredFromSdk, userId);
                             }
                         }
                         // Currently in SUW, the user can't see gesture shortcut option as the
@@ -1078,7 +1081,7 @@
                              Settings.Secure.ACCESSIBILITY_QS_TARGETS,
                              Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE ->
                                 restoreShortcutTargets(newValue,
-                                        ShortcutUtils.convertToType(which));
+                                        ShortcutUtils.convertToType(which), userId);
                     }
                 }
             }
@@ -1144,10 +1147,10 @@
         }
     }
 
-    // Called only during settings restore; currently supports only the owner user
-    // TODO: b/22388012
-    private void restoreLegacyDisplayMagnificationNavBarIfNeededLocked(String newSetting,
-            int restoreFromSdkInt) {
+    // Called only during settings restore; currently supports only the main user
+    // TODO: http://b/374830726
+    private void restoreLegacyDisplayMagnificationNavBarIfNeededLocked(
+            String newSetting, int restoreFromSdkInt, int userId) {
         if (restoreFromSdkInt >= Build.VERSION_CODES.R) {
             return;
         }
@@ -1160,7 +1163,7 @@
             return;
         }
 
-        final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
+        final AccessibilityUserState userState = getUserStateLocked(userId);
         final Set<String> targetsFromSetting = new ArraySet<>();
         readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
                 userState.mUserId, str -> str, targetsFromSetting);
@@ -2225,20 +2228,20 @@
         getMagnificationController().onUserRemoved(userId);
     }
 
-    // Called only during settings restore; currently supports only the owner user
-    // TODO: http://b/22388012
-    void restoreEnabledAccessibilityServicesLocked(String oldSetting, String newSetting,
-            int restoreFromSdkInt) {
+    // Called only during settings restore; currently supports only the main user
+    // TODO: http://b/374830726
+    void restoreEnabledAccessibilityServicesLocked(
+            String oldSetting, String newSetting, int restoreFromSdkInt, int userId) {
         readComponentNamesFromStringLocked(oldSetting, mTempComponentNameSet, false);
         readComponentNamesFromStringLocked(newSetting, mTempComponentNameSet, true);
 
-        AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
+        AccessibilityUserState userState = getUserStateLocked(userId);
         userState.mEnabledServices.clear();
         userState.mEnabledServices.addAll(mTempComponentNameSet);
         persistComponentNamesToSettingLocked(
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 userState.mEnabledServices,
-                UserHandle.USER_SYSTEM);
+                userState.mUserId);
         onUserStateChangedLocked(userState);
         migrateAccessibilityButtonSettingsIfNecessaryLocked(userState, null, restoreFromSdkInt);
     }
@@ -2247,21 +2250,22 @@
      * User could configure accessibility shortcut during the SUW before restoring user data.
      * Merges the current value and the new value to make sure we don't lost the setting the user's
      * preferences of accessibility shortcut updated in SUW are not lost.
-     * Called only during settings restore; currently supports only the owner user.
+     *
      * <P>
      * Throws an exception if used with {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP}.
      * </P>
-     * TODO: http://b/22388012
      */
-    private void restoreShortcutTargets(String newValue,
-            @UserShortcutType int shortcutType) {
+    // Called only during settings restore; currently supports only the main user.
+    // TODO: http://b/374830726
+    private void restoreShortcutTargets(
+            String newValue, @UserShortcutType int shortcutType, int userId) {
         assertNoTapShortcut(shortcutType);
         if (shortcutType == QUICK_SETTINGS && !android.view.accessibility.Flags.a11yQsShortcut()) {
             return;
         }
 
         synchronized (mLock) {
-            final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
+            final AccessibilityUserState userState = getUserStateLocked(userId);
             final Set<String> mergedTargets = (shortcutType == HARDWARE)
                     ? new ArraySet<>(ShortcutUtils.getShortcutTargetsFromSettings(
                             mContext, shortcutType, userState.mUserId))
@@ -2295,7 +2299,7 @@
 
             userState.updateShortcutTargetsLocked(mergedTargets, shortcutType);
             persistColonDelimitedSetToSettingLocked(ShortcutUtils.convertToKey(shortcutType),
-                    UserHandle.USER_SYSTEM, mergedTargets, str -> str);
+                    userState.mUserId, mergedTargets, str -> str);
             scheduleNotifyClientsOfServicesStateChangeLocked(userState);
             onUserStateChangedLocked(userState);
         }
diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java
index ccc44a4..bedc130 100644
--- a/services/core/java/com/android/server/GestureLauncherService.java
+++ b/services/core/java/com/android/server/GestureLauncherService.java
@@ -16,9 +16,13 @@
 
 package com.android.server;
 
+import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap;
+
 import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis;
 
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
 import android.app.StatusBarManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -33,6 +37,7 @@
 import android.hardware.SensorManager;
 import android.hardware.TriggerEvent;
 import android.hardware.TriggerEventListener;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
@@ -41,10 +46,12 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.service.quickaccesswallet.QuickAccessWalletClient;
 import android.util.MutableBoolean;
 import android.util.Slog;
 import android.view.KeyEvent;
 
+import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEvent;
@@ -70,7 +77,7 @@
      * Time in milliseconds in which the power button must be pressed twice so it will be considered
      * as a camera launch.
      */
-    @VisibleForTesting static final long CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS = 300;
+    @VisibleForTesting static final long POWER_DOUBLE_TAP_MAX_TIME_MS = 300;
 
 
     /**
@@ -100,10 +107,23 @@
     @VisibleForTesting
     static final int EMERGENCY_GESTURE_POWER_BUTTON_COOLDOWN_PERIOD_MS_MAX = 5000;
 
-    /**
-     * Number of taps required to launch camera shortcut.
-     */
-    private static final int CAMERA_POWER_TAP_COUNT_THRESHOLD = 2;
+    /** Indicates camera should be launched on power double tap. */
+    @VisibleForTesting static final int LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER = 0;
+
+    /** Indicates wallet should be launched on power double tap. */
+    @VisibleForTesting static final int LAUNCH_WALLET_ON_DOUBLE_TAP_POWER = 1;
+
+    /** Number of taps required to launch the double tap shortcut (either camera or wallet). */
+    public static final int DOUBLE_POWER_TAP_COUNT_THRESHOLD = 2;
+
+    /** Bundle to send with PendingIntent to grant background activity start privileges. */
+    private static final Bundle GRANT_BACKGROUND_START_PRIVILEGES =
+            ActivityOptions.makeBasic()
+                    .setPendingIntentBackgroundActivityStartMode(
+                            ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS)
+                    .toBundle();
+
+    private final QuickAccessWalletClient mQuickAccessWalletClient;
 
     /** The listener that receives the gesture event. */
     private final GestureEventListener mGestureListener = new GestureEventListener();
@@ -158,6 +178,9 @@
      */
     private boolean mCameraDoubleTapPowerEnabled;
 
+    /** Whether wallet double tap power button gesture is currently enabled. */
+    private boolean mWalletDoubleTapPowerEnabled;
+
     /**
      * Whether emergency gesture is currently enabled
      */
@@ -204,15 +227,17 @@
         }
     }
     public GestureLauncherService(Context context) {
-        this(context, new MetricsLogger(), new UiEventLoggerImpl());
+        this(context, new MetricsLogger(),
+                QuickAccessWalletClient.create(context), new UiEventLoggerImpl());
     }
 
     @VisibleForTesting
     GestureLauncherService(Context context, MetricsLogger metricsLogger,
-            UiEventLogger uiEventLogger) {
+            QuickAccessWalletClient quickAccessWalletClient, UiEventLogger uiEventLogger) {
         super(context);
         mContext = context;
         mMetricsLogger = metricsLogger;
+        mQuickAccessWalletClient = quickAccessWalletClient;
         mUiEventLogger = uiEventLogger;
     }
 
@@ -237,6 +262,9 @@
                     "GestureLauncherService");
             updateCameraRegistered();
             updateCameraDoubleTapPowerEnabled();
+            if (launchWalletOptionOnPowerDoubleTap()) {
+                updateWalletDoubleTapPowerEnabled();
+            }
             updateEmergencyGestureEnabled();
             updateEmergencyGesturePowerButtonCooldownPeriodMs();
 
@@ -250,12 +278,24 @@
     }
 
     private void registerContentObservers() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.CAMERA_GESTURE_DISABLED),
-                false, mSettingObserver, mUserId);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED),
-                false, mSettingObserver, mUserId);
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Secure.getUriFor(
+                            Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED),
+                    false, mSettingObserver, mUserId);
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Secure.getUriFor(
+                            Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE),
+                    false, mSettingObserver, mUserId);
+        } else {
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.CAMERA_GESTURE_DISABLED),
+                    false, mSettingObserver, mUserId);
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Secure.getUriFor(
+                            Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED),
+                    false, mSettingObserver, mUserId);
+        }
         mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.CAMERA_LIFT_TRIGGER_ENABLED),
                 false, mSettingObserver, mUserId);
@@ -292,6 +332,14 @@
     }
 
     @VisibleForTesting
+    void updateWalletDoubleTapPowerEnabled() {
+        boolean enabled = isWalletDoubleTapPowerSettingEnabled(mContext, mUserId);
+        synchronized (this) {
+            mWalletDoubleTapPowerEnabled = enabled;
+        }
+    }
+
+    @VisibleForTesting
     void updateEmergencyGestureEnabled() {
         boolean enabled = isEmergencyGestureSettingEnabled(mContext, mUserId);
         synchronized (this) {
@@ -418,10 +466,34 @@
                         Settings.Secure.CAMERA_GESTURE_DISABLED, 0, userId) == 0);
     }
 
+
+    /** Checks if camera should be launched on double press of the power button. */
     public static boolean isCameraDoubleTapPowerSettingEnabled(Context context, int userId) {
-        return isCameraDoubleTapPowerEnabled(context.getResources())
-                && (Settings.Secure.getIntForUser(context.getContentResolver(),
-                        Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0);
+        boolean res;
+
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            res = isDoubleTapPowerGestureSettingEnabled(context, userId)
+                    && getDoubleTapPowerGestureAction(context, userId)
+                    == LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER;
+        } else {
+            // These are legacy settings that will be deprecated once the option to launch both
+            // wallet and camera has been created.
+            res = isCameraDoubleTapPowerEnabled(context.getResources())
+                    && (Settings.Secure.getIntForUser(context.getContentResolver(),
+                    Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0);
+        }
+        return res;
+    }
+
+    /** Checks if wallet should be launched on double tap of the power button. */
+    public static boolean isWalletDoubleTapPowerSettingEnabled(Context context, int userId) {
+        if (!launchWalletOptionOnPowerDoubleTap()) {
+            return false;
+        }
+
+        return isDoubleTapPowerGestureSettingEnabled(context, userId)
+                && getDoubleTapPowerGestureAction(context, userId)
+                == LAUNCH_WALLET_ON_DOUBLE_TAP_POWER;
     }
 
     public static boolean isCameraLiftTriggerSettingEnabled(Context context, int userId) {
@@ -441,6 +513,28 @@
                 isDefaultEmergencyGestureEnabled(context.getResources()) ? 1 : 0, userId) != 0;
     }
 
+    private static int getDoubleTapPowerGestureAction(Context context, int userId) {
+        return Settings.Secure.getIntForUser(
+                context.getContentResolver(),
+                Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE,
+                LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER,
+                userId);
+    }
+
+    /** Whether the shortcut to launch app on power double press is enabled. */
+    private static boolean isDoubleTapPowerGestureSettingEnabled(Context context, int userId) {
+        return Settings.Secure.getIntForUser(
+                context.getContentResolver(),
+                Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED,
+                isDoubleTapConfigEnabled(context.getResources()) ? 1 : 0,
+                userId)
+                == 1;
+    }
+
+    private static boolean isDoubleTapConfigEnabled(Resources resources) {
+        return resources.getBoolean(R.bool.config_doubleTapPowerGestureEnabled);
+    }
+
     /**
      * Gets power button cooldown period in milliseconds after emergency gesture is triggered. The
      * value is capped at a maximum
@@ -494,12 +588,19 @@
      * Whether GestureLauncherService should be enabled according to system properties.
      */
     public static boolean isGestureLauncherEnabled(Resources resources) {
-        return isCameraLaunchEnabled(resources)
-                || isCameraDoubleTapPowerEnabled(resources)
-                || isCameraLiftTriggerEnabled(resources)
-                || isEmergencyGestureEnabled(resources);
+        boolean res =
+                isCameraLaunchEnabled(resources)
+                        || isCameraLiftTriggerEnabled(resources)
+                        || isEmergencyGestureEnabled(resources);
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            res |= isDoubleTapConfigEnabled(resources);
+        } else {
+            res |= isCameraDoubleTapPowerEnabled(resources);
+        }
+        return res;
     }
 
+
     /**
      * Attempts to intercept power key down event by detecting certain gesture patterns
      *
@@ -530,6 +631,7 @@
             return false;
         }
         boolean launchCamera = false;
+        boolean launchWallet = false;
         boolean launchEmergencyGesture = false;
         boolean intercept = false;
         long powerTapInterval;
@@ -541,7 +643,7 @@
                 mFirstPowerDown  = event.getEventTime();
                 mPowerButtonConsecutiveTaps = 1;
                 mPowerButtonSlowConsecutiveTaps = 1;
-            } else if (powerTapInterval >= CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS) {
+            } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) {
                 // Tap too slow for shortcuts
                 mFirstPowerDown  = event.getEventTime();
                 mPowerButtonConsecutiveTaps = 1;
@@ -586,10 +688,16 @@
                 }
             }
             if (mCameraDoubleTapPowerEnabled
-                    && powerTapInterval < CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS
-                    && mPowerButtonConsecutiveTaps == CAMERA_POWER_TAP_COUNT_THRESHOLD) {
+                    && powerTapInterval < POWER_DOUBLE_TAP_MAX_TIME_MS
+                    && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD) {
                 launchCamera = true;
                 intercept = interactive;
+            } else if (launchWalletOptionOnPowerDoubleTap()
+                    && mWalletDoubleTapPowerEnabled
+                    && powerTapInterval < POWER_DOUBLE_TAP_MAX_TIME_MS
+                    && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD) {
+                launchWallet = true;
+                intercept = interactive;
             }
         }
         if (mPowerButtonConsecutiveTaps > 1 || mPowerButtonSlowConsecutiveTaps > 1) {
@@ -608,6 +716,10 @@
                         (int) powerTapInterval);
                 mUiEventLogger.log(GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER);
             }
+        } else if (launchWallet) {
+            Slog.i(TAG, "Power button double tap gesture detected, launching wallet. Interval="
+                    + powerTapInterval + "ms");
+            launchWallet = sendGestureTargetActivityPendingIntent();
         } else if (launchEmergencyGesture) {
             Slog.i(TAG, "Emergency gesture detected, launching.");
             launchEmergencyGesture = handleEmergencyGesture();
@@ -623,11 +735,75 @@
                 mPowerButtonSlowConsecutiveTaps);
         mMetricsLogger.histogram("power_double_tap_interval", (int) powerTapInterval);
 
-        outLaunched.value = launchCamera || launchEmergencyGesture;
+        outLaunched.value = launchCamera || launchEmergencyGesture || launchWallet;
         // Intercept power key event if the press is part of a gesture (camera, eGesture) and the
         // user has completed setup.
         return intercept && isUserSetupComplete();
     }
+
+    /**
+     * Fetches and sends gestureTargetActivityPendingIntent from QuickAccessWallet, which is a
+     * specific activity that QuickAccessWalletService has defined to be launch on detection of the
+     * power button gesture.
+     */
+    private boolean sendGestureTargetActivityPendingIntent() {
+        boolean userSetupComplete = isUserSetupComplete();
+        if (mQuickAccessWalletClient == null
+                || !mQuickAccessWalletClient.isWalletServiceAvailable()) {
+            Slog.w(TAG, "QuickAccessWalletService is not available, ignoring wallet gesture.");
+            return false;
+        }
+
+        if (!userSetupComplete) {
+            if (DBG) {
+                Slog.d(TAG, "userSetupComplete = false, ignoring wallet gesture.");
+            }
+            return false;
+        }
+        if (DBG) {
+            Slog.d(TAG, "userSetupComplete = true, performing wallet gesture.");
+        }
+
+        mQuickAccessWalletClient.getGestureTargetActivityPendingIntent(
+                getContext().getMainExecutor(),
+                gesturePendingIntent -> {
+                    if (gesturePendingIntent == null) {
+                        Slog.d(TAG, "getGestureTargetActivityPendingIntent is null.");
+                        sendFallbackPendingIntent();
+                        return;
+                    }
+                    sendPendingIntentWithBackgroundStartPrivileges(gesturePendingIntent);
+                });
+        return true;
+    }
+
+    /**
+     * If gestureTargetActivityPendingIntent is null, this method is invoked to start the activity
+     * that QuickAccessWalletService has defined to host the Wallet view, which is typically the
+     * home screen of the Wallet application.
+     */
+    private void sendFallbackPendingIntent() {
+        mQuickAccessWalletClient.getWalletPendingIntent(
+                getContext().getMainExecutor(),
+                walletPendingIntent -> {
+                    if (walletPendingIntent == null) {
+                        Slog.w(TAG, "getWalletPendingIntent returns null. Not launching "
+                                + "anything for wallet.");
+                        return;
+                    }
+                    sendPendingIntentWithBackgroundStartPrivileges(walletPendingIntent);
+                });
+    }
+
+    private void sendPendingIntentWithBackgroundStartPrivileges(PendingIntent pendingIntent) {
+        try {
+            pendingIntent.send(GRANT_BACKGROUND_START_PRIVILEGES);
+        } catch (PendingIntent.CanceledException e) {
+            Slog.e(TAG, "PendingIntent was canceled", e);
+        }
+    }
+
+
     /**
      * @return true if camera was launched, false otherwise.
      */
@@ -709,6 +885,9 @@
                 registerContentObservers();
                 updateCameraRegistered();
                 updateCameraDoubleTapPowerEnabled();
+                if (launchWalletOptionOnPowerDoubleTap()) {
+                    updateWalletDoubleTapPowerEnabled();
+                }
                 updateEmergencyGestureEnabled();
                 updateEmergencyGesturePowerButtonCooldownPeriodMs();
             }
@@ -720,6 +899,9 @@
             if (userId == mUserId) {
                 updateCameraRegistered();
                 updateCameraDoubleTapPowerEnabled();
+                if (launchWalletOptionOnPowerDoubleTap()) {
+                    updateWalletDoubleTapPowerEnabled();
+                }
                 updateEmergencyGestureEnabled();
                 updateEmergencyGesturePowerButtonCooldownPeriodMs();
             }
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index aea24d9..600aa1f 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -427,11 +427,14 @@
                 com.android.internal.R.bool.config_batteryStatsResetOnUnplugHighBatteryLevel);
         final boolean resetOnUnplugAfterSignificantCharge = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_batteryStatsResetOnUnplugAfterSignificantCharge);
+        final int batteryHistoryStorageSize = context.getResources().getInteger(
+                com.android.internal.R.integer.config_batteryHistoryStorageSize);
         BatteryStatsImpl.BatteryStatsConfig.Builder batteryStatsConfigBuilder =
                 new BatteryStatsImpl.BatteryStatsConfig.Builder()
                         .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel)
                         .setResetOnUnplugAfterSignificantCharge(
-                                resetOnUnplugAfterSignificantCharge);
+                                resetOnUnplugAfterSignificantCharge)
+                        .setMaxHistorySizeBytes(batteryHistoryStorageSize);
         setPowerStatsThrottlePeriods(batteryStatsConfigBuilder, context.getResources().getString(
                 com.android.internal.R.string.config_powerStatsThrottlePeriods));
         mBatteryStatsConfig = batteryStatsConfigBuilder.build();
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index 354f281..aa06b7e 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -316,8 +316,7 @@
                 return null;
             }
             if (callerApp.info.uid != SYSTEM_UID
-                    && !callerApp.getPkgList().containsKey(callerPackage)
-                    && !"android".equals(callerPackage)) {
+                    && !callerApp.getPkgList().containsKey(callerPackage)) {
                 throw new SecurityException("Given caller package " + callerPackage
                         + " is not running in process " + callerApp);
             }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1799b77..0fd4716 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -11845,7 +11845,7 @@
 
     private AudioDeviceAttributes anonymizeAudioDeviceAttributesUnchecked(
             AudioDeviceAttributes ada) {
-        if (!AudioSystem.isBluetoothDevice(ada.getInternalType())) {
+        if (ada == null || !AudioSystem.isBluetoothDevice(ada.getInternalType())) {
             return ada;
         }
         AudioDeviceAttributes res = new AudioDeviceAttributes(ada);
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index 6feae34..8b6b0f9 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -523,7 +523,7 @@
         // older target sdk to impact all system uid apps
         if (Flags.systemUidTargetSystemSdk() && !mIsWear &&
                 uid == Process.SYSTEM_UID && appInfo != null) {
-            appInfo.targetSdkVersion = Build.VERSION.SDK_INT;
+            appInfo.targetSdkVersion = mBuildClassifier.platformTargetSdk();
         }
         return appInfo;
     }
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 4c5f652..ac0892b 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -1960,6 +1960,10 @@
     public void onUserAdded(int userId) {
         // If the user is restricted tie them to the parent user's VPN
         UserInfo user = mUserManager.getUserInfo(userId);
+        if (user == null) {
+            Log.e(TAG, "Can not retrieve UserInfo for userId=" + userId);
+            return;
+        }
         if (user.isRestricted() && user.restrictedProfileParentId == mUserId) {
             synchronized(Vpn.this) {
                 final Set<Range<Integer>> existingRanges = mNetworkCapabilities.getUids();
@@ -1989,6 +1993,14 @@
     public void onUserRemoved(int userId) {
         // clean up if restricted
         UserInfo user = mUserManager.getUserInfo(userId);
+        // TODO: Retrieving UserInfo upon receiving the USER_REMOVED intent is not guaranteed.
+        //  This could prevent the removal of associated ranges. To ensure proper range removal,
+        //  store the user info when adding ranges. This allows using the user ID in the
+        //  USER_REMOVED intent to handle the removal process.
+        if (user == null) {
+            Log.e(TAG, "Can not retrieve UserInfo for userId=" + userId);
+            return;
+        }
         if (user.isRestricted() && user.restrictedProfileParentId == mUserId) {
             synchronized(Vpn.this) {
                 final Set<Range<Integer>> existingRanges = mNetworkCapabilities.getUids();
diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java
index 4bbddae..2bdb5c2 100644
--- a/services/core/java/com/android/server/display/DisplayDevice.java
+++ b/services/core/java/com/android/server/display/DisplayDevice.java
@@ -306,6 +306,14 @@
     }
 
     /**
+     * Returns if the display should only mirror another display rather than showing other content
+     * until it is destroyed.
+     */
+    public boolean shouldOnlyMirror() {
+        return false;
+    }
+
+    /**
      * Sets the display layer stack while in a transaction.
      */
     public final void setLayerStackLocked(SurfaceControl.Transaction t, int layerStack,
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 0b633bd..bad5b8b 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -199,7 +199,6 @@
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Consumer;
 
-
 /**
  * Manages attached displays.
  * <p>
@@ -906,6 +905,16 @@
         }
     }
 
+    @VisibleForTesting
+    ContentObserver getSettingsObserver() {
+        return mSettingsObserver;
+    }
+
+    @VisibleForTesting
+    boolean shouldMirrorBuiltInDisplay() {
+        return mMirrorBuiltInDisplay;
+    }
+
     DisplayNotificationManager getDisplayNotificationManager() {
         return mDisplayNotificationManager;
     }
@@ -1230,11 +1239,6 @@
     }
 
     private void updateMirrorBuiltInDisplaySettingLocked() {
-        if (!mFlags.isDisplayContentModeManagementEnabled()) {
-            Slog.e(TAG, "MirrorBuiltInDisplay setting shouldn't be updated when the flag is off.");
-            return;
-        }
-
         synchronized (mSyncRoot) {
             ContentResolver resolver = mContext.getContentResolver();
             final boolean mirrorBuiltInDisplay = Settings.Secure.getIntForUser(resolver,
@@ -1243,6 +1247,9 @@
                 return;
             }
             mMirrorBuiltInDisplay = mirrorBuiltInDisplay;
+            if (mFlags.isDisplayContentModeManagementEnabled()) {
+                mLogicalDisplayMapper.forEachLocked(this::updateCanHostTasksIfNeededLocked);
+            }
         }
     }
 
@@ -2308,6 +2315,10 @@
         mDisplayBrightnesses.append(displayId,
                 new BrightnessPair(brightnessDefault, brightnessDefault));
 
+        if (mFlags.isDisplayContentModeManagementEnabled()) {
+            updateCanHostTasksIfNeededLocked(display);
+        }
+
         DisplayManagerGlobal.invalidateLocalDisplayInfoCaches();
     }
 
@@ -2630,6 +2641,12 @@
         }
     }
 
+    private void updateCanHostTasksIfNeededLocked(LogicalDisplay display) {
+        if (display.setCanHostTasksLocked(!mMirrorBuiltInDisplay)) {
+            sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED);
+        }
+    }
+
     private void recordTopInsetLocked(@Nullable LogicalDisplay d) {
         // We must only persist the inset after boot has completed, otherwise we will end up
         // overwriting the persisted value before the masking flag has been loaded from the
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 9387e9ede..2f82b2a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -521,6 +521,8 @@
             BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting,
             Runnable onBrightnessChangeRunnable, HighBrightnessModeMetadata hbmMetadata,
             boolean bootCompleted, DisplayManagerFlags flags) {
+        final Resources resources = context.getResources();
+
         mFlags = flags;
         mInjector = injector != null ? injector : new Injector();
         mClock = mInjector.getClock();
@@ -540,7 +542,9 @@
         mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
                 mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
                 () -> updatePowerState(), mDisplayId, mSensorManager);
-        mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
+        mDisplayStateController = new DisplayStateController(
+            mDisplayPowerProximityStateController,
+            resources.getBoolean(R.bool.config_skipScreenOffTransition));
         mTag = TAG + "[" + mDisplayId + "]";
         mThermalBrightnessThrottlingDataId =
                 logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
@@ -574,17 +578,14 @@
                 Settings.Global.getUriFor(Settings.Global.Wearable.BEDTIME_MODE),
                 false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
 
-        final Resources resources = context.getResources();
-
         // DOZE AND DIM SETTINGS
         mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness(
                 mDisplayDeviceConfig.getDefaultDozeBrightness());
         loadBrightnessRampRates();
         mSkipScreenOnBrightnessRamp = resources.getBoolean(
                 R.bool.config_skipScreenOnBrightnessRamp);
-        mDozeScaleFactor = context.getResources().getFraction(
-                R.fraction.config_screenAutoBrightnessDozeScaleFactor,
-                1, 1);
+        mDozeScaleFactor = resources.getFraction(
+                R.fraction.config_screenAutoBrightnessDozeScaleFactor, 1, 1);
 
         Runnable modeChangeCallback = () -> {
             sendUpdatePowerState();
@@ -2235,7 +2236,6 @@
                     setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_OFF);
                     blockScreenOff();
                     mWindowManagerPolicy.screenTurningOff(mDisplayId, mPendingScreenOffUnblocker);
-                    unblockScreenOff();
                 } else if (mPendingScreenOffUnblocker != null) {
                     // Abort doing the state change until screen off is unblocked.
                     return false;
diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
index f34d2cc..519763a 100644
--- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
+++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
@@ -217,8 +217,10 @@
 
         mExternalDisplayStatsService.onDisplayConnected(logicalDisplay);
 
-        if ((Build.IS_ENG || Build.IS_USERDEBUG)
-                && SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)) {
+        if (((Build.IS_ENG || Build.IS_USERDEBUG)
+                        && SystemProperties.getBoolean(ENABLE_ON_CONNECT, false))
+                || (mFlags.isDisplayContentModeManagementEnabled()
+                        && logicalDisplay.canHostTasksLocked())) {
             Slog.w(TAG, "External display is enabled by default, bypassing user consent.");
             mInjector.sendExternalDisplayEventLocked(logicalDisplay, EVENT_DISPLAY_CONNECTED);
             return;
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 1de9c95..f9d4137 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -17,6 +17,7 @@
 package com.android.server.display;
 
 import static com.android.server.display.DisplayDeviceInfo.TOUCH_NONE;
+import static com.android.server.display.layout.Layout.Display.POSITION_REAR;
 import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS;
 import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
 
@@ -227,6 +228,8 @@
      */
     private final boolean mIsAnisotropyCorrectionEnabled;
 
+    private boolean mCanHostTasks;
+
     LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice) {
         this(displayId, layerStack, primaryDisplayDevice, false, false);
     }
@@ -245,6 +248,7 @@
         mBaseDisplayInfo.thermalBrightnessThrottlingDataId = mThermalBrightnessThrottlingDataId;
         mIsAnisotropyCorrectionEnabled = isAnisotropyCorrectionEnabled;
         mAlwaysRotateDisplayDeviceEnabled = isAlwaysRotateDisplayDeviceEnabled;
+        mCanHostTasks = (mDisplayId == Display.DEFAULT_DISPLAY);
     }
 
     public void setDevicePositionLocked(int position) {
@@ -568,6 +572,7 @@
             mBaseDisplayInfo.layoutLimitedRefreshRate = mLayoutLimitedRefreshRate;
             mBaseDisplayInfo.thermalRefreshRateThrottling = mThermalRefreshRateThrottling;
             mBaseDisplayInfo.thermalBrightnessThrottlingDataId = mThermalBrightnessThrottlingDataId;
+            mBaseDisplayInfo.canHostTasks = mCanHostTasks;
 
             mPrimaryDisplayDeviceInfo = deviceInfo;
             mInfo.set(null);
@@ -927,6 +932,61 @@
         return handleLogicalDisplayChangedLocked;
     }
 
+    boolean canHostTasksLocked() {
+        return mCanHostTasks;
+    }
+
+    /**
+     * Sets whether the display can host tasks.
+     *
+     * @param canHostTasks Whether the display can host tasks according to the user's setting.
+     * @return Whether Display Manager should call sendDisplayEventIfEnabledLocked().
+     */
+    boolean setCanHostTasksLocked(boolean canHostTasks) {
+        canHostTasks = validateCanHostTasksLocked(canHostTasks);
+        if (mBaseDisplayInfo.canHostTasks == canHostTasks) {
+            return false;
+        }
+
+        mCanHostTasks = canHostTasks;
+        mBaseDisplayInfo.canHostTasks = canHostTasks;
+        mInfo.set(null);
+        return true;
+    }
+
+    /**
+     * Checks whether the display's ability to host tasks should be determined independently of the
+     * user's setting value. If so, returns the actual validated value based on the display's
+     * usage; otherwise, returns the user's setting value.
+     *
+     * @param canHostTasks Whether the display can host tasks according to the user's setting.
+     * @return Whether the display can actually host task after configuration.
+     */
+    private boolean validateCanHostTasksLocked(boolean canHostTasks) {
+        // The default display can always host tasks.
+        if (getDisplayIdLocked() == Display.DEFAULT_DISPLAY) {
+            return true;
+        }
+
+        // The display that should only mirror can never host tasks.
+        if (mPrimaryDisplayDevice.shouldOnlyMirror()) {
+            return false;
+        }
+
+        // The display that has its own content can always host tasks.
+        final boolean isRearDisplay = getDevicePositionLocked() == POSITION_REAR;
+        final boolean ownContent =
+                ((mPrimaryDisplayDevice.getDisplayDeviceInfoLocked().flags
+                        & DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY)
+                        != 0)
+                        || isRearDisplay;
+        if (ownContent) {
+            return true;
+        }
+
+        return canHostTasks;
+    }
+
     /**
      * Swap the underlying {@link DisplayDevice} with the specified LogicalDisplay.
      *
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index f14e452..558afd1 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -307,13 +307,14 @@
 
     private VirtualDisplayDevice removeVirtualDisplayDeviceLocked(IBinder appToken) {
         if (getFeatureFlags().isVirtualDisplayLimitEnabled()) {
-            int ownerUid = mOwnerUids.get(appToken);
-            int noOfDevices = mNoOfDevicesPerPackage.get(ownerUid, /* valueIfKeyNotFound= */ 0);
-            if (noOfDevices <= 1) {
-                mNoOfDevicesPerPackage.delete(ownerUid);
-                mOwnerUids.remove(appToken);
-            } else {
-                mNoOfDevicesPerPackage.put(ownerUid, noOfDevices - 1);
+            Integer ownerUid = mOwnerUids.remove(appToken);
+            if (ownerUid != null) {
+                int noOfDevices = mNoOfDevicesPerPackage.get(ownerUid, /* valueIfKeyNotFound= */ 0);
+                if (noOfDevices <= 1) {
+                    mNoOfDevicesPerPackage.delete(ownerUid);
+                } else {
+                    mNoOfDevicesPerPackage.put(ownerUid, noOfDevices - 1);
+                }
             }
         }
         return mVirtualDisplayDevices.remove(appToken);
@@ -500,6 +501,11 @@
             mPendingChanges = 0;
         }
 
+        @Override
+        public boolean shouldOnlyMirror() {
+            return mProjection != null || ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0);
+        }
+
         public void setSurfaceLocked(Surface surface) {
             if (!mStopped && mSurface != surface) {
                 if (mDisplayState == Display.STATE_ON
diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java
index 0b46e0f..f3b0799 100644
--- a/services/core/java/com/android/server/display/state/DisplayStateController.java
+++ b/services/core/java/com/android/server/display/state/DisplayStateController.java
@@ -31,14 +31,17 @@
  * clients about the changes
  */
 public class DisplayStateController {
-    private DisplayPowerProximityStateController mDisplayPowerProximityStateController;
+    private final DisplayPowerProximityStateController mDisplayPowerProximityStateController;
+    private final boolean mShouldSkipScreenOffTransition;
     private boolean mPerformScreenOffTransition = false;
     private int mDozeStateOverride = Display.STATE_UNKNOWN;
     private int mDozeStateOverrideReason = Display.STATE_REASON_UNKNOWN;
 
-    public DisplayStateController(DisplayPowerProximityStateController
-            displayPowerProximityStateController) {
+    public DisplayStateController(
+            DisplayPowerProximityStateController displayPowerProximityStateController,
+            boolean shouldSkipScreenOffTransition) {
         this.mDisplayPowerProximityStateController = displayPowerProximityStateController;
+        this.mShouldSkipScreenOffTransition = shouldSkipScreenOffTransition;
     }
 
     /**
@@ -65,7 +68,7 @@
         switch (displayPowerRequest.policy) {
             case DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF:
                 state = Display.STATE_OFF;
-                mPerformScreenOffTransition = true;
+                mPerformScreenOffTransition = !mShouldSkipScreenOffTransition;
                 break;
             case DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE:
                 if (mDozeStateOverride != Display.STATE_UNKNOWN) {
@@ -117,7 +120,8 @@
     public void dump(PrintWriter pw) {
         pw.println("DisplayStateController:");
         pw.println("-----------------------");
-        pw.println("  mPerformScreenOffTransition:" + mPerformScreenOffTransition);
+        pw.println("  mShouldSkipScreenOffTransition=" + mShouldSkipScreenOffTransition);
+        pw.println("  mPerformScreenOffTransition=" + mPerformScreenOffTransition);
         pw.println("  mDozeStateOverride=" + mDozeStateOverride);
 
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 7505c71..424102c 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -212,11 +212,11 @@
                     HdmiConfig.TIMEOUT_MS);
         }
 
-        launchRoutingControl(reason != HdmiControlService.INITIATED_BY_ENABLE_CEC &&
-                reason != HdmiControlService.INITIATED_BY_BOOT_UP);
         resetSelectRequestBuffer();
         launchDeviceDiscovery();
         startQueuedActions();
+        final boolean routingForBootup = reason != HdmiControlService.INITIATED_BY_ENABLE_CEC
+                && reason != HdmiControlService.INITIATED_BY_BOOT_UP;
         List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer
                 .getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE);
         if (bufferedActiveSource.isEmpty()) {
@@ -227,14 +227,8 @@
             addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() {
                 @Override
                 public void onComplete(int result) {
-                    if (!mService.getLocalActiveSource().isValid()
-                            && result != HdmiControlManager.RESULT_SUCCESS) {
-                        mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(
-                                getDeviceInfo().getLogicalAddress(),
-                                getDeviceInfo().getPhysicalAddress()));
-                        updateActiveSource(getDeviceInfo().getLogicalAddress(),
-                                getDeviceInfo().getPhysicalAddress(),
-                                "RequestActiveSourceAction#finishWithCallback()");
+                    if (result != HdmiControlManager.RESULT_SUCCESS) {
+                        launchRoutingControl(routingForBootup);
                     }
                 }
             }));
@@ -1384,8 +1378,7 @@
         } else {
             int activePath = mService.getPhysicalAddress();
             setActivePath(activePath);
-            if (!routingForBootup
-                    && !mDelayedMessageBuffer.isBuffered(Constants.MESSAGE_ACTIVE_SOURCE)) {
+            if (!mDelayedMessageBuffer.isBuffered(Constants.MESSAGE_ACTIVE_SOURCE)) {
                 mService.sendCecCommand(
                         HdmiCecMessageBuilder.buildActiveSource(
                                 getDeviceInfo().getLogicalAddress(), activePath));
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index 78c1045..4e1df76 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -112,9 +112,10 @@
     }
 
     @Override
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     public int openSession(HubEndpointInfo destination, String serviceDescriptor)
             throws RemoteException {
-        ContextHubServiceUtil.checkPermissions(mContext);
+        super.openSession_enforcePermission();
         if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered");
         int sessionId = mEndpointManager.reserveSessionId();
         EndpointInfo halEndpointInfo = ContextHubServiceUtil.convertHalEndpointInfo(destination);
@@ -139,8 +140,9 @@
     }
 
     @Override
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     public void closeSession(int sessionId, int reason) throws RemoteException {
-        ContextHubServiceUtil.checkPermissions(mContext);
+        super.closeSession_enforcePermission();
         if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered");
         try {
             mContextHubProxy.closeEndpointSession(sessionId, (byte) reason);
@@ -151,8 +153,9 @@
     }
 
     @Override
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     public void unregister() {
-        ContextHubServiceUtil.checkPermissions(mContext);
+        super.unregister_enforcePermission();
         mIsRegistered.set(false);
         try {
             mContextHubProxy.unregisterEndpoint(mHalEndpointInfo);
@@ -174,8 +177,9 @@
     }
 
     @Override
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     public void openSessionRequestComplete(int sessionId) {
-        ContextHubServiceUtil.checkPermissions(mContext);
+        super.openSessionRequestComplete_enforcePermission();
         synchronized (mOpenSessionLock) {
             try {
                 mContextHubProxy.endpointSessionOpenComplete(sessionId);
@@ -187,9 +191,10 @@
     }
 
     @Override
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     public void sendMessage(
             int sessionId, HubMessage message, IContextHubTransactionCallback callback) {
-        ContextHubServiceUtil.checkPermissions(mContext);
+        super.sendMessage_enforcePermission();
         Message halMessage = ContextHubServiceUtil.createHalMessage(message);
         synchronized (mOpenSessionLock) {
             if (!mActiveSessionIds.contains(sessionId)
@@ -227,8 +232,9 @@
     }
 
     @Override
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     public void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode) {
-        ContextHubServiceUtil.checkPermissions(mContext);
+        super.sendMessageDeliveryStatus_enforcePermission();
         MessageDeliveryStatus status = new MessageDeliveryStatus();
         status.messageSequenceNumber = messageSeqNumber;
         status.errorCode = errorCode;
diff --git a/services/core/java/com/android/server/location/fudger/LocationFudger.java b/services/core/java/com/android/server/location/fudger/LocationFudger.java
index 0da1514..bbd8aa1 100644
--- a/services/core/java/com/android/server/location/fudger/LocationFudger.java
+++ b/services/core/java/com/android/server/location/fudger/LocationFudger.java
@@ -16,6 +16,9 @@
 
 package com.android.server.location.fudger;
 
+import static com.android.internal.location.geometry.S2CellIdUtils.LAT_INDEX;
+import static com.android.internal.location.geometry.S2CellIdUtils.LNG_INDEX;
+
 import android.annotation.FlaggedApi;
 import android.annotation.Nullable;
 import android.location.Location;
@@ -184,31 +187,33 @@
         synchronized (this) {
             cacheCopy = mLocationFudgerCache;
         }
-
+        double[] coarsened = new double[] {0.0, 0.0};
         // TODO(b/381204398): To ensure a safe rollout, two algorithms co-exist. The first is the
         // new density-based algorithm, while the second is the traditional coarsening algorithm.
         // Once rollout is done, clean up the unused algorithm.
-        if (Flags.densityBasedCoarseLocations() && cacheCopy != null
-                && cacheCopy.hasDefaultValue()) {
-            int level = cacheCopy.getCoarseningLevel(latitude, longitude);
-            double[] center = snapToCenterOfS2Cell(latitude, longitude, level);
-            latitude = center[S2CellIdUtils.LAT_INDEX];
-            longitude = center[S2CellIdUtils.LNG_INDEX];
+        // The new algorithm is applied if and only if (1) the flag is on, (2) the cache has been
+        // set, and (3) the cache has successfully queried the provider for the default coarsening
+        // value.
+        if (Flags.populationDensityProvider() && Flags.densityBasedCoarseLocations()
+                && cacheCopy != null) {
+            if (cacheCopy.hasDefaultValue()) {
+                // New algorithm that snaps to the center of a S2 cell.
+                int level = cacheCopy.getCoarseningLevel(latitude, longitude);
+                coarsened = snapToCenterOfS2Cell(latitude, longitude, level);
+            } else {
+                // Try to fetch the default value. The answer won't come in time, but will be used
+                // for the next location to coarsen.
+                cacheCopy.fetchDefaultCoarseningLevelIfNeeded();
+                // Previous algorithm that snaps to a grid of width mAccuracyM.
+                coarsened = snapToGrid(latitude, longitude);
+            }
         } else {
-            // quantize location by snapping to a grid. this is the primary means of obfuscation. it
-            // gives nice consistent results and is very effective at hiding the true location (as
-            // long as you are not sitting on a grid boundary, which the random offsets mitigate).
-            //
-            // note that we quantize the latitude first, since the longitude quantization depends on
-            // the latitude value and so leaks information about the latitude
-            double latGranularity = metersToDegreesLatitude(mAccuracyM);
-            latitude = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity);
-            double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude);
-            longitude = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity);
+            // Previous algorithm that snaps to a grid of width mAccuracyM.
+            coarsened = snapToGrid(latitude, longitude);
         }
 
-        coarse.setLatitude(latitude);
-        coarse.setLongitude(longitude);
+        coarse.setLatitude(coarsened[LAT_INDEX]);
+        coarse.setLongitude(coarsened[LNG_INDEX]);
         coarse.setAccuracy(Math.max(mAccuracyM, coarse.getAccuracy()));
 
         synchronized (this) {
@@ -219,6 +224,21 @@
         return coarse;
     }
 
+    // quantize location by snapping to a grid. this is the primary means of obfuscation. it
+    // gives nice consistent results and is very effective at hiding the true location (as
+    // long as you are not sitting on a grid boundary, which the random offsets mitigate).
+    //
+    // note that we quantize the latitude first, since the longitude quantization depends on
+    // the latitude value and so leaks information about the latitude
+    private double[] snapToGrid(double latitude, double longitude) {
+        double[] center = new double[] {0.0, 0.0};
+        double latGranularity = metersToDegreesLatitude(mAccuracyM);
+        center[LAT_INDEX] = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity);
+        double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude);
+        center[LNG_INDEX] = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity);
+        return center;
+    }
+
     @VisibleForTesting
     protected double[] snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level) {
         long leafCell = S2CellIdUtils.fromLatLngDegrees(latDegrees, lngDegrees);
diff --git a/services/core/java/com/android/server/location/fudger/LocationFudgerCache.java b/services/core/java/com/android/server/location/fudger/LocationFudgerCache.java
index ce8bec8..19ec38c 100644
--- a/services/core/java/com/android/server/location/fudger/LocationFudgerCache.java
+++ b/services/core/java/com/android/server/location/fudger/LocationFudgerCache.java
@@ -76,6 +76,13 @@
         asyncFetchDefaultCoarseningLevel();
     }
 
+    /** If the cache's default coarsening value hasn't been set, asynchronously fetches it. */
+    public void fetchDefaultCoarseningLevelIfNeeded() {
+        if (!hasDefaultValue()) {
+            asyncFetchDefaultCoarseningLevel();
+        }
+    }
+
     /** Returns true if the cache has successfully received a default value from the provider. */
     public boolean hasDefaultValue() {
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 3e488bf..c810231 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -42,6 +42,7 @@
 import org.json.JSONObject;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
@@ -138,7 +139,7 @@
             try (
                     Cursor cursor = getCursorAfterQuerying(
                             mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME,
-                            getAllMediaProfileColumns(), selection, selectionArguments)
+                            getMediaProfileColumns(includeParams), selection, selectionArguments)
             ) {
                 int count = cursor.getCount();
                 if (count == 0) {
@@ -160,8 +161,8 @@
                 String packageName, boolean includeParams, UserHandle user) {
             String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
             String[] selectionArguments = {packageName};
-            return getPictureProfilesBasedOnConditions(getAllMediaProfileColumns(), selection,
-                    selectionArguments);
+            return getPictureProfilesBasedOnConditions(getMediaProfileColumns(includeParams),
+                    selection, selectionArguments);
         }
 
         @Override
@@ -259,7 +260,7 @@
             try (
                     Cursor cursor = getCursorAfterQuerying(
                             mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
-                            getAllMediaProfileColumns(), selection, selectionArguments)
+                            getMediaProfileColumns(includeParams), selection, selectionArguments)
             ) {
                 int count = cursor.getCount();
                 if (count == 0) {
@@ -281,8 +282,8 @@
                 String packageName, boolean includeParams, UserHandle user) {
             String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
             String[] selectionArguments = {packageName};
-            return getSoundProfilesBasedOnConditions(getAllMediaProfileColumns(), selection,
-                    selectionArguments);
+            return getSoundProfilesBasedOnConditions(getMediaProfileColumns(includeParams),
+                    selection, selectionArguments);
         }
 
         @Override
@@ -406,15 +407,18 @@
             return values;
         }
 
-        private String[] getAllMediaProfileColumns() {
-            return new String[]{
+        private String[] getMediaProfileColumns(boolean includeParams) {
+            ArrayList<String> columns = new ArrayList<>(Arrays.asList(
                     BaseParameters.PARAMETER_ID,
                     BaseParameters.PARAMETER_TYPE,
                     BaseParameters.PARAMETER_NAME,
                     BaseParameters.PARAMETER_INPUT_ID,
-                    BaseParameters.PARAMETER_PACKAGE,
-                    mMediaQualityDbHelper.SETTINGS
-            };
+                    BaseParameters.PARAMETER_PACKAGE)
+            );
+            if (includeParams) {
+                columns.add(mMediaQualityDbHelper.SETTINGS);
+            }
+            return columns.toArray(new String[0]);
         }
 
         private PictureProfile getPictureProfileWithTempIdFromCursor(Cursor cursor) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 504c298..c6d7fc7 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -6145,7 +6145,7 @@
         }
 
         private void enforcePolicyAccess(int uid, String method) {
-            if (PERMISSION_GRANTED == getContext().checkCallingPermission(
+            if (PERMISSION_GRANTED == getContext().checkCallingOrSelfPermission(
                     android.Manifest.permission.MANAGE_NOTIFICATIONS)) {
                 return;
             }
@@ -6176,7 +6176,7 @@
         }
 
         private void enforcePolicyAccess(String pkg, String method) {
-            if (PERMISSION_GRANTED == getContext().checkCallingPermission(
+            if (PERMISSION_GRANTED == getContext().checkCallingOrSelfPermission(
                     android.Manifest.permission.MANAGE_NOTIFICATIONS)) {
                 return;
             }
@@ -6985,6 +6985,7 @@
 
     protected void checkNotificationListenerAccess() {
         if (!isCallerSystemOrPhone()) {
+            // Safe to check calling permission as caller is already not system or phone
             getContext().enforceCallingPermission(
                     permission.MANAGE_NOTIFICATION_LISTENERS,
                     "Caller must hold " + permission.MANAGE_NOTIFICATION_LISTENERS);
diff --git a/services/core/java/com/android/server/pm/InstallDependencyHelper.java b/services/core/java/com/android/server/pm/InstallDependencyHelper.java
index 837adf0..81bb929 100644
--- a/services/core/java/com/android/server/pm/InstallDependencyHelper.java
+++ b/services/core/java/com/android/server/pm/InstallDependencyHelper.java
@@ -16,6 +16,7 @@
 
 package com.android.server.pm;
 
+import static android.app.role.RoleManager.ROLE_SYSTEM_DEPENDENCY_INSTALLER;
 import static android.content.pm.PackageInstaller.ACTION_INSTALL_DEPENDENCY;
 import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SHARED_LIBRARY;
 import static android.os.Process.SYSTEM_UID;
@@ -56,8 +57,6 @@
 public class InstallDependencyHelper {
     private static final String TAG = InstallDependencyHelper.class.getSimpleName();
     private static final boolean DEBUG = true;
-    private static final String ROLE_SYSTEM_DEPENDENCY_INSTALLER =
-            "android.app.role.SYSTEM_DEPENDENCY_INSTALLER";
     // The maximum amount of time to wait before the system unbinds from the verifier.
     private static final long UNBIND_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(6);
     private static final long REQUEST_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(1);
diff --git a/services/core/java/com/android/server/pm/RestrictionsSet.java b/services/core/java/com/android/server/pm/RestrictionsSet.java
index 0804769..38075c1 100644
--- a/services/core/java/com/android/server/pm/RestrictionsSet.java
+++ b/services/core/java/com/android/server/pm/RestrictionsSet.java
@@ -65,6 +65,7 @@
             throw new IllegalArgumentException("empty restriction bundle cannot be added.");
         }
         mUserRestrictions.put(userId, restrictions);
+        UserManager.invalidateUserRestriction();
     }
 
     /**
@@ -84,6 +85,7 @@
         } else {
             mUserRestrictions.delete(userId);
         }
+        UserManager.invalidateUserRestriction();
         return true;
     }
 
@@ -102,6 +104,9 @@
                 removed = true;
             }
         }
+        if (removed) {
+            UserManager.invalidateUserRestriction();
+        }
         return removed;
     }
 
@@ -129,6 +134,7 @@
                     i--;
                 }
             }
+            UserManager.invalidateUserRestriction();
         }
     }
 
@@ -192,6 +198,7 @@
     public boolean remove(@UserIdInt int userId) {
         boolean hasUserRestriction = mUserRestrictions.contains(userId);
         mUserRestrictions.remove(userId);
+        UserManager.invalidateUserRestriction();
         return hasUserRestriction;
     }
 
@@ -200,6 +207,7 @@
      */
     public void removeAllRestrictions() {
         mUserRestrictions.clear();
+        UserManager.invalidateUserRestriction();
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 066fce0..8249d65 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1113,6 +1113,7 @@
                 UserManager.invalidateUserPropertiesCache();
             }
             UserManager.invalidateCacheOnUserListChange();
+            UserManager.invalidateUserRestriction();
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 09feb18..6ab3059 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -216,12 +216,12 @@
 
     private static final Set<String> SENSORS_PERMISSIONS = new ArraySet<>();
     static {
+        SENSORS_PERMISSIONS.add(Manifest.permission.BODY_SENSORS);
+        SENSORS_PERMISSIONS.add(Manifest.permission.BODY_SENSORS_BACKGROUND);
+
         if (Flags.replaceBodySensorPermissionEnabled()) {
             SENSORS_PERMISSIONS.add(HealthPermissions.READ_HEART_RATE);
             SENSORS_PERMISSIONS.add(HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND);
-        } else {
-            SENSORS_PERMISSIONS.add(Manifest.permission.BODY_SENSORS);
-            SENSORS_PERMISSIONS.add(Manifest.permission.BODY_SENSORS_BACKGROUND);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 05bc69a..672eb4c 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -264,6 +264,16 @@
                 persistentDeviceId, mPermissionManagerServiceImpl::checkUidPermission);
     }
 
+    @Override
+    @Context.PermissionRequestState
+    public int getPermissionRequestState(@NonNull String packageName,
+            @NonNull String permissionName, int deviceId) {
+        Objects.requireNonNull(permissionName, "permission can't be null.");
+        Objects.requireNonNull(packageName, "package name can't be null.");
+        return mPermissionManagerServiceImpl.getPermissionRequestState(packageName, permissionName,
+                getPersistentDeviceId(deviceId));
+    }
+
     private String getPersistentDeviceId(int deviceId) {
         if (deviceId == Context.DEVICE_ID_DEFAULT) {
             return VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT;
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index ea71953..ca70bddc 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -1014,6 +1014,11 @@
     }
 
     @Override
+    public int getPermissionRequestState(String packageName, String permName, String deviceId) {
+        throw new IllegalStateException("getPermissionRequestState is not supported.");
+    }
+
+    @Override
     public Map<String, PermissionManager.PermissionState> getAllPermissionStates(
             @NonNull String packageName, @NonNull String deviceId, int userId) {
         throw new UnsupportedOperationException(
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java
index 754b141..b607832 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.PermissionGroupInfo;
 import android.content.pm.PermissionInfo;
@@ -407,6 +408,16 @@
     int checkUidPermission(int uid, String permName, String deviceId);
 
     /**
+     * Returns one of the permission state
+     * {@link Context.PermissionRequestState#PERMISSION_REQUEST_STATE_GRANTED},
+     * {@link Context.PermissionRequestState#PERMISSION_REQUEST_STATE_REQUESTABLE}, or
+     * {@link Context.PermissionRequestState#PERMISSION_REQUEST_STATE_UNREQUESTABLE}
+     *  for permission request permission flow.
+     */
+    int getPermissionRequestState(@NonNull String packageName, @NonNull String permName,
+            @NonNull String deviceId);
+
+    /**
      * Gets the permission states for requested package, persistent device and user.
      * <p>
      * <strong>Note: </strong>Default device permissions are not inherited in this API. Returns the
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java
index c18f856..ba5e97e 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java
@@ -247,6 +247,13 @@
     }
 
     @Override
+    public int getPermissionRequestState(String packageName, String permName, String deviceId) {
+        Log.i(LOG_TAG, "checkUidPermissionState(permName = " + permName + ", deviceId = "
+                + deviceId + ", packageName = " + packageName + ")");
+        return mService.getPermissionRequestState(packageName, permName, deviceId);
+    }
+
+    @Override
     public Map<String, PermissionState> getAllPermissionStates(@NonNull String packageName,
             @NonNull String deviceId, int userId) {
         Log.i(LOG_TAG,
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java
index 40139ba..008c14d 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java
@@ -319,6 +319,11 @@
     }
 
     @Override
+    public int getPermissionRequestState(String packageName, String permName, String deviceId) {
+        return mNewImplementation.getPermissionRequestState(packageName, permName, deviceId);
+    }
+
+    @Override
     public Map<String, PermissionState> getAllPermissionStates(@NonNull String packageName,
             @NonNull String deviceId, int userId) {
         return mNewImplementation.getAllPermissionStates(packageName, deviceId, userId);
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java
index 981d3d9..2a47f51 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java
@@ -345,6 +345,18 @@
         }
     }
 
+
+    @Override
+    public int getPermissionRequestState(String packageName, String permName, String deviceId) {
+        Trace.traceBegin(TRACE_TAG,
+                "TaggedTracingPermissionManagerServiceImpl#checkUidPermissionState");
+        try {
+            return mService.getPermissionRequestState(packageName, permName, deviceId);
+        } finally {
+            Trace.traceEnd(TRACE_TAG);
+        }
+    }
+
     @Override
     public Map<String, PermissionState> getAllPermissionStates(@NonNull String packageName,
             @NonNull String deviceId, int userId) {
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 7c7504d..aae7417 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -297,7 +297,11 @@
         mPowerHalVersion = 0;
         mUsesFmq = false;
         if (mPowerHal != null) {
-            mSupportInfo = getSupportInfo();
+            try {
+                mSupportInfo = getSupportInfo();
+            } catch (RemoteException e) {
+                throw new IllegalStateException("Could not contact PowerHAL!", e);
+            }
         }
         mDefaultCpuHeadroomCalculationWindowMillis =
                 new CpuHeadroomParamsInternal().calculationWindowMillis;
@@ -315,7 +319,7 @@
         }
     }
 
-    SupportInfo getSupportInfo() {
+    SupportInfo getSupportInfo() throws RemoteException {
         try {
             mPowerHalVersion = mPowerHal.getInterfaceVersion();
             if (mPowerHalVersion >= 6) {
@@ -326,9 +330,42 @@
         }
 
         SupportInfo supportInfo = new SupportInfo();
+        supportInfo.usesSessions = isHintSessionSupported();
+        // Global boosts & modes aren't currently relevant for HMS clients
+        supportInfo.boosts = 0;
+        supportInfo.modes = 0;
+        supportInfo.sessionHints = 0;
+        supportInfo.sessionModes = 0;
+        supportInfo.sessionTags = 0;
+
         supportInfo.headroom = new SupportInfo.HeadroomSupportInfo();
         supportInfo.headroom.isCpuSupported = false;
         supportInfo.headroom.isGpuSupported = false;
+
+        supportInfo.compositionData = new SupportInfo.CompositionDataSupportInfo();
+        if (isHintSessionSupported()) {
+            if (mPowerHalVersion == 4) {
+                // Assume we support the V4 hints & modes unless specified
+                // otherwise; this is to avoid breaking backwards compat
+                // since we historically just assumed they were.
+                supportInfo.sessionHints = 31; // first 5 bits are ones
+            }
+            if (mPowerHalVersion == 5) {
+                // Assume we support the V5 hints & modes unless specified
+                // otherwise; this is to avoid breaking backwards compat
+                // since we historically just assumed they were.
+
+                // Hal V5 has 8 modes, all of which it assumes are supported,
+                // so we represent that by having the first 8 bits set
+                supportInfo.sessionHints = 255; // first 8 bits are ones
+                // Hal V5 has 1 mode which it assumes is supported, so we
+                // represent that by having the first bit set
+                supportInfo.sessionModes = 1;
+                // Hal V5 has 5 tags, all of which it assumes are supported,
+                // so we represent that by having the first 5 bits set
+                supportInfo.sessionTags = 31;
+            }
+        }
         return supportInfo;
     }
 
@@ -1229,7 +1266,7 @@
                     @SessionTag int tag, SessionCreationConfig creationConfig,
                     SessionConfig config) {
             if (!isHintSessionSupported()) {
-                throw new UnsupportedOperationException("PowerHAL is not supported!");
+                throw new UnsupportedOperationException("PowerHintSessions are not supported!");
             }
 
             java.util.Objects.requireNonNull(token);
@@ -1425,12 +1462,6 @@
             removeChannelItem(callingTgid, callingUid);
         };
 
-        @Override
-        public long getHintSessionPreferredRate() {
-            return mHintSessionPreferredRate;
-        }
-
-        @Override
         public int getMaxGraphicsPipelineThreadsCount() {
             return MAX_GRAPHICS_PIPELINE_THREADS_COUNT;
         }
@@ -1621,13 +1652,24 @@
         }
 
         @Override
+        public IHintManager.HintManagerClientData
+                registerClient(@NonNull IHintManager.IHintManagerClient clientBinder) {
+            IHintManager.HintManagerClientData out = new IHintManager.HintManagerClientData();
+            out.preferredRateNanos = mHintSessionPreferredRate;
+            out.maxGraphicsPipelineThreads = getMaxGraphicsPipelineThreadsCount();
+            out.powerHalVersion = mPowerHalVersion;
+            out.supportInfo = mSupportInfo;
+            return out;
+        }
+
+        @Override
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
                 return;
             }
             pw.println("HintSessionPreferredRate: " + mHintSessionPreferredRate);
             pw.println("MaxGraphicsPipelineThreadsCount: " + MAX_GRAPHICS_PIPELINE_THREADS_COUNT);
-            pw.println("HAL Support: " + isHintSessionSupported());
+            pw.println("Hint Session Support: " + isHintSessionSupported());
             pw.println("Active Sessions:");
             synchronized (mLock) {
                 for (int i = 0; i < mActiveSessions.size(); i++) {
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index fe14f6b..95690cd 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -600,13 +600,7 @@
         private final int mFlags;
         private final Long mDefaultPowerStatsThrottlePeriod;
         private final Map<String, Long> mPowerStatsThrottlePeriods;
-
-        @VisibleForTesting
-        public BatteryStatsConfig() {
-            mFlags = 0;
-            mDefaultPowerStatsThrottlePeriod = 0L;
-            mPowerStatsThrottlePeriods = Map.of();
-        }
+        private final int mMaxHistorySizeBytes;
 
         private BatteryStatsConfig(Builder builder) {
             int flags = 0;
@@ -619,6 +613,7 @@
             mFlags = flags;
             mDefaultPowerStatsThrottlePeriod = builder.mDefaultPowerStatsThrottlePeriod;
             mPowerStatsThrottlePeriods = builder.mPowerStatsThrottlePeriods;
+            mMaxHistorySizeBytes = builder.mMaxHistorySizeBytes;
         }
 
         /**
@@ -648,18 +643,24 @@
                     mDefaultPowerStatsThrottlePeriod);
         }
 
+        public int getMaxHistorySizeBytes() {
+            return mMaxHistorySizeBytes;
+        }
+
         /**
          * Builder for BatteryStatsConfig
          */
         public static class Builder {
             private boolean mResetOnUnplugHighBatteryLevel;
             private boolean mResetOnUnplugAfterSignificantCharge;
-            public static final long DEFAULT_POWER_STATS_THROTTLE_PERIOD =
+            private static final long DEFAULT_POWER_STATS_THROTTLE_PERIOD =
                     TimeUnit.HOURS.toMillis(1);
-            public static final long DEFAULT_POWER_STATS_THROTTLE_PERIOD_CPU =
+            private static final long DEFAULT_POWER_STATS_THROTTLE_PERIOD_CPU =
                     TimeUnit.MINUTES.toMillis(1);
+            private static final int DEFAULT_MAX_HISTORY_SIZE = 4 * 1024 * 1024;
             private long mDefaultPowerStatsThrottlePeriod = DEFAULT_POWER_STATS_THROTTLE_PERIOD;
             private final Map<String, Long> mPowerStatsThrottlePeriods = new HashMap<>();
+            private int mMaxHistorySizeBytes = DEFAULT_MAX_HISTORY_SIZE;
 
             public Builder() {
                 mResetOnUnplugHighBatteryLevel = true;
@@ -712,6 +713,15 @@
                 mDefaultPowerStatsThrottlePeriod = periodMs;
                 return this;
             }
+
+            /**
+             * Sets the maximum amount of disk space, in bytes, that battery history can
+             * utilize. As this space fills up, the oldest history chunks must be expunged.
+             */
+            public Builder setMaxHistorySizeBytes(int maxHistorySizeBytes) {
+                mMaxHistorySizeBytes = maxHistorySizeBytes;
+                return this;
+            }
         }
     }
 
@@ -11425,7 +11435,7 @@
         }
 
         mHistory = new BatteryStatsHistory(null /* historyBuffer */, systemDir,
-                mConstants.MAX_HISTORY_FILES, mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator,
+                mConstants.MAX_HISTORY_SIZE, mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator,
                 mClock, mMonotonicClock, traceDelegate, eventLogger);
 
         mCpuPowerStatsCollector = new CpuPowerStatsCollector(mPowerStatsCollectorInjector);
@@ -11970,9 +11980,8 @@
         return mNextMaxDailyDeadlineMs;
     }
 
-    @GuardedBy("this")
     public int getHistoryTotalSize() {
-        return mConstants.MAX_HISTORY_BUFFER * mConstants.MAX_HISTORY_FILES;
+        return mHistory.getMaxHistorySize();
     }
 
     public int getHistoryUsedSize() {
@@ -16101,7 +16110,7 @@
                 = "battery_level_collection_delay_ms";
         public static final String KEY_PROC_STATE_CHANGE_COLLECTION_DELAY_MS =
                 "procstate_change_collection_delay_ms";
-        public static final String KEY_MAX_HISTORY_FILES = "max_history_files";
+        public static final String KEY_MAX_HISTORY_SIZE = "max_history_size";
         public static final String KEY_MAX_HISTORY_BUFFER_KB = "max_history_buffer_kb";
         public static final String KEY_BATTERY_CHARGED_DELAY_MS =
                 "battery_charged_delay_ms";
@@ -16152,9 +16161,7 @@
         private static final long DEFAULT_EXTERNAL_STATS_COLLECTION_RATE_LIMIT_MS = 600_000;
         private static final long DEFAULT_BATTERY_LEVEL_COLLECTION_DELAY_MS = 300_000;
         private static final long DEFAULT_PROC_STATE_CHANGE_COLLECTION_DELAY_MS = 60_000;
-        private static final int DEFAULT_MAX_HISTORY_FILES = 32;
         private static final int DEFAULT_MAX_HISTORY_BUFFER_KB = 128; /*Kilo Bytes*/
-        private static final int DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE = 64;
         private static final int DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB = 64; /*Kilo Bytes*/
         private static final int DEFAULT_BATTERY_CHARGED_DELAY_MS = 900000; /* 15 min */
         private static final int DEFAULT_BATTERY_CHARGING_ENFORCE_LEVEL = 90;
@@ -16176,7 +16183,7 @@
                 = DEFAULT_BATTERY_LEVEL_COLLECTION_DELAY_MS;
         public long PROC_STATE_CHANGE_COLLECTION_DELAY_MS =
                 DEFAULT_PROC_STATE_CHANGE_COLLECTION_DELAY_MS;
-        public int MAX_HISTORY_FILES;
+        public int MAX_HISTORY_SIZE;
         public int MAX_HISTORY_BUFFER; /*Bytes*/
         public int BATTERY_CHARGED_DELAY_MS = DEFAULT_BATTERY_CHARGED_DELAY_MS;
         public int BATTERY_CHARGING_ENFORCE_LEVEL = DEFAULT_BATTERY_CHARGING_ENFORCE_LEVEL;
@@ -16192,12 +16199,11 @@
         public Constants(Handler handler) {
             super(handler);
             if (isLowRamDevice()) {
-                MAX_HISTORY_FILES = DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE;
                 MAX_HISTORY_BUFFER = DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB * 1024;
             } else {
-                MAX_HISTORY_FILES = DEFAULT_MAX_HISTORY_FILES;
                 MAX_HISTORY_BUFFER = DEFAULT_MAX_HISTORY_BUFFER_KB * 1024;
             }
+            MAX_HISTORY_SIZE = mBatteryStatsConfig.getMaxHistorySizeBytes();
         }
 
         public void startObserving(ContentResolver resolver) {
@@ -16260,13 +16266,23 @@
                 PROC_STATE_CHANGE_COLLECTION_DELAY_MS = mParser.getLong(
                         KEY_PROC_STATE_CHANGE_COLLECTION_DELAY_MS,
                         DEFAULT_PROC_STATE_CHANGE_COLLECTION_DELAY_MS);
-                MAX_HISTORY_FILES = mParser.getInt(KEY_MAX_HISTORY_FILES,
-                        isLowRamDevice() ? DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE
-                                : DEFAULT_MAX_HISTORY_FILES);
                 MAX_HISTORY_BUFFER = mParser.getInt(KEY_MAX_HISTORY_BUFFER_KB,
                         isLowRamDevice() ? DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB
                                 : DEFAULT_MAX_HISTORY_BUFFER_KB)
                         * 1024;
+                int maxHistorySize = mParser.getInt(KEY_MAX_HISTORY_SIZE, -1);
+                if (maxHistorySize == -1) {
+                    // Process the deprecated max_history_files parameter for compatibility
+                    int maxHistoryFiles = mParser.getInt("max_history_files", -1);
+                    if (maxHistoryFiles != -1) {
+                        maxHistorySize = maxHistoryFiles * MAX_HISTORY_BUFFER;
+                    }
+                }
+                if (maxHistorySize == -1) {
+                    maxHistorySize = mBatteryStatsConfig.getMaxHistorySizeBytes();
+                }
+                MAX_HISTORY_SIZE = maxHistorySize;
+
                 final String perUidModemModel = mParser.getString(KEY_PER_UID_MODEM_POWER_MODEL,
                         "");
                 PER_UID_MODEM_MODEL = getPerUidModemModel(perUidModemModel);
@@ -16291,7 +16307,7 @@
          */
         @VisibleForTesting
         public void onChange() {
-            mHistory.setMaxHistoryFiles(MAX_HISTORY_FILES);
+            mHistory.setMaxHistorySize(MAX_HISTORY_SIZE);
             mHistory.setMaxHistoryBufferSize(MAX_HISTORY_BUFFER);
         }
 
@@ -16354,8 +16370,8 @@
             pw.println(BATTERY_LEVEL_COLLECTION_DELAY_MS);
             pw.print(KEY_PROC_STATE_CHANGE_COLLECTION_DELAY_MS); pw.print("=");
             pw.println(PROC_STATE_CHANGE_COLLECTION_DELAY_MS);
-            pw.print(KEY_MAX_HISTORY_FILES); pw.print("=");
-            pw.println(MAX_HISTORY_FILES);
+            pw.print(KEY_MAX_HISTORY_SIZE); pw.print("=");
+            pw.println(MAX_HISTORY_SIZE);
             pw.print(KEY_MAX_HISTORY_BUFFER_KB); pw.print("=");
             pw.println(MAX_HISTORY_BUFFER/1024);
             pw.print(KEY_BATTERY_CHARGED_DELAY_MS); pw.print("=");
diff --git a/services/core/java/com/android/server/security/advancedprotection/features/DisallowCellular2GAdvancedProtectionHook.java b/services/core/java/com/android/server/security/advancedprotection/features/DisallowCellular2GAdvancedProtectionHook.java
index f51c25d..acdea88 100644
--- a/services/core/java/com/android/server/security/advancedprotection/features/DisallowCellular2GAdvancedProtectionHook.java
+++ b/services/core/java/com/android/server/security/advancedprotection/features/DisallowCellular2GAdvancedProtectionHook.java
@@ -48,7 +48,7 @@
         mTelephonyManager = context.getSystemService(TelephonyManager.class);
         mSubscriptionManager = context.getSystemService(SubscriptionManager.class);
 
-        setPolicy(enabled);
+        onAdvancedProtectionChanged(enabled);
     }
 
     @NonNull
@@ -94,7 +94,8 @@
         return false;
     }
 
-    private void setPolicy(boolean enabled) {
+    @Override
+    public void onAdvancedProtectionChanged(boolean enabled) {
         if (enabled) {
             Slog.d(TAG, "Setting DISALLOW_CELLULAR_2G_GLOBALLY restriction");
             mDevicePolicyManager.addUserRestrictionGlobally(
@@ -105,21 +106,4 @@
                     ADVANCED_PROTECTION_SYSTEM_ENTITY, UserManager.DISALLOW_CELLULAR_2G);
         }
     }
-
-    @Override
-    public void onAdvancedProtectionChanged(boolean enabled) {
-        setPolicy(enabled);
-
-        // Leave 2G disabled even if APM is disabled.
-        if (!enabled) {
-            for (TelephonyManager telephonyManager : getActiveTelephonyManagers()) {
-                long oldAllowedTypes =
-                        telephonyManager.getAllowedNetworkTypesForReason(
-                                TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G);
-                long newAllowedTypes = oldAllowedTypes & ~TelephonyManager.NETWORK_CLASS_BITMASK_2G;
-                telephonyManager.setAllowedNetworkTypesForReason(
-                        TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G, newAllowedTypes);
-            }
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 348d326..64758eb 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8507,7 +8507,7 @@
         final boolean isFixedOrientationLetterboxAllowed = !getLaunchedFromBubble()
                 && (parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW
                         || parentWindowingMode == WINDOWING_MODE_FULLSCREEN
-                        || AppCompatCameraPolicy.shouldCameraCompatControlOrientation(this)
+                        || AppCompatCameraPolicy.isFreeformLetterboxingForCameraAllowed(this)
                         // When starting to switch between PiP and fullscreen, the task is pinned
                         // and the activity is fullscreen. But only allow to apply letterbox if the
                         // activity is exiting PiP because an entered PiP should fill the task.
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 27d32be..0aff1de 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -1269,7 +1269,8 @@
             // Checks if the caller can be shown in the given public display.
             int userId = UserHandle.getUserId(callingUid);
             int displayId = display.getDisplayId();
-            boolean allowed = mWindowManager.mUmInternal.isUserVisible(userId, displayId);
+            boolean allowed = userId == UserHandle.USER_SYSTEM
+                    || mWindowManager.mUmInternal.isUserVisible(userId, displayId);
             ProtoLog.d(WM_DEBUG_TASKS,
                     "Launch on display check: %s launch for userId=%d on displayId=%d",
                     (allowed ? "allow" : "disallow"), userId, displayId);
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index 8be66cc..9547e5cc 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -193,6 +193,17 @@
     }
 
     // TODO(b/369070416): have policies implement the same interface.
+    static boolean isFreeformLetterboxingForCameraAllowed(@NonNull ActivityRecord activity) {
+        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+        if (cameraPolicy == null) {
+            return false;
+        }
+        return cameraPolicy.mCameraCompatFreeformPolicy != null
+                        && cameraPolicy.mCameraCompatFreeformPolicy
+                                .isFreeformLetterboxingForCameraAllowed(activity);
+    }
+
+    // TODO(b/369070416): have policies implement the same interface.
     static boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) {
         final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
         if (cameraPolicy == null) {
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index a418324..0369a0f 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -285,7 +285,7 @@
         info.topActivityLetterboxAppWidth = TaskInfo.PROPERTY_VALUE_UNSET;
         info.topActivityLetterboxBounds = null;
         info.cameraCompatTaskInfo.freeformCameraCompatMode =
-                CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
+                CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_UNSPECIFIED;
         info.clearTopActivityFlags();
     }
 }
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e9e3c9e..1a7c6b7 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -275,12 +275,8 @@
             final boolean isOccluded = isKeyguardOccluded(window);
             if (!canAnimate) {
                 backType = BackNavigationInfo.TYPE_CALLBACK;
-            } else if ((window.getParent().getChildCount() > 1
-                    && window.getParent().getChildAt(0) != window)) {
-                // TODO Dialog window does not need to attach on activity, check
-                // window.mAttrs.type != TYPE_BASE_APPLICATION
-                // Are we the top window of our parent? If not, we are a window on top of the
-                // activity, we won't close the activity.
+            } else if (window.mAttrs.type != TYPE_BASE_APPLICATION) {
+                // The focus window belongs to an activity and it's not the base window.
                 backType = BackNavigationInfo.TYPE_DIALOG_CLOSE;
                 removedWindowContainer = window;
             } else if (hasTranslucentActivity(currentActivity, prevActivities)) {
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index ae65db4..f5bc9f0 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -21,6 +21,7 @@
 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_UNSPECIFIED;
 import static android.app.WindowConfiguration.WINDOW_CONFIG_APP_BOUNDS;
 import static android.app.WindowConfiguration.WINDOW_CONFIG_DISPLAY_ROTATION;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
@@ -205,6 +206,16 @@
         }
     }
 
+    /**
+     * Returns true if letterboxing should be allowed for camera apps, even if otherwise it isn't.
+     *
+     * <p>Camera compat is currently the only use-case of letterboxing for desktop windowing.
+     */
+    boolean isFreeformLetterboxingForCameraAllowed(@NonNull ActivityRecord activity) {
+        // Letterboxing is normally not allowed in desktop windowing.
+        return isCameraRunningAndWindowingModeEligible(activity);
+    }
+
     boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
         return isCameraRunningAndWindowingModeEligible(activity);
     }
@@ -225,7 +236,8 @@
     }
 
     boolean isInFreeformCameraCompatMode(@NonNull ActivityRecord activity) {
-        return getCameraCompatMode(activity) != CAMERA_COMPAT_FREEFORM_NONE;
+        return getCameraCompatMode(activity) != CAMERA_COMPAT_FREEFORM_UNSPECIFIED
+                && getCameraCompatMode(activity) != CAMERA_COMPAT_FREEFORM_NONE;
     }
 
     float getCameraCompatAspectRatio(@NonNull ActivityRecord activityRecord) {
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index b076aeb..4eaa11b 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -432,7 +432,8 @@
                 || !first.thermalRefreshRateThrottling.contentEquals(
                 second.thermalRefreshRateThrottling)
                 || !Objects.equals(first.thermalBrightnessThrottlingDataId,
-                second.thermalBrightnessThrottlingDataId)) {
+                second.thermalBrightnessThrottlingDataId)
+                || first.canHostTasks != second.canHostTasks) {
             diff |= DIFF_NOT_WM_DEFERRABLE;
         }
 
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 82699ea..01639cc 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -61,6 +61,7 @@
         "com_android_server_SystemServer.cpp",
         "com_android_server_tv_TvUinputBridge.cpp",
         "com_android_server_tv_TvInputHal.cpp",
+        "com_android_server_UsbAlsaDevice.cpp",
         "com_android_server_UsbAlsaJackDetector.cpp",
         "com_android_server_UsbAlsaMidiDevice.cpp",
         "com_android_server_UsbDeviceManager.cpp",
diff --git a/services/core/jni/com_android_server_UsbAlsaDevice.cpp b/services/core/jni/com_android_server_UsbAlsaDevice.cpp
new file mode 100644
index 0000000..166932f
--- /dev/null
+++ b/services/core/jni/com_android_server_UsbAlsaDevice.cpp
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "UsbAlsaDeviceJNI"
+
+#include <nativehelper/JNIPlatformHelp.h>
+#include <tinyalsa/asoundlib.h>
+
+#include <string>
+#include <vector>
+
+#include "jni.h"
+#include "utils/Log.h"
+
+static const std::vector<std::string> POSSIBLE_HARDWARE_VOLUME_MIXER_NAMES =
+        {"Headphone Playback Volume", "Headset Playback Volume", "PCM Playback Volume"};
+
+namespace android {
+
+static void android_server_UsbAlsaDevice_setVolume(JNIEnv* /*env*/, jobject /*thiz*/, jint card,
+                                                   float volume) {
+    ALOGD("%s(%d, %f)", __func__, card, volume);
+    struct mixer* alsaMixer = mixer_open(card);
+    if (alsaMixer == nullptr) {
+        ALOGW("%s(%d, %f) returned as no mixer is opened", __func__, card, volume);
+        return;
+    }
+    struct mixer_ctl* ctl = nullptr;
+    for (const auto& mixerName : POSSIBLE_HARDWARE_VOLUME_MIXER_NAMES) {
+        ctl = mixer_get_ctl_by_name(alsaMixer, mixerName.c_str());
+        if (ctl != nullptr) {
+            break;
+        }
+    }
+    if (ctl == nullptr) {
+        ALOGW("%s(%d, %f) returned as no volume mixer is found", __func__, card, volume);
+        return;
+    }
+    const unsigned int n = mixer_ctl_get_num_values(ctl);
+    for (unsigned int id = 0; id < n; id++) {
+        if (int error = mixer_ctl_set_percent(ctl, id, 100 * volume); error != 0) {
+            ALOGE("%s(%d, %f) failed, error=%d", __func__, card, volume, error);
+            return;
+        }
+    }
+    ALOGD("%s(%d, %f) succeed", __func__, card, volume);
+}
+
+static JNINativeMethod method_table[] = {
+        {"nativeSetVolume", "(IF)V", (void*)android_server_UsbAlsaDevice_setVolume},
+};
+
+int register_android_server_UsbAlsaDevice(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "com/android/server/usb/UsbAlsaDevice", method_table,
+                                    NELEM(method_table));
+}
+} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 09fd8d4..e3bd69c 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -33,6 +33,7 @@
 int register_android_server_HintManagerService(JNIEnv* env);
 int register_android_server_storage_AppFuse(JNIEnv* env);
 int register_android_server_SystemServer(JNIEnv* env);
+int register_android_server_UsbAlsaDevice(JNIEnv* env);
 int register_android_server_UsbAlsaJackDetector(JNIEnv* env);
 int register_android_server_UsbAlsaMidiDevice(JNIEnv* env);
 int register_android_server_UsbDeviceManager(JavaVM* vm, JNIEnv* env);
@@ -98,6 +99,7 @@
     register_android_server_InputManager(env);
     register_android_server_LightsService(env);
     register_android_server_UsbDeviceManager(vm, env);
+    register_android_server_UsbAlsaDevice(env);
     register_android_server_UsbAlsaJackDetector(env);
     register_android_server_UsbAlsaMidiDevice(env);
     register_android_server_UsbHostManager(env);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 29e0487..0eac4f2 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -109,6 +109,7 @@
 import com.android.internal.os.ApplicationSharedMemory;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.RuntimeInit;
+import com.android.internal.os.logging.MetricsLoggerWrapper;
 import com.android.internal.pm.RoSystemFeatures;
 import com.android.internal.policy.AttributeCache;
 import com.android.internal.protolog.ProtoLog;
@@ -1002,6 +1003,17 @@
             }
         });
 
+        // Register callback to report native memory metrics post GC cleanup
+        // for system_server
+        if (android.app.Flags.reportPostgcMemoryMetrics() &&
+            com.android.libcore.readonly.Flags.postCleanupApis()) {
+            VMRuntime.addPostCleanupCallback(new Runnable() {
+                @Override public void run() {
+                    MetricsLoggerWrapper.logPostGcMemorySnapshot();
+                }
+            });
+        }
+
         // Loop forever.
         Looper.loop();
         throw new RuntimeException("Main thread loop unexpectedly exited");
diff --git a/services/manifest_services.xml b/services/manifest_services.xml
deleted file mode 100644
index 9457205..0000000
--- a/services/manifest_services.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<manifest version="1.0" type="framework">
-    <hal format="aidl">
-        <name>android.frameworks.location.altitude</name>
-        <version>2</version>
-        <fqname>IAltitudeService/default</fqname>
-    </hal>
-    <hal format="aidl">
-        <name>android.frameworks.devicestate</name>
-        <version>1</version>
-        <fqname>IDeviceStateService/default</fqname>
-    </hal>
-</manifest>
diff --git a/services/manifest_services_android.frameworks.devicestate.xml b/services/manifest_services_android.frameworks.devicestate.xml
new file mode 100644
index 0000000..dc189ec
--- /dev/null
+++ b/services/manifest_services_android.frameworks.devicestate.xml
@@ -0,0 +1,7 @@
+<manifest version="1.0" type="framework">
+    <hal format="aidl">
+        <name>android.frameworks.devicestate</name>
+        <version>1</version>
+        <fqname>IDeviceStateService/default</fqname>
+    </hal>
+</manifest>
diff --git a/services/manifest_services_android.frameworks.location.xml b/services/manifest_services_android.frameworks.location.xml
new file mode 100644
index 0000000..114fe32
--- /dev/null
+++ b/services/manifest_services_android.frameworks.location.xml
@@ -0,0 +1,7 @@
+<manifest version="1.0" type="framework">
+    <hal format="aidl">
+        <name>android.frameworks.location.altitude</name>
+        <version>2</version>
+        <fqname>IAltitudeService/default</fqname>
+    </hal>
+</manifest>
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index 0b7438c..018cf20 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -464,6 +464,48 @@
         return size
     }
 
+    override fun getPermissionRequestState(
+        packageName: String,
+        permissionName: String,
+        deviceId: String
+    ): Int {
+        val uid = Binder.getCallingUid()
+        val result = context.checkPermission(permissionName, Binder.getCallingPid(), uid)
+        if (result == PackageManager.PERMISSION_GRANTED) {
+            return Context.PERMISSION_REQUEST_STATE_GRANTED
+        }
+
+        val appId = UserHandle.getAppId(uid)
+        val userId = UserHandle.getUserId(uid)
+        val packageState =
+                packageManagerLocal.withFilteredSnapshot(uid, userId).use {
+                    it.getPackageState(packageName)
+                } ?: return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE
+        val androidPackage = packageState.androidPackage
+                ?: return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE
+        if (appId != packageState.appId) {
+            return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE
+        }
+        val permission = service.getState {
+            with(policy) { getPermissions()[permissionName] }
+        }
+        if (permission == null || !permission.isRuntime) {
+            return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE
+        }
+        if (permissionName !in androidPackage.requestedPermissions) {
+            return Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE
+        }
+
+        val permissionFlags = service.getState {
+            getPermissionFlagsWithPolicy(appId, userId, permissionName, deviceId)
+        }
+        return if (permissionFlags.hasAnyBit(UNREQUESTABLE_MASK)) {
+            Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE
+        } else {
+            Context.PERMISSION_REQUEST_STATE_REQUESTABLE
+        }
+    }
+
     override fun checkUidPermission(uid: Int, permissionName: String, deviceId: String): Int {
         val userId = UserHandle.getUserId(uid)
         if (!userManagerInternal.exists(userId)) {
@@ -472,7 +514,7 @@
 
         // PackageManagerInternal.getPackage(int) already checks package visibility and enforces
         // that instant apps can't see shared UIDs. Note that on the contrary,
-        // Note that PackageManagerInternal.getPackage(String) doesn't perform any checks.
+        // PackageManagerInternal.getPackage(String) doesn't perform any checks.
         val androidPackage = packageManagerInternal.getPackage(uid)
         if (androidPackage != null) {
             // Note that PackageManagerInternal.getPackageStateInternal() is not filtered.
diff --git a/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java b/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java
index 5db6a8f..9117cc8 100644
--- a/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java
+++ b/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java
@@ -918,6 +918,30 @@
     }
 
     @Test
+    public void testOnUserAddedAndRemoved_nullUserInfo() throws Exception {
+        final Vpn vpn = createVpn(PRIMARY_USER.id);
+        final Set<Range<Integer>> initialRange = rangeSet(PRIMARY_USER_RANGE);
+        // Note since mVpnProfile is a Ikev2VpnProfile, this starts an IkeV2VpnRunner.
+        startLegacyVpn(vpn, mVpnProfile);
+        // Set an initial Uid range and mock the network agent
+        vpn.mNetworkCapabilities.setUids(initialRange);
+        vpn.mNetworkAgent = mMockNetworkAgent;
+
+        // Add the restricted user and then remove it immediately. So the getUserInfo() will return
+        // null for the given restricted user id.
+        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
+        doReturn(null).when(mUserManager).getUserInfo(RESTRICTED_PROFILE_A.id);
+        vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
+        // Expect no range change to the NetworkCapabilities.
+        assertEquals(initialRange, vpn.mNetworkCapabilities.getUids());
+
+        // Remove the restricted user
+        vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
+        // Expect no range change to the NetworkCapabilities.
+        assertEquals(initialRange, vpn.mNetworkCapabilities.getUids());
+    }
+
+    @Test
     public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller()
             throws Exception {
         mTestDeps.mIgnoreCallingUidChecks = false;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 724f083..f96294ed 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -30,6 +30,7 @@
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
+import static android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
 import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
@@ -88,6 +89,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.hardware.Sensor;
@@ -3830,6 +3832,96 @@
         assertThat(callback.receivedEvents()).isEmpty();
     }
 
+    @Test
+    public void testMirrorBuiltInDisplay_flagEnabled() {
+        when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(true);
+        Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0);
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        displayManager.systemReady(/* safeMode= */ false);
+        assertThat(displayManager.shouldMirrorBuiltInDisplay()).isFalse();
+
+        Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1);
+        final ContentObserver observer = displayManager.getSettingsObserver();
+        observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY));
+        assertThat(displayManager.shouldMirrorBuiltInDisplay()).isTrue();
+    }
+
+    @Test
+    public void testMirrorBuiltInDisplay_flagDisabled() {
+        when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(false);
+        Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0);
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        displayManager.systemReady(/* safeMode= */ false);
+        assertThat(displayManager.shouldMirrorBuiltInDisplay()).isFalse();
+
+        Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1);
+        final ContentObserver observer = displayManager.getSettingsObserver();
+        observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY));
+        assertThat(displayManager.shouldMirrorBuiltInDisplay()).isFalse();
+    }
+
+    @Test
+    public void testShouldNotNotifyDefaultDisplayChanges_whenMirrorBuiltInDisplayChanges() {
+        when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(true);
+        Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0);
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        displayManager.systemReady(/* safeMode= */ false);
+
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        displayManagerBinderService.registerCallbackWithEventMask(
+                callback, STANDARD_DISPLAY_EVENTS);
+        waitForIdleHandler(handler);
+
+        // Create a default display device
+        createFakeDisplayDevice(displayManager, new float[] {60f}, Display.TYPE_INTERNAL);
+
+        Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1);
+        final ContentObserver observer = displayManager.getSettingsObserver();
+        observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY));
+        waitForIdleHandler(handler);
+
+        assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_CHANGED);
+    }
+
+    @Test
+    public void testShouldNotifyNonDefaultDisplayChanges_whenMirrorBuiltInDisplayChanges() {
+        when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(true);
+        Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0);
+
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        displayManager.systemReady(/* safeMode= */ false);
+
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        Handler handler = displayManager.getDisplayHandler();
+        waitForIdleHandler(handler);
+
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+        displayManagerBinderService.registerCallbackWithEventMask(
+                callback, STANDARD_DISPLAY_EVENTS);
+        waitForIdleHandler(handler);
+
+        // Create a default display device
+        createFakeDisplayDevice(displayManager, new float[] {60f}, Display.TYPE_INTERNAL);
+        // Create a non-default display device
+        createFakeDisplayDevice(displayManager, new float[] {60f}, Display.TYPE_EXTERNAL);
+
+        Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1);
+        final ContentObserver observer = displayManager.getSettingsObserver();
+        observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY));
+        waitForIdleHandler(handler);
+
+        assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED);
+    }
+
     private void initDisplayPowerController(DisplayManagerInternal localService) {
         localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() {
             @Override
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
index 241dc10..1a0ab25 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
@@ -609,4 +609,69 @@
         DisplayInfo info = mLogicalDisplay.getDisplayInfoLocked();
         assertArrayEquals(appSupportedModes, info.appsSupportedModes);
     }
+
+    @Test
+    public void testSetCanHostTasks_defaultDisplay() {
+        mLogicalDisplay = new LogicalDisplay(Display.DEFAULT_DISPLAY, LAYER_STACK, mDisplayDevice);
+        assertTrue(mLogicalDisplay.canHostTasksLocked());
+
+        mLogicalDisplay.setCanHostTasksLocked(true);
+        assertTrue(mLogicalDisplay.canHostTasksLocked());
+
+        mLogicalDisplay.setCanHostTasksLocked(false);
+        assertTrue(mLogicalDisplay.canHostTasksLocked());
+    }
+
+    @Test
+    public void testSetCanHostTasks_nonDefaultNormalDisplay() {
+        mLogicalDisplay =
+                new LogicalDisplay(Display.DEFAULT_DISPLAY + 1, LAYER_STACK, mDisplayDevice);
+
+        mLogicalDisplay.setCanHostTasksLocked(true);
+        assertTrue(mLogicalDisplay.canHostTasksLocked());
+
+        mLogicalDisplay.setCanHostTasksLocked(false);
+        assertFalse(mLogicalDisplay.canHostTasksLocked());
+    }
+
+    @Test
+    public void testSetCanHostTasks_nonDefaultVirtualMirrorDisplay() {
+        mDisplayDeviceInfo.type = Display.TYPE_VIRTUAL;
+        when(mDisplayDevice.shouldOnlyMirror()).thenReturn(true);
+        mLogicalDisplay =
+                new LogicalDisplay(Display.DEFAULT_DISPLAY + 1, LAYER_STACK, mDisplayDevice);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
+
+        mLogicalDisplay.setCanHostTasksLocked(true);
+        assertFalse(mLogicalDisplay.canHostTasksLocked());
+
+        mLogicalDisplay.setCanHostTasksLocked(false);
+        assertFalse(mLogicalDisplay.canHostTasksLocked());
+    }
+
+    @Test
+    public void testSetCanHostTasks_nonDefaultRearDisplay() {
+        mLogicalDisplay =
+                new LogicalDisplay(Display.DEFAULT_DISPLAY + 1, LAYER_STACK, mDisplayDevice);
+        mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_REAR);
+
+        mLogicalDisplay.setCanHostTasksLocked(true);
+        assertTrue(mLogicalDisplay.canHostTasksLocked());
+
+        mLogicalDisplay.setCanHostTasksLocked(false);
+        assertTrue(mLogicalDisplay.canHostTasksLocked());
+    }
+
+    @Test
+    public void testSetCanHostTasks_nonDefaultOwnContentOnly() {
+        mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY;
+        mLogicalDisplay =
+                new LogicalDisplay(Display.DEFAULT_DISPLAY + 1, LAYER_STACK, mDisplayDevice);
+
+        mLogicalDisplay.setCanHostTasksLocked(true);
+        assertTrue(mLogicalDisplay.canHostTasksLocked());
+
+        mLogicalDisplay.setCanHostTasksLocked(false);
+        assertTrue(mLogicalDisplay.canHostTasksLocked());
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
index fc4cc25..f0a77bb 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
@@ -52,7 +52,9 @@
     @Before
     public void before() {
         MockitoAnnotations.initMocks(this);
-        mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
+        final boolean shouldSkipScreenOffTransition = false;
+        mDisplayStateController = new DisplayStateController(
+                mDisplayPowerProximityStateController, /* shouldSkipScreenOffTransition= */ false);
     }
 
     @Test
@@ -236,6 +238,38 @@
         assertTrue(Display.STATE_REASON_OFFLOAD == stateAndReason.second);
     }
 
+    @Test
+    public void shouldPerformScreenOffTransition_whenRequestedOffAndNotConfiguredToSkip_true() {
+        mDisplayStateController = new DisplayStateController(
+                mDisplayPowerProximityStateController, /* shouldSkipScreenOffTransition= */ false);
+        when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn(
+                false);
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+
+        displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF;
+        displayPowerRequest.policyReason = Display.STATE_REASON_KEY;
+        mDisplayStateController.updateDisplayState(
+                displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
+        assertEquals(true, mDisplayStateController.shouldPerformScreenOffTransition());
+    }
+
+    @Test
+    public void shouldPerformScreenOffTransition_whenRequestedOffAndConfiguredToSkip_false() {
+        mDisplayStateController = new DisplayStateController(
+                mDisplayPowerProximityStateController, /* shouldSkipScreenOffTransition= */ true);
+        when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn(
+                false);
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+
+        displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF;
+        displayPowerRequest.policyReason = Display.STATE_REASON_KEY;
+        mDisplayStateController.updateDisplayState(
+                displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
+        assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
+    }
+
     private void validDisplayState(int policy, int displayState, boolean isEnabled,
             boolean isInTransition) {
         DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java
index 3b6c86e..0c92c10 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java
@@ -29,15 +29,20 @@
 import android.app.job.JobParameters;
 import android.net.Uri;
 import android.os.Parcel;
-import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.platform.test.flag.junit.SetFlagsRule;
 
+import libcore.junit.util.compat.CoreCompatChangeRule;
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.mockito.Mock;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
@@ -47,7 +52,10 @@
     private static final int TEST_JOB_ID_1 = 123;
     private static final String TEST_NAMESPACE = "TEST_NAMESPACE";
     private static final String TEST_DEBUG_STOP_REASON = "TEST_DEBUG_STOP_REASON";
-    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Rule
+    public TestRule compatChangeRule = new CoreCompatChangeRule();
 
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@@ -129,9 +137,10 @@
     }
 
     /** Test to verify that the JobParameters Cleaner is disabled */
-    @RequiresFlagsEnabled(FLAG_HANDLE_ABANDONED_JOBS)
     @Test
-    public void testCleanerWithLeakedJobCleanerDisabled_flagHandleAbandonedJobs() {
+    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
+    @DisableCompatChanges({JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS})
+    public void testCleanerWithLeakedNoJobCleaner_EnableFlagDisableCompatHandleAbandonedJobs() {
         // Inject real JobCallbackCleanup
         JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel);
 
@@ -150,4 +159,31 @@
         assertThat(jobParameters.getCleanable()).isNull();
         assertThat(jobParameters.getJobCleanupCallback()).isNull();
     }
+
+    /**
+     * Test to verify that the JobParameters Cleaner is not enabled
+     * when the compat change is enabled and the flag is enabled
+     */
+    @Test
+    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
+    @EnableCompatChanges({JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS})
+    public void testCleanerWithLeakedNoJobCleaner_EnableFlagEnableCompatHandleAbandonedJobs() {
+        // Inject real JobCallbackCleanup
+        JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel);
+
+        // Enable the cleaner
+        jobParameters.enableCleaner();
+
+        // Verify the cleaner is not enabled
+        assertThat(jobParameters.getCleanable()).isNull();
+        assertThat(jobParameters.getJobCleanupCallback()).isNull();
+
+        // Disable the cleaner
+        jobParameters.disableCleaner();
+
+        // Verify the cleaner is disabled
+        assertThat(jobParameters.getCleanable()).isNull();
+        assertThat(jobParameters.getJobCleanupCallback()).isNull();
+    }
+
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index 1e7a4f6..8c09f26 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -53,6 +53,7 @@
 
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.AppGlobals;
 import android.app.IActivityManager;
 import android.app.UiModeManager;
 import android.app.job.JobInfo;
@@ -60,6 +61,7 @@
 import android.app.job.JobScheduler;
 import android.app.job.JobWorkItem;
 import android.app.usage.UsageStatsManagerInternal;
+import android.compat.testing.PlatformCompatChangeRule;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -79,7 +81,6 @@
 import android.os.Looper;
 import android.os.Process;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.WorkSource;
 import android.os.WorkSource.WorkChain;
@@ -105,10 +106,14 @@
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.usage.AppStandbyInternal;
 
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
@@ -120,6 +125,7 @@
 import java.time.ZoneOffset;
 
 public class JobSchedulerServiceTest {
+    private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
     private static final String TAG = JobSchedulerServiceTest.class.getSimpleName();
     private static final int TEST_UID = 10123;
 
@@ -141,8 +147,13 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
     private ChargingPolicyChangeListener mChargingPolicyChangeListener;
 
+    private int mSourceUid;
+
     private class TestJobSchedulerService extends JobSchedulerService {
         TestJobSchedulerService(Context context) {
             super(context);
@@ -157,7 +168,6 @@
                 .strictness(Strictness.LENIENT)
                 .mockStatic(LocalServices.class)
                 .mockStatic(PermissionChecker.class)
-                .mockStatic(ServiceManager.class)
                 .startMocking();
 
         // Called in JobSchedulerService constructor.
@@ -226,6 +236,7 @@
         verify(mBatteryManagerInternal).registerChargingPolicyChangeListener(
                 chargingPolicyChangeListenerCaptor.capture());
         mChargingPolicyChangeListener = chargingPolicyChangeListenerCaptor.getValue();
+        mSourceUid = AppGlobals.getPackageManager().getPackageUid(SOURCE_PACKAGE, 0, 0);
     }
 
     @After
@@ -1063,6 +1074,7 @@
      */
     @Test
     @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
+    @DisableCompatChanges({JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS})
     public void testGetRescheduleJobForFailure_abandonedJob() {
         final long nowElapsed = sElapsedRealtimeClock.millis();
         final long initialBackoffMs = MINUTE_IN_MILLIS;
@@ -1074,6 +1086,9 @@
         assertEquals(JobStatus.NO_EARLIEST_RUNTIME, originalJob.getEarliestRunTime());
         assertEquals(JobStatus.NO_LATEST_RUNTIME, originalJob.getLatestRunTimeElapsed());
 
+        spyOn(originalJob);
+        doReturn(mSourceUid).when(originalJob).getSourceUid();
+
         // failure = 1, systemStop = 0, abandoned = 1
         JobStatus rescheduledJob = mService.getRescheduleJobForFailureLocked(originalJob,
                 JobParameters.STOP_REASON_DEVICE_STATE,
@@ -1081,6 +1096,8 @@
         assertEquals(nowElapsed + initialBackoffMs, rescheduledJob.getEarliestRunTime());
         assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed());
 
+        spyOn(rescheduledJob);
+        doReturn(mSourceUid).when(rescheduledJob).getSourceUid();
         // failure = 2, systemstop = 0, abandoned = 2
         rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob,
                 JobParameters.STOP_REASON_DEVICE_STATE,
@@ -1126,6 +1143,44 @@
     }
 
     /**
+     * Confirm that {@link JobSchedulerService#shouldUseAggressiveBackoff(int, int)} returns true
+     * when the number of abandoned jobs is greater than the threshold.
+     */
+    @Test
+    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
+    @DisableCompatChanges({JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS})
+    public void testGetRescheduleJobForFailure_EnableFlagDisableCompatCheckAggressiveBackoff() {
+        assertFalse(mService.shouldUseAggressiveBackoff(
+                        mService.mConstants.ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF - 1,
+                        mSourceUid));
+        assertFalse(mService.shouldUseAggressiveBackoff(
+                        mService.mConstants.ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF,
+                        mSourceUid));
+        assertTrue(mService.shouldUseAggressiveBackoff(
+                        mService.mConstants.ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF + 1,
+                        mSourceUid));
+    }
+
+    /**
+     * Confirm that {@link JobSchedulerService#shouldUseAggressiveBackoff(int, int)} returns false
+     * always when the compat change is enabled and the flag is enabled.
+     */
+    @Test
+    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
+    @EnableCompatChanges({JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS})
+    public void testGetRescheduleJobForFailure_EnableFlagEnableCompatCheckAggressiveBackoff() {
+        assertFalse(mService.shouldUseAggressiveBackoff(
+                        mService.mConstants.ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF - 1,
+                        mSourceUid));
+        assertFalse(mService.shouldUseAggressiveBackoff(
+                        mService.mConstants.ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF,
+                        mSourceUid));
+        assertFalse(mService.shouldUseAggressiveBackoff(
+                        mService.mConstants.ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF + 1,
+                        mSourceUid));
+    }
+
+    /**
      * Confirm that
      * {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int, int)}
      * returns a job that is correctly marked as demoted by the user.
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java
index 8c66fd0..904545b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java
@@ -31,6 +31,7 @@
 
 import android.app.AppGlobals;
 import android.app.job.JobParameters;
+import android.compat.testing.PlatformCompatChangeRule;
 import android.content.Context;
 import android.os.Looper;
 import android.os.PowerManager;
@@ -43,11 +44,15 @@
 import com.android.server.job.JobServiceContext.JobCallback;
 import com.android.server.job.controllers.JobStatus;
 
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoSession;
@@ -58,11 +63,14 @@
 import java.time.ZoneOffset;
 
 public class JobServiceContextTest {
+    private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
     private static final String TAG = JobServiceContextTest.class.getSimpleName();
     @ClassRule
     public static final SetFlagsRule.ClassRule mSetFlagsClassRule = new SetFlagsRule.ClassRule();
     @Rule
     public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule();
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
     @Mock
     private JobSchedulerService mMockJobSchedulerService;
     @Mock
@@ -86,13 +94,13 @@
     private MockitoSession mMockingSession;
     private JobServiceContext mJobServiceContext;
     private Object mLock;
+    private int mSourceUid;
 
     @Before
     public void setUp() throws Exception {
         mMockingSession =
                 mockitoSession()
                         .initMocks(this)
-                        .mockStatic(AppGlobals.class)
                         .strictness(Strictness.LENIENT)
                         .startMocking();
         JobSchedulerService.sElapsedRealtimeClock =
@@ -111,6 +119,7 @@
                         mMockLooper);
         spyOn(mJobServiceContext);
         mJobServiceContext.setJobParamsLockedForTest(mMockJobParameters);
+        mSourceUid = AppGlobals.getPackageManager().getPackageUid(SOURCE_PACKAGE, 0, 0);
     }
 
     @After
@@ -130,11 +139,14 @@
     }
 
     /**
-     * Test that Abandoned jobs that are timed out are stopped with the correct stop reason
+     * Test that with the compat change disabled and the flag enabled, abandoned
+     * jobs that are timed out are stopped with the correct stop reason and the
+     * job is marked as abandoned.
      */
     @Test
     @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
-    public void testJobServiceContext_TimeoutAbandonedJob() {
+    @DisableCompatChanges({JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS})
+    public void testJobServiceContext_TimeoutAbandonedJob_EnableFlagDisableCompat() {
         mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
         ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
         doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture());
@@ -143,6 +155,7 @@
         mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED);
 
         mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus);
+        doReturn(mSourceUid).when(mMockJobStatus).getSourceUid();
         doReturn(true).when(mMockJobStatus).isAbandoned();
         mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
 
@@ -158,11 +171,14 @@
     }
 
     /**
-     * Test that non-abandoned jobs that are timed out are stopped with the correct stop reason
+     * Test that with the compat change enabled and the flag enabled, abandoned
+     * jobs that are timed out are stopped with the correct stop reason and the
+     * job is not marked as abandoned.
      */
     @Test
     @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
-    public void testJobServiceContext_TimeoutNoAbandonedJob() {
+    @EnableCompatChanges({JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS})
+    public void testJobServiceContext_TimeoutAbandonedJob_EnableFlagEnableCompat() {
         mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
         ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
         doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture());
@@ -171,7 +187,8 @@
         mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED);
 
         mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus);
-        doReturn(false).when(mMockJobStatus).isAbandoned();
+        doReturn(mSourceUid).when(mMockJobStatus).getSourceUid();
+        doReturn(true).when(mMockJobStatus).isAbandoned();
         mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
 
         mJobServiceContext.handleOpTimeoutLocked();
@@ -186,12 +203,14 @@
     }
 
     /**
-     * Test that abandoned jobs that are timed out while the flag is disabled
-     * are stopped with the correct stop reason
+     * Test that with the compat change disabled and the flag disabled, abandoned
+     * jobs that are timed out are stopped with the correct stop reason and the
+     * job is not marked as abandoned.
      */
     @Test
     @DisableFlags(FLAG_HANDLE_ABANDONED_JOBS)
-    public void testJobServiceContext_TimeoutAbandonedJob_flagHandleAbandonedJobsDisabled() {
+    @DisableCompatChanges({JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS})
+    public void testJobServiceContext_TimeoutAbandonedJob_DisableFlagDisableCompat() {
         mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
         ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
         doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture());
@@ -201,6 +220,39 @@
 
         mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus);
         doReturn(true).when(mMockJobStatus).isAbandoned();
+        doReturn(mSourceUid).when(mMockJobStatus).getSourceUid();
+        mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
+
+        synchronized (mLock) {
+            mJobServiceContext.handleOpTimeoutLocked();
+        }
+
+        String stopMessage = captor.getValue();
+        assertEquals("timeout while executing", stopMessage);
+        verify(mMockJobParameters)
+                .setStopReason(
+                        JobParameters.STOP_REASON_TIMEOUT,
+                        JobParameters.INTERNAL_STOP_REASON_TIMEOUT,
+                        "client timed out");
+    }
+
+    /**
+     * Test that non-abandoned jobs that are timed out are stopped with the correct stop reason
+     */
+    @Test
+    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
+    @DisableCompatChanges({JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS})
+    public void testJobServiceContext_TimeoutNoAbandonedJob() {
+        mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
+        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+        synchronized (mLock) {
+            doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture());
+        }
+        advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes
+        mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED);
+
+        mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus);
+        doReturn(false).when(mMockJobStatus).isAbandoned();
         mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
 
         mJobServiceContext.handleOpTimeoutLocked();
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerCacheTest.java b/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerCacheTest.java
index 6b7eda2..c89048a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerCacheTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerCacheTest.java
@@ -280,6 +280,51 @@
                 eq(POINT_IN_TIMES_SQUARE[1]), eq(numAdditionalCells), any());
     }
 
+    @Test
+    public void fetchDefaultCoarseningLevelIfNeeded_withDefaultValue_doesNotQueryProvider()
+            throws RemoteException {
+        // Arrange.
+        ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+        LocationFudgerCache cache = new LocationFudgerCache(provider);
+
+        ArgumentCaptor<IS2LevelCallback> argumentCaptor = ArgumentCaptor.forClass(
+                IS2LevelCallback.class);
+        verify(provider, times(1)).getDefaultCoarseningLevel(argumentCaptor.capture());
+
+        IS2LevelCallback cb = argumentCaptor.getValue();
+        cb.onResult(10);
+
+        assertThat(cache.hasDefaultValue()).isTrue();
+
+        // Act.
+        cache.fetchDefaultCoarseningLevelIfNeeded();
+
+        // Assert. The method is not called again.
+        verify(provider, times(1)).getDefaultCoarseningLevel(any());
+    }
+
+    @Test
+    public void fetchDefaultCoarseningLevelIfNeeded_withoutDefaultValue_doesQueryProvider()
+            throws RemoteException {
+        // Arrange.
+        ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+        LocationFudgerCache cache = new LocationFudgerCache(provider);
+
+        ArgumentCaptor<IS2LevelCallback> argumentCaptor = ArgumentCaptor.forClass(
+                IS2LevelCallback.class);
+        verify(provider, times(1)).getDefaultCoarseningLevel(argumentCaptor.capture());
+
+        IS2LevelCallback cb = argumentCaptor.getValue();
+        cb.onError();
+
+        assertThat(cache.hasDefaultValue()).isFalse();
+
+        // Act.
+        cache.fetchDefaultCoarseningLevelIfNeeded();
+
+        // Assert. The method is called again.
+        verify(provider, times(2)).getDefaultCoarseningLevel(any());
+    }
 
     @Test
     public void locationFudgerCache_canContainUpToMaxSizeItems() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java
index 835705d..2e4652e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java
@@ -176,8 +176,28 @@
     }
 
     @Test
-    public void testDensityBasedCoarsening_ifFeatureIsDisabled_cacheIsNotUsed() {
+    public void testDensityBasedCoarsening_ifAnyFlagIsOff1_cacheIsNotUsed() {
+        // This feature requires two flags: one for the population density provider (which could
+        // be used by various client), and a second one for actually enabling the new coarsening
+        // algorithm.
         mSetFlagsRule.disableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POPULATION_DENSITY_PROVIDER);
+        LocationFudgerCache cache = mock(LocationFudgerCache.class);
+
+        mFudger.setLocationFudgerCache(cache);
+
+        mFudger.createCoarse(createLocation("test", mRandom));
+
+        verify(cache, never()).getCoarseningLevel(anyDouble(), anyDouble());
+    }
+
+    @Test
+    public void testDensityBasedCoarsening_ifAnyFlagIsOff2_cacheIsNotUsed() {
+        // This feature requires two flags: one for the population density provider (which could
+        // be used by various client), and a second one for actually enabling the new coarsening
+        // algorithm.
+        mSetFlagsRule.enableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_POPULATION_DENSITY_PROVIDER);
         LocationFudgerCache cache = mock(LocationFudgerCache.class);
 
         mFudger.setLocationFudgerCache(cache);
@@ -190,6 +210,7 @@
     @Test
     public void testDensityBasedCoarsening_ifFeatureIsEnabledButNoDefaultValue_cacheIsNotUsed() {
         mSetFlagsRule.enableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POPULATION_DENSITY_PROVIDER);
         LocationFudgerCache cache = mock(LocationFudgerCache.class);
         doReturn(false).when(cache).hasDefaultValue();
 
@@ -201,8 +222,23 @@
     }
 
     @Test
+    public void testDensityBasedCoarsening_ifFeatureIsEnabledButNoDefaultValue_defaultIsFetched() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POPULATION_DENSITY_PROVIDER);
+        LocationFudgerCache cache = mock(LocationFudgerCache.class);
+        doReturn(false).when(cache).hasDefaultValue();
+
+        mFudger.setLocationFudgerCache(cache);
+
+        mFudger.createCoarse(createLocation("test", mRandom));
+
+        verify(cache).fetchDefaultCoarseningLevelIfNeeded();
+    }
+
+    @Test
     public void testDensityBasedCoarsening_ifFeatureIsEnabledAndDefaultIsSet_cacheIsUsed() {
         mSetFlagsRule.enableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POPULATION_DENSITY_PROVIDER);
         LocationFudgerCache cache = mock(LocationFudgerCache.class);
         doReturn(true).when(cache).hasDefaultValue();
 
@@ -223,6 +259,7 @@
         // location/geometry/S2CellIdUtilsTest.java
 
         mSetFlagsRule.enableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POPULATION_DENSITY_PROVIDER);
         // Arbitrary location in Times Square, NYC
         double[] latLng = new double[] {40.758896, -73.985130};
         int s2Level = 1;
diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 5c73fd3..4b2e850 100644
--- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -64,6 +64,7 @@
 import android.os.CpuHeadroomParamsInternal;
 import android.os.GpuHeadroomParamsInternal;
 import android.os.IBinder;
+import android.os.IHintManager;
 import android.os.IHintSession;
 import android.os.PerformanceHintManager;
 import android.os.Process;
@@ -154,6 +155,8 @@
     private ActivityManagerInternal mAmInternalMock;
     @Mock
     private PackageManager mMockPackageManager;
+    @Mock
+    private IHintManager.IHintManagerClient mClientCallback;
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
@@ -171,6 +174,24 @@
         };
     }
 
+    private SupportInfo makeDefaultSupportInfo() {
+        mSupportInfo = new SupportInfo();
+        mSupportInfo.usesSessions = true;
+        // By default, mark everything as fully supported
+        mSupportInfo.sessionHints = -1;
+        mSupportInfo.sessionModes = -1;
+        mSupportInfo.modes = -1;
+        mSupportInfo.boosts = -1;
+        mSupportInfo.sessionTags = -1;
+        mSupportInfo.headroom = new SupportInfo.HeadroomSupportInfo();
+        mSupportInfo.headroom.isCpuSupported = true;
+        mSupportInfo.headroom.cpuMinIntervalMillis = 2000;
+        mSupportInfo.headroom.isGpuSupported = true;
+        mSupportInfo.headroom.gpuMinIntervalMillis = 2000;
+        mSupportInfo.compositionData = new SupportInfo.CompositionDataSupportInfo();
+        return mSupportInfo;
+    }
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -181,12 +202,7 @@
         mConfig.eventFlagDescriptor = new MQDescriptor<Byte, Byte>();
         ApplicationInfo applicationInfo = new ApplicationInfo();
         applicationInfo.category = ApplicationInfo.CATEGORY_GAME;
-        mSupportInfo = new SupportInfo();
-        mSupportInfo.headroom = new SupportInfo.HeadroomSupportInfo();
-        mSupportInfo.headroom.isCpuSupported = true;
-        mSupportInfo.headroom.cpuMinIntervalMillis = 2000;
-        mSupportInfo.headroom.isGpuSupported = true;
-        mSupportInfo.headroom.gpuMinIntervalMillis = 2000;
+        mSupportInfo = makeDefaultSupportInfo();
         when(mContext.getPackageManager()).thenReturn(mMockPackageManager);
         when(mMockPackageManager.getNameForUid(anyInt())).thenReturn(TEST_APP_NAME);
         when(mMockPackageManager.getApplicationInfo(eq(TEST_APP_NAME), anyInt()))
@@ -215,6 +231,7 @@
         when(mIPowerMock.getInterfaceVersion()).thenReturn(6);
         when(mIPowerMock.getSupportInfo()).thenReturn(mSupportInfo);
         when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenReturn(mConfig);
+        when(mIPowerMock.getSupportInfo()).thenReturn(mSupportInfo);
         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
         LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock);
     }
@@ -409,8 +426,11 @@
         HintManagerService service = createService();
         IBinder token = new Binder();
 
-        final int threadCount =
-                service.getBinderServiceInstance().getMaxGraphicsPipelineThreadsCount();
+        IHintManager.HintManagerClientData data = service.getBinderServiceInstance()
+                .registerClient(mClientCallback);
+
+        final int threadCount = data.maxGraphicsPipelineThreads;
+
         long sessionPtr1 = 1111L;
         long sessionId1 = 11111L;
         CountDownLatch stopLatch1 = new CountDownLatch(1);
@@ -1447,4 +1467,67 @@
         verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams1));
         verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams2));
     }
+
+    @Test
+    public void testRegisteringClient() throws Exception {
+        HintManagerService service = createService();
+        IHintManager.HintManagerClientData data = service.getBinderServiceInstance()
+                .registerClient(mClientCallback);
+        assertNotNull(data);
+        assertEquals(data.supportInfo, mSupportInfo);
+    }
+
+    @Test
+    public void testRegisteringClientOnV4() throws Exception {
+        when(mIPowerMock.getInterfaceVersion()).thenReturn(4);
+        HintManagerService service = createService();
+        IHintManager.HintManagerClientData data = service.getBinderServiceInstance()
+                .registerClient(mClientCallback);
+        assertNotNull(data);
+        assertEquals(data.supportInfo.usesSessions, true);
+        assertEquals(data.supportInfo.boosts, 0);
+        assertEquals(data.supportInfo.modes, 0);
+        assertEquals(data.supportInfo.sessionHints, 31);
+        assertEquals(data.supportInfo.sessionModes, 0);
+        assertEquals(data.supportInfo.sessionTags, 0);
+        assertEquals(data.powerHalVersion, 4);
+        assertEquals(data.preferredRateNanos, DEFAULT_HINT_PREFERRED_RATE);
+    }
+
+    @Test
+    public void testRegisteringClientOnV5() throws Exception {
+        when(mIPowerMock.getInterfaceVersion()).thenReturn(5);
+        HintManagerService service = createService();
+        IHintManager.HintManagerClientData data = service.getBinderServiceInstance()
+                .registerClient(mClientCallback);
+        assertNotNull(data);
+        assertEquals(data.supportInfo.usesSessions, true);
+        assertEquals(data.supportInfo.boosts, 0);
+        assertEquals(data.supportInfo.modes, 0);
+        assertEquals(data.supportInfo.sessionHints, 255);
+        assertEquals(data.supportInfo.sessionModes, 1);
+        assertEquals(data.supportInfo.sessionTags, 31);
+        assertEquals(data.powerHalVersion, 5);
+        assertEquals(data.preferredRateNanos, DEFAULT_HINT_PREFERRED_RATE);
+    }
+
+    @Test
+    public void testSettingUpOldClientWhenUnsupported() throws Exception {
+        when(mIPowerMock.getInterfaceVersion()).thenReturn(5);
+        // Mock unsupported to modify the default support behavior
+        when(mNativeWrapperMock.halGetHintSessionPreferredRate())
+                .thenReturn(-1L);
+        HintManagerService service = createService();
+        IHintManager.HintManagerClientData data = service.getBinderServiceInstance()
+                .registerClient(mClientCallback);
+        assertNotNull(data);
+        assertEquals(data.supportInfo.usesSessions, false);
+        assertEquals(data.supportInfo.boosts, 0);
+        assertEquals(data.supportInfo.modes, 0);
+        assertEquals(data.supportInfo.sessionHints, 0);
+        assertEquals(data.supportInfo.sessionModes, 0);
+        assertEquals(data.supportInfo.sessionTags, 0);
+        assertEquals(data.powerHalVersion, 5);
+        assertEquals(data.preferredRateNanos, -1);
+    }
 }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index b67ec8b..bc81feb 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -68,6 +68,7 @@
 @RunWith(AndroidJUnit4.class)
 public class BatteryStatsHistoryTest {
     private static final String TAG = "BatteryStatsHistoryTest";
+    private static final int MAX_HISTORY_BUFFER_SIZE = 1024;
     private final Parcel mHistoryBuffer = Parcel.obtain();
     private File mSystemDir;
     private File mHistoryDir;
@@ -98,8 +99,9 @@
 
         mClock.realtime = 123;
 
-        mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
-                mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger);
+        mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32768,
+                MAX_HISTORY_BUFFER_SIZE, mStepDetailsCalculator, mClock, mMonotonicClock, mTracer,
+                mEventLogger);
 
         when(mStepDetailsCalculator.getHistoryStepDetails())
                 .thenReturn(new BatteryStats.HistoryStepDetails());
@@ -196,12 +198,15 @@
     }
 
     @Test
-    public void testStartNextFile() {
+    public void testStartNextFile() throws Exception {
+        mHistory.forceRecordAllHistory();
+
         mClock.realtime = 123;
 
         List<String> fileList = new ArrayList<>();
         fileList.add("123.bh");
         createActiveFile(mHistory);
+        fillActiveFile(mHistory);
 
         // create file 1 to 31.
         for (int i = 1; i < 32; i++) {
@@ -210,6 +215,8 @@
 
             mHistory.startNextFile(mClock.realtime);
             createActiveFile(mHistory);
+            fillActiveFile(mHistory);
+
             verifyFileNames(mHistory, fileList);
             verifyActiveFile(mHistory, mClock.realtime + ".bh");
         }
@@ -225,6 +232,8 @@
         verifyFileNames(mHistory, fileList);
         verifyActiveFile(mHistory, "32000.bh");
 
+        fillActiveFile(mHistory);
+
         // create file 33
         mClock.realtime = 1000 * 33;
         mHistory.startNextFile(mClock.realtime);
@@ -401,6 +410,14 @@
         }
     }
 
+    private void fillActiveFile(BatteryStatsHistory history) {
+        // Create roughly 1K of history
+        int initialSize = history.getHistoryUsedSize();
+        while (history.getHistoryUsedSize() < initialSize + 1000) {
+            history.recordCurrentTimeChange(mClock.realtime, mClock.uptime, 0xFFFFFFFFL);
+        }
+    }
+
     @Test
     public void recordPowerStats() {
         PowerStats.Descriptor descriptor = new PowerStats.Descriptor(42, "foo", 1, null, 0, 2,
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 9a38209..4b6fcc3 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -92,7 +92,8 @@
                 powerStatsUidResolver, mock(FrameworkStatsLogger.class),
                 mock(BatteryStatsHistory.TraceDelegate.class),
                 mock(BatteryStatsHistory.EventLogger.class));
-        setMaxHistoryBuffer(128 * 1024);
+        mConstants.MAX_HISTORY_BUFFER = 128 * 1024;
+        mConstants.onChange();
 
         setExternalStatsSyncLocked(mExternalStatsSync);
         informThatAllExternalStatsAreFlushed();
@@ -257,20 +258,6 @@
     }
 
     @GuardedBy("this")
-    public MockBatteryStatsImpl setMaxHistoryFiles(int maxHistoryFiles) {
-        mConstants.MAX_HISTORY_FILES = maxHistoryFiles;
-        mConstants.onChange();
-        return this;
-    }
-
-    @GuardedBy("this")
-    public MockBatteryStatsImpl setMaxHistoryBuffer(int maxHistoryBuffer) {
-        mConstants.MAX_HISTORY_BUFFER = maxHistoryBuffer;
-        mConstants.onChange();
-        return this;
-    }
-
-    @GuardedBy("this")
     public MockBatteryStatsImpl setPerUidModemModel(int perUidModemModel) {
         mConstants.PER_UID_MODEM_MODEL = perUidModemModel;
         mConstants.onChange();
diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
index 8024915..71a2651 100644
--- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
@@ -16,7 +16,12 @@
 
 package com.android.server;
 
-import static com.android.server.GestureLauncherService.CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS;
+import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP;
+import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap;
+
+import static com.android.server.GestureLauncherService.LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER;
+import static com.android.server.GestureLauncherService.LAUNCH_WALLET_ON_DOUBLE_TAP_POWER;
+import static com.android.server.GestureLauncherService.POWER_DOUBLE_TAP_MAX_TIME_MS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -24,19 +29,27 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.PendingIntent;
 import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Resources;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
+import android.service.quickaccesswallet.QuickAccessWalletClient;
 import android.telecom.TelecomManager;
 import android.test.mock.MockContentResolver;
 import android.testing.TestableLooper;
@@ -55,6 +68,7 @@
 
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -62,6 +76,8 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Unit tests for {@link GestureLauncherService}.
@@ -90,9 +106,32 @@
     private @Mock TelecomManager mTelecomManager;
     private @Mock MetricsLogger mMetricsLogger;
     @Mock private UiEventLogger mUiEventLogger;
+    @Mock private QuickAccessWalletClient mQuickAccessWalletClient;
     private MockContentResolver mContentResolver;
     private GestureLauncherService mGestureLauncherService;
 
+    private Context mInstrumentationContext =
+            InstrumentationRegistry.getInstrumentation().getContext();
+
+    private static final String LAUNCH_TEST_WALLET_ACTION = "LAUNCH_TEST_WALLET_ACTION";
+    private static final String LAUNCH_FALLBACK_ACTION = "LAUNCH_FALLBACK_ACTION";
+    private PendingIntent mGesturePendingIntent =
+            PendingIntent.getBroadcast(
+                    mInstrumentationContext,
+                    0,
+                    new Intent(LAUNCH_TEST_WALLET_ACTION),
+                    PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
+
+    private PendingIntent mFallbackPendingIntent =
+            PendingIntent.getBroadcast(
+                    mInstrumentationContext,
+                    0,
+                    new Intent(LAUNCH_FALLBACK_ACTION),
+                    PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @BeforeClass
     public static void oneTimeInitialization() {
         if (Looper.myLooper() == null) {
@@ -115,9 +154,49 @@
         when(mContext.getContentResolver()).thenReturn(mContentResolver);
         when(mContext.getSystemService(Context.TELECOM_SERVICE)).thenReturn(mTelecomManager);
         when(mTelecomManager.createLaunchEmergencyDialerIntent(null)).thenReturn(new Intent());
+        when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(true);
 
-        mGestureLauncherService = new GestureLauncherService(mContext, mMetricsLogger,
-                mUiEventLogger);
+        mGestureLauncherService =
+                new GestureLauncherService(
+                        mContext, mMetricsLogger, mQuickAccessWalletClient, mUiEventLogger);
+
+        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+    }
+
+    private WalletLaunchedReceiver registerWalletLaunchedReceiver(String action) {
+        IntentFilter filter = new IntentFilter(action);
+        WalletLaunchedReceiver receiver = new WalletLaunchedReceiver();
+        mInstrumentationContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
+        return receiver;
+    }
+
+    /**
+     * A simple {@link BroadcastReceiver} implementation that counts down a {@link CountDownLatch}
+     * when a matching message is received
+     */
+    private static final class WalletLaunchedReceiver extends BroadcastReceiver {
+        private static final int TIMEOUT_SECONDS = 3;
+
+        private final CountDownLatch mLatch;
+
+        WalletLaunchedReceiver() {
+            mLatch = new CountDownLatch(1);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mLatch.countDown();
+            context.unregisterReceiver(this);
+        }
+
+        Boolean waitUntilShown() {
+            try {
+                return mLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                return false;
+            }
+        }
     }
 
     @Test
@@ -134,37 +213,123 @@
 
     @Test
     public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingDisabled() {
-        withCameraDoubleTapPowerEnableConfigValue(false);
-        withCameraDoubleTapPowerDisableSettingValue(1);
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            withDoubleTapPowerEnabledConfigValue(false);
+            withDoubleTapPowerGestureEnableSettingValue(false);
+            withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+        } else {
+            withCameraDoubleTapPowerEnableConfigValue(false);
+            withCameraDoubleTapPowerDisableSettingValue(1);
+        }
         assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
                 mContext, FAKE_USER_ID));
     }
 
     @Test
     public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingEnabled() {
-        withCameraDoubleTapPowerEnableConfigValue(false);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
-                mContext, FAKE_USER_ID));
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            withDoubleTapPowerEnabledConfigValue(false);
+            withDoubleTapPowerGestureEnableSettingValue(true);
+            withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+            assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                    mContext, FAKE_USER_ID));
+        } else {
+            withCameraDoubleTapPowerEnableConfigValue(false);
+            withCameraDoubleTapPowerDisableSettingValue(0);
+            assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                    mContext, FAKE_USER_ID));
+        }
     }
 
     @Test
     public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingDisabled() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(1);
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            withDoubleTapPowerEnabledConfigValue(true);
+            withDoubleTapPowerGestureEnableSettingValue(false);
+            withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+        } else {
+            withCameraDoubleTapPowerEnableConfigValue(true);
+            withCameraDoubleTapPowerDisableSettingValue(1);
+        }
         assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
                 mContext, FAKE_USER_ID));
     }
 
     @Test
     public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingEnabled() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            withDoubleTapPowerEnabledConfigValue(true);
+            withDoubleTapPowerGestureEnableSettingValue(true);
+            withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+        } else {
+            withCameraDoubleTapPowerEnableConfigValue(true);
+            withCameraDoubleTapPowerDisableSettingValue(0);
+        }
         assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
                 mContext, FAKE_USER_ID));
     }
 
     @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_actionWallet() {
+        withDoubleTapPowerEnabledConfigValue(true);
+        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
+
+        assertFalse(
+                mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                        mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsWalletDoubleTapPowerSettingEnabled() {
+        withDoubleTapPowerEnabledConfigValue(true);
+        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
+
+        assertTrue(
+                mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled(
+                        mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsWalletDoubleTapPowerSettingEnabled_configDisabled() {
+        withDoubleTapPowerEnabledConfigValue(false);
+        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
+
+        assertTrue(
+                mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled(
+                        mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsWalletDoubleTapPowerSettingEnabled_settingDisabled() {
+        withDoubleTapPowerEnabledConfigValue(true);
+        withDoubleTapPowerGestureEnableSettingValue(false);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
+
+        assertFalse(
+                mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled(
+                        mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsWalletDoubleTapPowerSettingEnabled_actionCamera() {
+        withDoubleTapPowerEnabledConfigValue(true);
+        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+
+        assertFalse(
+                mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled(
+                        mContext, FAKE_USER_ID));
+    }
+
+    @Test
     public void testIsEmergencyGestureSettingEnabled_settingDisabled() {
         withEmergencyGestureEnabledConfigValue(true);
         withEmergencyGestureEnabledSettingValue(false);
@@ -245,12 +410,9 @@
 
     @Test
     public void testInterceptPowerKeyDown_firstPowerDownCameraPowerGestureOnInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        enableCameraGesture();
 
-        long eventTime = INITIAL_EVENT_TIME_MILLIS +
-                CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        long eventTime = INITIAL_EVENT_TIME_MILLIS + POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
         boolean interactive = true;
@@ -284,8 +446,12 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(false);
-        withCameraDoubleTapPowerDisableSettingValue(1);
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            withDoubleTapPowerGestureEnableSettingValue(false);
+        } else {
+            withCameraDoubleTapPowerEnableConfigValue(false);
+            withCameraDoubleTapPowerDisableSettingValue(1);
+        }
         mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
@@ -298,7 +464,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -309,7 +475,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -329,8 +495,12 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOffInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(false);
-        withCameraDoubleTapPowerDisableSettingValue(1);
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            withDoubleTapPowerGestureEnableSettingValue(false);
+        } else {
+            withCameraDoubleTapPowerEnableConfigValue(false);
+            withCameraDoubleTapPowerDisableSettingValue(1);
+        }
         mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
@@ -343,7 +513,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -354,7 +524,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -401,7 +571,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -422,10 +592,7 @@
     @Test
     public void
     testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnInteractiveSetupComplete() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
-        withUserSetupCompleteValue(true);
+        enableCameraGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -437,7 +604,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -450,7 +617,7 @@
         verify(mStatusBarManagerInternal).onCameraLaunchGestureDetected(
                 StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP);
         verify(mMetricsLogger)
-            .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval);
+                .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval);
         verify(mUiEventLogger, times(1))
                 .log(GestureLauncherService.GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER);
 
@@ -470,15 +637,145 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void
+            testInterceptPowerKeyDown_fiveInboundPresses_walletAndEmergencyEnabled_bothLaunch() {
+        WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION);
+        setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent);
+        enableEmergencyGesture();
+        enableWalletGesture();
+
+        // First event
+        long eventTime = INITIAL_EVENT_TIME_MILLIS;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false);
+
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        eventTime += interval;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true);
+
+        assertTrue(receiver.waitUntilShown());
+
+        // Presses 3 and 4 should not trigger any gesture
+        for (int i = 0; i < 2; i++) {
+            eventTime += interval;
+            sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false);
+        }
+
+        // Fifth button press should trigger the emergency flow
+        eventTime += interval;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true);
+
+        verify(mUiEventLogger, times(1))
+                .log(GestureLauncherService.GestureLauncherEvent.GESTURE_EMERGENCY_TAP_POWER);
+        verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testInterceptPowerKeyDown_intervalInBoundsWalletPowerGesture() {
+        WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION);
+        setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent);
+        enableWalletGesture();
+        enableEmergencyGesture();
+
+        long eventTime = INITIAL_EVENT_TIME_MILLIS;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false);
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        eventTime += interval;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true);
+        assertTrue(receiver.waitUntilShown());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testInterceptPowerKeyDown_walletGestureOn_quickAccessWalletServiceUnavailable() {
+        when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(false);
+        WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION);
+        setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent);
+        enableWalletGesture();
+
+        // First event
+        long eventTime = INITIAL_EVENT_TIME_MILLIS;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false);
+
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        eventTime += interval;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false);
+
+        assertFalse(receiver.waitUntilShown());
+    }
+
+    @Test
+    public void testInterceptPowerKeyDown_walletGestureOn_userSetupIncomplete() {
+        WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION);
+        setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent);
+        enableWalletGesture();
+        withUserSetupCompleteValue(false);
+
+        // First event
+        long eventTime = INITIAL_EVENT_TIME_MILLIS;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false);
+
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        eventTime += interval;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false);
+
+        assertFalse(receiver.waitUntilShown());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testInterceptPowerKeyDown_walletPowerGesture_nullPendingIntent() {
+        WalletLaunchedReceiver gestureReceiver =
+                registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION);
+        setUpGetGestureTargetActivityPendingIntent(null);
+        WalletLaunchedReceiver fallbackReceiver =
+                registerWalletLaunchedReceiver(LAUNCH_FALLBACK_ACTION);
+        setUpWalletFallbackPendingIntent(mFallbackPendingIntent);
+        enableWalletGesture();
+        enableEmergencyGesture();
+
+        // First event
+        long eventTime = INITIAL_EVENT_TIME_MILLIS;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false);
+
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        eventTime += interval;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true);
+
+        assertFalse(gestureReceiver.waitUntilShown());
+        assertTrue(fallbackReceiver.waitUntilShown());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testInterceptPowerKeyDown_walletPowerGesture_intervalOutOfBounds() {
+        WalletLaunchedReceiver gestureReceiver =
+                registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION);
+        setUpGetGestureTargetActivityPendingIntent(null);
+        WalletLaunchedReceiver fallbackReceiver =
+                registerWalletLaunchedReceiver(LAUNCH_FALLBACK_ACTION);
+        setUpWalletFallbackPendingIntent(mFallbackPendingIntent);
+        enableWalletGesture();
+        enableEmergencyGesture();
+
+        // First event
+        long eventTime = INITIAL_EVENT_TIME_MILLIS;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false);
+
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS;
+        eventTime += interval;
+        sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false);
+
+        assertFalse(gestureReceiver.waitUntilShown());
+        assertFalse(fallbackReceiver.waitUntilShown());
+    }
+
+    @Test
     public void
             testInterceptPowerKeyDown_fiveInboundPresses_cameraAndEmergencyEnabled_bothLaunch() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        withEmergencyGestureEnabledConfigValue(true);
-        withEmergencyGestureEnabledSettingValue(true);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
-        mGestureLauncherService.updateEmergencyGestureEnabled();
-        withUserSetupCompleteValue(true);
+        enableCameraGesture();
+        enableEmergencyGesture();
 
         // First button press does nothing
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
@@ -491,7 +788,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
 
         // 2nd button triggers camera
         eventTime += interval;
@@ -507,7 +804,7 @@
         verify(mStatusBarManagerInternal).onCameraLaunchGestureDetected(
                 StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP);
         verify(mMetricsLogger)
-            .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval);
+                .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval);
         verify(mUiEventLogger, times(1))
                 .log(GestureLauncherService.GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER);
 
@@ -580,7 +877,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         // 3 more button presses which should not trigger any gesture (camera gesture disabled)
         for (int i = 0; i < 3; i++) {
             eventTime += interval;
@@ -634,7 +931,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         // 3 more button presses which should not trigger any gesture, but intercepts action.
         for (int i = 0; i < 3; i++) {
             eventTime += interval;
@@ -737,7 +1034,7 @@
                     interactive, outLaunched);
             assertTrue(intercepted);
             assertFalse(outLaunched.value);
-            interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+            interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
             eventTime += interval;
         }
     }
@@ -765,7 +1062,7 @@
                     interactive, outLaunched);
             assertTrue(intercepted);
             assertFalse(outLaunched.value);
-            interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+            interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
             eventTime += interval;
         }
     }
@@ -916,7 +1213,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT, IGNORED_META_STATE, IGNORED_DEVICE_ID, IGNORED_SCANCODE,
@@ -947,9 +1244,7 @@
     @Test
     public void
     testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnInteractiveSetupIncomplete() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        enableCameraGesture();
         withUserSetupCompleteValue(false);
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
@@ -962,7 +1257,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -973,7 +1268,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -995,9 +1290,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOnInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        enableCameraGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1009,7 +1302,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -1020,7 +1313,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1042,9 +1335,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOnInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        enableCameraGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1067,7 +1358,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1087,8 +1378,12 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffNotInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(false);
-        withCameraDoubleTapPowerDisableSettingValue(1);
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            withDoubleTapPowerGestureEnableSettingValue(false);
+        } else {
+            withCameraDoubleTapPowerEnableConfigValue(false);
+            withCameraDoubleTapPowerDisableSettingValue(1);
+        }
         mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
@@ -1101,7 +1396,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -1112,7 +1407,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1146,7 +1441,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -1156,7 +1451,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1202,7 +1497,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1223,10 +1518,7 @@
     @Test
     public void
     testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnNotInteractiveSetupComplete() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
-        withUserSetupCompleteValue(true);
+        enableCameraGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1238,7 +1530,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -1250,7 +1542,7 @@
         verify(mStatusBarManagerInternal).onCameraLaunchGestureDetected(
                 StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP);
         verify(mMetricsLogger)
-            .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval);
+                .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval);
         verify(mUiEventLogger, times(1))
                 .log(GestureLauncherService.GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER);
 
@@ -1272,9 +1564,7 @@
     @Test
     public void
     testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnNotInteractiveSetupIncomplete() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        enableCameraGesture();
         withUserSetupCompleteValue(false);
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
@@ -1287,7 +1577,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -1298,7 +1588,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1332,7 +1622,7 @@
         assertFalse(intercepted);
         assertFalse(outLaunched.value);
 
-        final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS;
+        final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS;
         eventTime += interval;
         keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
                 IGNORED_REPEAT);
@@ -1343,7 +1633,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1365,9 +1655,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOnNotInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        enableCameraGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1390,7 +1678,7 @@
         assertFalse(outLaunched.value);
 
         verify(mMetricsLogger, never())
-            .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
+                .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt());
         verify(mUiEventLogger, never()).log(any());
 
         final ArgumentCaptor<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1414,7 +1702,7 @@
      * @return last event time.
      */
     private long triggerEmergencyGesture() {
-        return triggerEmergencyGesture(CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1);
+        return triggerEmergencyGesture(POWER_DOUBLE_TAP_MAX_TIME_MS - 1);
     }
 
     /**
@@ -1473,6 +1761,27 @@
                 .thenReturn(enableConfigValue);
     }
 
+    private void withDoubleTapPowerEnabledConfigValue(boolean enable) {
+        when(mResources.getBoolean(com.android.internal.R.bool.config_doubleTapPowerGestureEnabled))
+                .thenReturn(enable);
+    }
+
+    private void withDoubleTapPowerGestureEnableSettingValue(boolean enable) {
+        Settings.Secure.putIntForUser(
+                mContentResolver,
+                Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED,
+                enable ? 1 : 0,
+                UserHandle.USER_CURRENT);
+    }
+
+    private void withDefaultDoubleTapPowerGestureAction(int action) {
+        Settings.Secure.putIntForUser(
+                mContentResolver,
+                Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE,
+                action,
+                UserHandle.USER_CURRENT);
+    }
+
     private void withEmergencyGestureEnabledConfigValue(boolean enableConfigValue) {
         when(mResources.getBoolean(
                 com.android.internal.R.bool.config_emergencyGestureEnabled))
@@ -1510,4 +1819,72 @@
                 userSetupCompleteValue,
                 UserHandle.USER_CURRENT);
     }
+
+    private void setUpGetGestureTargetActivityPendingIntent(PendingIntent pendingIntent) {
+        doAnswer(
+                invocation -> {
+                    QuickAccessWalletClient.GesturePendingIntentCallback callback =
+                            (QuickAccessWalletClient.GesturePendingIntentCallback)
+                                    invocation.getArguments()[1];
+                    callback.onGesturePendingIntentRetrieved(pendingIntent);
+                    return null;
+                })
+                .when(mQuickAccessWalletClient)
+                .getGestureTargetActivityPendingIntent(any(), any());
+    }
+
+    private void setUpWalletFallbackPendingIntent(PendingIntent pendingIntent) {
+        doAnswer(
+                invocation -> {
+                    QuickAccessWalletClient.WalletPendingIntentCallback callback =
+                            (QuickAccessWalletClient.WalletPendingIntentCallback)
+                                    invocation.getArguments()[1];
+                    callback.onWalletPendingIntentRetrieved(pendingIntent);
+                    return null;
+                })
+                .when(mQuickAccessWalletClient)
+                .getWalletPendingIntent(any(), any());
+    }
+
+    private void enableWalletGesture() {
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
+        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDoubleTapPowerEnabledConfigValue(true);
+
+        mGestureLauncherService.updateWalletDoubleTapPowerEnabled();
+        withUserSetupCompleteValue(true);
+    }
+
+    private void enableEmergencyGesture() {
+        withEmergencyGestureEnabledConfigValue(true);
+        withEmergencyGestureEnabledSettingValue(true);
+        mGestureLauncherService.updateEmergencyGestureEnabled();
+        withUserSetupCompleteValue(true);
+    }
+
+    private void enableCameraGesture() {
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            withDoubleTapPowerEnabledConfigValue(true);
+            withDoubleTapPowerGestureEnableSettingValue(true);
+            withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+        } else {
+            withCameraDoubleTapPowerEnableConfigValue(true);
+            withCameraDoubleTapPowerDisableSettingValue(0);
+        }
+        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        withUserSetupCompleteValue(true);
+    }
+
+    private void sendPowerKeyDownToGestureLauncherServiceAndAssertValues(
+            long eventTime, boolean expectedIntercept, boolean expectedOutLaunchedValue) {
+        KeyEvent keyEvent =
+                new KeyEvent(
+                        IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT);
+        boolean interactive = true;
+        MutableBoolean outLaunched = new MutableBoolean(true);
+        boolean intercepted =
+                mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, outLaunched);
+        assertEquals(intercepted, expectedIntercept);
+        assertEquals(outLaunched.value, expectedOutLaunchedValue);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index a2965b3..fa78dfc 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -64,6 +64,7 @@
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.annotation.NonNull;
+import android.annotation.UserIdInt;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.app.admin.DevicePolicyManager;
@@ -1652,10 +1653,17 @@
 
     @Test
     @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
-    public void restoreShortcutTargets_qs_a11yQsTargetsRestored() {
-        // TODO: remove the assumption when we fix b/381294327
+    @DisableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargetsAssumeUser0_qs_a11yQsTargetsRestored() {
         assumeTrue("The test is setup to run as a user 0",
                 mTestableContext.getUserId() == UserHandle.USER_SYSTEM);
+        restoreShortcutTargets_qs_a11yQsTargetsRestored();
+    }
+
+    @Test
+    @EnableFlags({android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT,
+            android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE})
+    public void restoreShortcutTargets_qs_a11yQsTargetsRestored() {
         String daltonizerTile =
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
         String colorInversionTile =
@@ -1667,7 +1675,7 @@
 
         broadcastSettingRestored(
                 ShortcutUtils.convertToKey(QUICK_SETTINGS),
-                /*newValue=*/colorInversionTile);
+                /*newValue=*/colorInversionTile, userState.mUserId);
 
         Set<String> expected = Set.of(daltonizerTile, colorInversionTile);
         assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(QUICK_SETTINGS)))
@@ -1677,11 +1685,18 @@
     }
 
     @Test
-    @DisableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
-    public void restoreShortcutTargets_qs_a11yQsTargetsNotRestored() {
-        // TODO: remove the assumption when we fix b/381294327
+    @DisableFlags({android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT,
+            android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE})
+    public void restoreShortcutTargetsAssumeUser0_qs_a11yQsTargetsNotRestored() {
         assumeTrue("The test is setup to run as a user 0",
                 mTestableContext.getUserId() == UserHandle.USER_SYSTEM);
+        restoreShortcutTargets_qs_a11yQsTargetsNotRestored();
+    }
+
+    @Test
+    @DisableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
+    @EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargets_qs_a11yQsTargetsNotRestored() {
         String daltonizerTile =
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
         String colorInversionTile =
@@ -1694,7 +1709,7 @@
 
         broadcastSettingRestored(
                 ShortcutUtils.convertToKey(QUICK_SETTINGS),
-                /*newValue=*/colorInversionTile);
+                /*newValue=*/colorInversionTile, userState.mUserId);
 
         Set<String> expected = Set.of(daltonizerTile);
         assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(QUICK_SETTINGS)))
@@ -1762,10 +1777,16 @@
     }
 
     @Test
-    public void restoreShortcutTargets_hardware_targetsMerged() {
-        // TODO: remove the assumption when we fix b/381294327
+    @DisableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargetsAssumeUser0_hardware_targetsMerged() {
         assumeTrue("The test is setup to run as a user 0",
                 mTestableContext.getUserId() == UserHandle.USER_SYSTEM);
+        restoreShortcutTargets_hardware_targetsMerged();
+    }
+
+    @Test
+    @EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargets_hardware_targetsMerged() {
         mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
         final String servicePrevious = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
         final String otherPrevious = TARGET_MAGNIFICATION;
@@ -1779,7 +1800,7 @@
 
         broadcastSettingRestored(
                 ShortcutUtils.convertToKey(HARDWARE),
-                /*newValue=*/serviceRestored);
+                /*newValue=*/serviceRestored, userState.mUserId);
 
         final Set<String> expected = Set.of(servicePrevious, otherPrevious, serviceRestored);
         assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
@@ -1789,10 +1810,16 @@
     }
 
     @Test
-    public void restoreShortcutTargets_hardware_alreadyHadDefaultService_doesNotClear() {
-        // TODO: remove the assumption when we fix b/381294327
+    @DisableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargetsAssumeUser0_hardware_alreadyHadDefaultService_doesNotClear() {
         assumeTrue("The test is setup to run as a user 0",
                 mTestableContext.getUserId() == UserHandle.USER_SYSTEM);
+        restoreShortcutTargets_hardware_alreadyHadDefaultService_doesNotClear();
+    }
+
+    @Test
+    @EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargets_hardware_alreadyHadDefaultService_doesNotClear() {
         final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE_NAME;
         mTestableContext.getOrCreateTestableResources().addOverride(
                 R.string.config_defaultAccessibilityService, serviceDefault);
@@ -1807,7 +1834,7 @@
 
         broadcastSettingRestored(
                 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                /*newValue=*/serviceDefault);
+                /*newValue=*/serviceDefault, userState.mUserId);
 
         final Set<String> expected = Set.of(serviceDefault);
         assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
@@ -1817,10 +1844,16 @@
     }
 
     @Test
-    public void restoreShortcutTargets_hardware_didNotHaveDefaultService_clearsDefaultService() {
-        // TODO: remove the assumption when we fix b/381294327
+    @DisableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargetsAsUser0_hardware_noDefaultService_clearsDefaultService() {
         assumeTrue("The test is setup to run as a user 0",
                 mTestableContext.getUserId() == UserHandle.USER_SYSTEM);
+        restoreShortcutTargets_hardware_didNotHaveDefaultService_clearsDefaultService();
+    }
+
+    @Test
+    @EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargets_hardware_didNotHaveDefaultService_clearsDefaultService() {
         final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE_NAME;
         final String serviceRestored = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
         // Restored value from the broadcast contains both default and non-default service.
@@ -1833,7 +1866,7 @@
         setupShortcutTargetServices(userState);
 
         broadcastSettingRestored(ShortcutUtils.convertToKey(HARDWARE),
-                /*newValue=*/combinedRestored);
+                /*newValue=*/combinedRestored, userState.mUserId);
 
         // The default service is cleared from the final restored value.
         final Set<String> expected = Set.of(serviceRestored);
@@ -1844,10 +1877,16 @@
     }
 
     @Test
-    public void restoreShortcutTargets_hardware_nullSetting_clearsDefaultService() {
-        // TODO: remove the assumption when we fix b/381294327
+    @DisableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargetsAssumeUser0_hardware_nullSetting_clearsDefaultService() {
         assumeTrue("The test is setup to run as a user 0",
                 mTestableContext.getUserId() == UserHandle.USER_SYSTEM);
+        restoreShortcutTargets_hardware_nullSetting_clearsDefaultService();
+    }
+
+    @Test
+    @EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SECURE_SETTINGS_ON_HSUM_DEVICE)
+    public void restoreShortcutTargets_hardware_nullSetting_clearsDefaultService() {
         final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE_NAME;
         final String serviceRestored = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
         // Restored value from the broadcast contains both default and non-default service.
@@ -1864,7 +1903,7 @@
         putShortcutSettingForUser(HARDWARE, null, userState.mUserId);
 
         broadcastSettingRestored(ShortcutUtils.convertToKey(HARDWARE),
-                /*newValue=*/combinedRestored);
+                /*newValue=*/combinedRestored, userState.mUserId);
 
         // The default service is cleared from the final restored value.
         final Set<String> expected = Set.of(serviceRestored);
@@ -2332,12 +2371,12 @@
                 setting, mA11yms.getCurrentUserIdLocked(), strings, str -> str);
     }
 
-    private void broadcastSettingRestored(String setting, String newValue) {
+    private void broadcastSettingRestored(String setting, String newValue, @UserIdInt int userId) {
         Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
                 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
                 .putExtra(Intent.EXTRA_SETTING_NAME, setting)
                 .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, newValue);
-        sendBroadcastToAccessibilityManagerService(intent);
+        sendBroadcastToAccessibilityManagerService(intent, userId);
         mTestableLooper.processAllMessages();
     }
 
@@ -2421,12 +2460,24 @@
         userState.updateTileServiceMapForAccessibilityServiceLocked();
     }
 
-    private void sendBroadcastToAccessibilityManagerService(Intent intent) {
+    private void sendBroadcastToAccessibilityManagerService(Intent intent, @UserIdInt int userId) {
         if (!mTestableContext.getBroadcastReceivers().containsKey(intent.getAction())) {
             return;
         }
         mTestableContext.getBroadcastReceivers().get(intent.getAction()).forEach(
-                broadcastReceiver -> broadcastReceiver.onReceive(mTestableContext, intent));
+                broadcastReceiver -> {
+                    BroadcastReceiver.PendingResult result = mock(
+                            BroadcastReceiver.PendingResult.class);
+                    try {
+                        FieldSetter.setField(result,
+                                BroadcastReceiver.PendingResult.class.getDeclaredField(
+                                        "mSendingUser"), userId);
+                    } catch (NoSuchFieldException e) {
+                        // do nothing
+                    }
+                    broadcastReceiver.setPendingResult(result);
+                    broadcastReceiver.onReceive(mTestableContext, intent);
+                });
     }
 
     public static class FakeInputFilter extends AccessibilityInputFilter {
@@ -2449,6 +2500,7 @@
         A11yTestableContext(Context base) {
             super(base);
             mMockContext = mock(Context.class);
+
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerCacheTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerCacheTest.java
index d69e476..9b878b3 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerCacheTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerCacheTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assume.assumeTrue;
+import static org.junit.Assert.fail;
 
 import android.app.ActivityManager;
 import android.app.LocaleManager;
@@ -26,6 +27,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.multiuser.Flags;
+import android.os.Bundle;
 import android.os.LocaleList;
 import android.os.SystemClock;
 import android.os.UserHandle;
@@ -261,6 +263,162 @@
         assertThat(um.getUserName()).isEqualTo(newName);
     }
 
+
+    @MediumTest
+    @Test
+    public void testDefaultRestrictionsApplied() throws Exception {
+        final UserInfo userInfo = mUserManager.createUser("Useroid",
+                UserManager.USER_TYPE_FULL_SECONDARY, 0);
+        mUsersToRemove.add(userInfo.id);
+        final UserTypeDetails userTypeDetails =
+                UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_FULL_SECONDARY);
+        final Bundle expectedRestrictions = userTypeDetails.getDefaultRestrictions();
+        // Note this can fail if DO unset those restrictions.
+        for (String restriction : expectedRestrictions.keySet()) {
+            if (expectedRestrictions.getBoolean(restriction)) {
+                assertThat(mUserManager.hasUserRestriction(restriction, UserHandle.of(userInfo.id)))
+                        .isTrue();
+                // Test cached value
+                assertThat(mUserManager.hasUserRestriction(restriction, UserHandle.of(userInfo.id)))
+                        .isTrue();
+            }
+        }
+    }
+
+    @MediumTest
+    @Test
+    public void testSetDefaultGuestRestrictions() {
+        final Bundle origRestrictions = mUserManager.getDefaultGuestRestrictions();
+        try {
+            final boolean isFunDisallowed = origRestrictions.getBoolean(UserManager.DISALLOW_FUN,
+                    false);
+            final UserInfo guest1 = mUserManager.createUser("Guest 1", UserInfo.FLAG_GUEST);
+            assertThat(guest1).isNotNull();
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    guest1.getUserHandle())).isEqualTo(isFunDisallowed);
+            removeUser(guest1.id, true);
+            // Cache return false after user was removed
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    guest1.getUserHandle())).isFalse();
+
+            Bundle restrictions = new Bundle();
+            restrictions.putBoolean(UserManager.DISALLOW_FUN, !isFunDisallowed);
+            mUserManager.setDefaultGuestRestrictions(restrictions);
+            UserInfo guest2 = mUserManager.createUser("Guest 2", UserInfo.FLAG_GUEST);
+            assertThat(guest2).isNotNull();
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    guest2.getUserHandle())).isNotEqualTo(isFunDisallowed);
+            removeUser(guest2.id, true);
+            assertThat(mUserManager.getUserInfo(guest2.id)).isNull();
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    guest2.getUserHandle())).isFalse();
+        } finally {
+            mUserManager.setDefaultGuestRestrictions(origRestrictions);
+        }
+    }
+
+    @MediumTest
+    @Test
+    public void testCacheInvalidatedAfterUserAddedOrRemoved() {
+        final Bundle origRestrictions = mUserManager.getDefaultGuestRestrictions();
+        try {
+            final boolean isFunDisallowed = origRestrictions.getBoolean(UserManager.DISALLOW_FUN,
+                    false);
+            final UserInfo guest1 = mUserManager.createUser("Guest 1", UserInfo.FLAG_GUEST);
+            assertThat(guest1).isNotNull();
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    guest1.getUserHandle())).isEqualTo(isFunDisallowed);
+            removeUser(guest1.id, true);
+
+            Bundle restrictions = new Bundle();
+            restrictions.putBoolean(UserManager.DISALLOW_FUN, !isFunDisallowed);
+            mUserManager.setDefaultGuestRestrictions(restrictions);
+            int latest_id = guest1.id;
+            // Cache removed id and few next ids.
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    UserHandle.of(latest_id))).isFalse();
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    UserHandle.of(latest_id + 1))).isFalse();
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    UserHandle.of(latest_id + 2))).isFalse();
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    UserHandle.of(latest_id + 3))).isFalse();
+
+            UserInfo guest2 = mUserManager.createUser("Guest 2", UserInfo.FLAG_GUEST);
+            assertThat(guest2).isNotNull();
+            // Cache was invalidated after user was added
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    guest2.getUserHandle())).isTrue();
+            removeUser(guest2.id, true);
+            assertThat(mUserManager.getUserInfo(guest2.id)).isNull();
+            // Cache was invalidated after user was removed
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    guest2.getUserHandle())).isFalse();
+        } finally {
+            mUserManager.setDefaultGuestRestrictions(origRestrictions);
+        }
+    }
+
+
+    @MediumTest
+    @Test
+    public void testAddRemoveUsersAndRestrictions() {
+        try {
+            final UserInfo userInfo = mUserManager.createUser("Useroid",
+                    UserManager.USER_TYPE_FULL_SECONDARY, 0);
+            mUsersToRemove.add(userInfo.id);
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    userInfo.getUserHandle())).isFalse();
+            mUserManager.setUserRestriction(UserManager.DISALLOW_FUN, true,
+                    userInfo.getUserHandle());
+
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    userInfo.getUserHandle())).isTrue();
+            removeUser(userInfo.id, true);
+            assertThat(mUserManager.getUserSerialNumber(userInfo.id)).isEqualTo(-1);
+            assertThat(mUserManager.getUserInfo(userInfo.id)).isNull();
+            assertThat(mUserManager.hasUserRestriction(UserManager.DISALLOW_FUN,
+                    userInfo.getUserHandle())).isFalse();
+        } catch (java.lang.Exception e) {
+        }
+    }
+
+
+    private void sleep(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    @MediumTest
+    @Test
+    public void testDefaultUserRestrictionsForPrivateProfile() {
+        assumeTrue(mUserManager.canAddPrivateProfile());
+        final int currentUserId = ActivityManager.getCurrentUser();
+        UserInfo privateProfileInfo = null;
+        try {
+            privateProfileInfo = mUserManager.createProfileForUser(
+                    "Private", UserManager.USER_TYPE_PROFILE_PRIVATE, 0, currentUserId, null);
+            assertThat(privateProfileInfo).isNotNull();
+        } catch (Exception e) {
+            fail("Creation of private profile failed due to " + e.getMessage());
+        }
+        assertDefaultPrivateProfileRestrictions(privateProfileInfo.getUserHandle());
+        // Assert cached values
+        assertDefaultPrivateProfileRestrictions(privateProfileInfo.getUserHandle());
+    }
+
+    private void assertDefaultPrivateProfileRestrictions(UserHandle userHandle) {
+        Bundle defaultPrivateProfileRestrictions =
+                UserTypeFactory.getDefaultPrivateProfileRestrictions();
+        for (String restriction : defaultPrivateProfileRestrictions.keySet()) {
+            assertThat(mUserManager.hasUserRestrictionForUser(restriction, userHandle)).isTrue();
+        }
+    }
+
     private void assumeManagedUsersSupported() {
         // In Automotive, if headless system user is enabled, a managed user cannot be created
         // under a primary user.
@@ -270,9 +428,23 @@
     }
 
     private void removeUser(int userId) {
+        removeUser(userId, false);
+    }
+
+    private void removeUser(int userId, boolean waitForCompleteRemoval) {
         mUserManager.removeUser(userId);
         mUserRemovalWaiter.waitFor(userId);
         mUsersToRemove.remove(userId);
+        if (waitForCompleteRemoval) {
+            int serialNumber = mUserManager.getUserSerialNumber(userId);
+            int timeout = REMOVE_USER_TIMEOUT_SECONDS * 5; // called every 200ms
+            // Wait for the user to be removed from memory
+            while (serialNumber > 0 && timeout > 0) {
+                sleep(200);
+                timeout--;
+                serialNumber = mUserManager.getUserSerialNumber(userId);
+            }
+        }
     }
 
     private boolean isAutomotive() {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/EventConditionProviderTest.java b/services/tests/uiservicestests/src/com/android/server/notification/EventConditionProviderTest.java
index fa1372d..87b9154 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/EventConditionProviderTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/EventConditionProviderTest.java
@@ -88,6 +88,8 @@
         mService.mContext = this.getContext();
 
         mContext.addMockSystemService(UserManager.class, mUserManager);
+        when(mUserManager.getProfiles(eq(UserHandle.USER_SYSTEM))).thenReturn(
+                List.of(new UserInfo(UserHandle.USER_SYSTEM, "USER_SYSTEM", 0)));
         when(mUserManager.getProfiles(eq(mUserId))).thenReturn(
                 List.of(new UserInfo(mUserId, "mUserId", 0)));
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 1fc0d24..20f4bb6 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -16712,6 +16712,10 @@
         when(mResources.getResourceName(eq(iconResId))).thenReturn(iconResName);
         when(mResources.getIdentifier(eq(iconResName), any(), any())).thenReturn(iconResId);
         when(mPackageManagerClient.getResourcesForApplication(eq(pkg))).thenReturn(mResources);
+
+        // Ensure that there is a zen configuration for the user running the test (won't be
+        // USER_SYSTEM if running on HSUM).
+        mService.mZenModeHelper.onUserSwitched(mUserId);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 661d07e..c9cbe0f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -32,6 +32,8 @@
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT;
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_IF_ALLOWLISTED;
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_NEVER;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
@@ -124,6 +126,7 @@
 import android.app.servertransaction.DestroyActivityItem;
 import android.app.servertransaction.PauseActivityItem;
 import android.app.servertransaction.WindowStateResizeItem;
+import android.compat.testing.PlatformCompatChangeRule;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -138,6 +141,7 @@
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
 import android.util.MutableBoolean;
@@ -159,9 +163,13 @@
 import com.android.server.wm.ActivityRecord.State;
 import com.android.window.flags.Flags;
 
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.invocation.InvocationOnMock;
@@ -183,6 +191,9 @@
 @RunWith(WindowTestRunner.class)
 public class ActivityRecordTests extends WindowTestsBase {
 
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
     private final String mPackageName = getInstrumentation().getTargetContext().getPackageName();
 
     private static final int ORIENTATION_CONFIG_CHANGES =
@@ -721,6 +732,64 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
+    public void testOrientation_allowFixedOrientationForCameraCompatInFreeformWindowing() {
+        final ActivityRecord activity = setupDisplayAndActivityForCameraCompat(
+                /* isCameraRunning= */ true, WINDOWING_MODE_FREEFORM);
+
+        // Task in landscape.
+        assertEquals(ORIENTATION_LANDSCAPE, activity.getTask().getConfiguration().orientation);
+        // The app should be letterboxed.
+        assertEquals(ORIENTATION_PORTRAIT, activity.getConfiguration().orientation);
+        assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy()
+                .isLetterboxedForFixedOrientationAndAspectRatio());
+    }
+
+    @Test
+    public void testOrientation_dontAllowFixedOrientationForCameraCompatFreeformIfNotEnabled() {
+        final ActivityRecord activity = setupDisplayAndActivityForCameraCompat(
+                /* isCameraRunning= */ true, WINDOWING_MODE_FREEFORM);
+
+        // Task in landscape.
+        assertEquals(ORIENTATION_LANDSCAPE, activity.getTask().getConfiguration().orientation);
+        // Activity is not letterboxed.
+        assertEquals(ORIENTATION_LANDSCAPE, activity.getConfiguration().orientation);
+        assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy()
+                .isLetterboxedForFixedOrientationAndAspectRatio());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
+    public void testOrientation_noFixedOrientationForCameraCompatFreeformIfCameraNotRunning() {
+        final ActivityRecord activity = setupDisplayAndActivityForCameraCompat(
+                /* isCameraRunning= */ false, WINDOWING_MODE_FREEFORM);
+
+        // Task in landscape.
+        assertEquals(ORIENTATION_LANDSCAPE, activity.getTask().getConfiguration().orientation);
+        // Activity is not letterboxed.
+        assertEquals(ORIENTATION_LANDSCAPE, activity.getConfiguration().orientation);
+        assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy()
+                .isLetterboxedForFixedOrientationAndAspectRatio());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
+    public void testOrientation_dontAllowFixedOrientationForCameraCompatFreeformIfInPip() {
+        final ActivityRecord activity = setupDisplayAndActivityForCameraCompat(
+                /* isCameraRunning= */ true, WINDOWING_MODE_PINNED);
+
+        // Task in landscape.
+        assertEquals(ORIENTATION_LANDSCAPE, activity.getTask().getConfiguration().orientation);
+        // Activity is not letterboxed.
+        assertEquals(ORIENTATION_LANDSCAPE, activity.getConfiguration().orientation);
+        assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy()
+                .isLetterboxedForFixedOrientationAndAspectRatio());
+    }
+
+    @Test
     public void testShouldMakeActive_deferredResume() {
         final ActivityRecord activity = createActivityWithTask();
         activity.setState(STOPPED, "Testing");
@@ -2472,11 +2541,14 @@
         final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
         assertEquals(0, activity.getChildCount());
 
-        final WindowState win1 = createWindow(null, TYPE_APPLICATION, activity, "win1");
-        final WindowState startingWin = createWindow(null, TYPE_APPLICATION_STARTING, activity,
-                "startingWin");
-        final WindowState baseWin = createWindow(null, TYPE_BASE_APPLICATION, activity, "baseWin");
-        final WindowState win4 = createWindow(null, TYPE_APPLICATION, activity, "win4");
+        final WindowState win1 = newWindowBuilder("app1", TYPE_APPLICATION).setWindowToken(
+                activity).build();
+        final WindowState startingWin = newWindowBuilder("startingWin",
+                TYPE_APPLICATION_STARTING).setWindowToken(activity).build();
+        final WindowState baseWin = newWindowBuilder("baseWin",
+                TYPE_BASE_APPLICATION).setWindowToken(activity).build();
+        final WindowState win4 = newWindowBuilder("win4", TYPE_APPLICATION).setWindowToken(
+                activity).build();
 
         // Should not contain the windows that were added above.
         assertEquals(4, activity.getChildCount());
@@ -2499,14 +2571,17 @@
         final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
         assertNull(activity.findMainWindow());
 
-        final WindowState window1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "window1");
-        final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, activity, "window11");
-        final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, activity, "window12");
+        final WindowState window1 = newWindowBuilder("window1",
+                TYPE_BASE_APPLICATION).setWindowToken(activity).build();
+        final WindowState window11 = newWindowBuilder("window11", FIRST_SUB_WINDOW).setParent(
+                window1).setWindowToken(activity).build();
+        final WindowState window12 = newWindowBuilder("window12", FIRST_SUB_WINDOW).setParent(
+                window1).setWindowToken(activity).build();
         assertEquals(window1, activity.findMainWindow());
         window1.mAnimatingExit = true;
         assertEquals(window1, activity.findMainWindow());
-        final WindowState window2 = createWindow(null, TYPE_APPLICATION_STARTING, activity,
-                "window2");
+        final WindowState window2 = newWindowBuilder("window2",
+                TYPE_APPLICATION_STARTING).setWindowToken(activity).build();
         assertEquals(window2, activity.findMainWindow());
         activity.removeImmediately();
     }
@@ -2651,8 +2726,8 @@
 
     @Test
     public void testStuckExitingWindow() {
-        final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
-                "closingWindow");
+        final WindowState closingWindow = newWindowBuilder("closingWindow",
+                FIRST_APPLICATION_WINDOW).build();
         closingWindow.mAnimatingExit = true;
         closingWindow.mRemoveOnExit = true;
         closingWindow.mActivityRecord.commitVisibility(
@@ -3313,7 +3388,7 @@
     @SetupWindows(addWindows = W_INPUT_METHOD)
     @Test
     public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         makeWindowVisibleAndDrawn(app, mImeWindow);
         mDisplayContent.setImeLayeringTarget(app);
         mDisplayContent.setImeInputTarget(app);
@@ -3341,7 +3416,7 @@
     @SetupWindows(addWindows = W_INPUT_METHOD)
     @Test
     public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
 
         mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer(
                 mImeWindow, null, null);
@@ -3385,8 +3460,8 @@
     @Test
     public void testImeInsetsFrozenFlag_noDispatchVisibleInsetsWhenAppNotRequest()
             throws RemoteException {
-        final WindowState app1 = createWindow(null, TYPE_APPLICATION, "app1");
-        final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2");
+        final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).build();
+        final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build();
 
         mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer(
                 mImeWindow, null, null);
@@ -3430,7 +3505,8 @@
     @Test
     public void testImeInsetsFrozenFlag_multiWindowActivities() {
         final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent);
-        final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, imeToken, "ime");
+        final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).setWindowToken(
+                imeToken).build();
         makeWindowVisibleAndDrawn(ime);
 
         // Create a split-screen root task with activity1 and activity 2.
@@ -3451,8 +3527,10 @@
         activity1.mImeInsetsFrozenUntilStartInput = true;
         activity2.mImeInsetsFrozenUntilStartInput = true;
 
-        final WindowState app1 = createWindow(null, TYPE_APPLICATION, activity1, "app1");
-        final WindowState app2 = createWindow(null, TYPE_APPLICATION, activity2, "app2");
+        final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).setWindowToken(
+                activity1).build();
+        final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowToken(
+                activity2).build();
         makeWindowVisibleAndDrawn(app1, app2);
 
         final InsetsStateController controller = mDisplayContent.getInsetsStateController();
@@ -3481,7 +3559,7 @@
 
     @Test
     public void testInClosingAnimation_visibilityNotCommitted_doNotHideSurface() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         makeWindowVisibleAndDrawn(app);
 
         // Put the activity in close transition.
@@ -3508,7 +3586,7 @@
 
     @Test
     public void testInClosingAnimation_visibilityCommitted_hideSurface() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         makeWindowVisibleAndDrawn(app);
         app.mActivityRecord.prepareSurfaces();
 
@@ -3695,6 +3773,37 @@
         assertTrue(appWindow.mResizeReported);
     }
 
+    private ActivityRecord setupDisplayAndActivityForCameraCompat(boolean isCameraRunning,
+            int windowingMode) {
+        doReturn(true).when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
+        // Create a new DisplayContent so that the flag values create the camera freeform policy.
+        mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayContent.getSurfaceWidth(),
+                mDisplayContent.getSurfaceHeight()).build();
+        final CameraStateMonitor cameraStateMonitor = mDisplayContent.mAppCompatCameraPolicy
+                .mCameraStateMonitor;
+        spyOn(cameraStateMonitor);
+        doReturn(isCameraRunning).when(cameraStateMonitor).isCameraRunningForActivity(any());
+        final TaskDisplayArea tda = mDisplayContent.getDefaultTaskDisplayArea();
+        spyOn(tda);
+        doReturn(true).when(tda).supportsNonResizableMultiWindow();
+        final Task rootTask = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent)
+                .setWindowingMode(windowingMode).build();
+        doReturn(mDisplayContent.getDisplayInfo())
+                .when(mDisplayContent.mWmService.mDisplayManagerInternal).getDisplayInfo(anyInt());
+        rootTask.setBounds(0, 0, 1000, 500);
+        final ActivityRecord activity = new ActivityBuilder(mAtm)
+                .setComponent(ComponentName.createRelative(mContext,
+                        com.android.server.wm.ActivityRecordTests.class.getName()))
+                .setParentTask(rootTask)
+                .setCreateTask(true)
+                .setOnTop(true)
+                .setResizeMode(RESIZE_MODE_RESIZEABLE)
+                .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+                .build();
+        activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode();
+        return activity;
+    }
+
     private void assertHasStartingWindow(ActivityRecord atoken) {
         assertNotNull(atoken.mStartingSurface);
         assertNotNull(atoken.mStartingData);
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index 3750dd3..748a47a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -203,6 +203,58 @@
     }
 
     @Test
+    @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
+    public void testIsFreeformLetterboxingForCameraAllowed_featureDisabled_returnsFalse() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    public void testIsFreeformLetterboxingForCameraAllowed_overrideDisabled_returnsFalse() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
+    public void testIsFreeformLetterboxingForCameraAllowed_cameraNotRunning_returnsFalse() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
+    public void testIsFreeformLetterboxingForCameraAllowed_notFreeformWindowing_returnsFalse() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
+    public void testIsFreeformLetterboxingForCameraAllowed_optInFreeformCameraRunning_true() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+    }
+
+    @Test
     @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
     @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
     public void testFullscreen_doesNotActivateCameraCompatMode() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index f339d29..429a396a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -92,7 +92,7 @@
  * Tests for the {@link DragDropController} class.
  *
  * Build/Install/Run:
- *  atest WmTests:DragDropControllerTests
+ * atest WmTests:DragDropControllerTests
  */
 @SmallTest
 @Presubmit
@@ -146,12 +146,12 @@
      */
     private WindowState createDropTargetWindow(String name, int ownerId) {
         final Task task = new TaskBuilder(mSupervisor).setUserId(ownerId).build();
-        final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task)
-                .setUseProcess(mProcess).build();
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess(
+                mProcess).build();
 
         // Use a new TestIWindow so we don't collect events for other windows
-        final WindowState window = createWindow(
-                null, TYPE_BASE_APPLICATION, activity, name, ownerId, false, new TestIWindow());
+        final WindowState window = createWindow(null, TYPE_BASE_APPLICATION, activity, name,
+                ownerId, false, new TestIWindow());
         InputChannel channel = new InputChannel();
         window.openInputChannel(channel);
         window.mHasSurface = true;
@@ -173,12 +173,11 @@
     @Before
     public void setUp() throws Exception {
         mTarget = new TestDragDropController(mWm, mWm.mH.getLooper());
-        mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc",
-                TEST_PID, TEST_UID);
+        mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc", TEST_PID, TEST_UID);
         mWindow = createDropTargetWindow("Drag test window", 0);
         doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0);
-        when(mWm.mInputManager.startDragAndDrop(any(IBinder.class),
-                any(IBinder.class))).thenReturn(true);
+        when(mWm.mInputManager.startDragAndDrop(any(IBinder.class), any(IBinder.class))).thenReturn(
+                true);
 
         mWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow);
     }
@@ -286,16 +285,15 @@
                     // Verify the start-drag event is sent for the local and global intercept window
                     // but not the other window
                     assertTrue(nonLocalWindowDragEvents.isEmpty());
-                    assertTrue(localWindowDragEvents.get(0).getAction()
-                            == ACTION_DRAG_STARTED);
+                    assertTrue(localWindowDragEvents.get(0).getAction() == ACTION_DRAG_STARTED);
                     assertTrue(globalInterceptWindowDragEvents.get(0).getAction()
                             == ACTION_DRAG_STARTED);
 
                     // Verify that only the global intercept window receives the clip data with the
                     // resolved activity info for the drag
                     assertNull(localWindowDragEvents.get(0).getClipData());
-                    assertTrue(globalInterceptWindowDragEvents.get(0).getClipData()
-                            .willParcelWithActivityInfo());
+                    assertTrue(globalInterceptWindowDragEvents.get(
+                            0).getClipData().willParcelWithActivityInfo());
 
                     mTarget.reportDropWindow(globalInterceptWindow.mInputChannelToken, 0, 0);
                     mTarget.handleMotionEvent(false, 0, 0);
@@ -330,9 +328,8 @@
                     // Verify the start-drag event has the drag flags
                     final DragEvent dragEvent = dragEvents.get(0);
                     assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED);
-                    assertTrue(dragEvent.getDragFlags() ==
-                            (View.DRAG_FLAG_GLOBAL
-                                    | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG));
+                    assertTrue(dragEvent.getDragFlags() == (View.DRAG_FLAG_GLOBAL
+                            | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG));
 
                     try {
                         mTarget.mDeferDragStateClosed = true;
@@ -340,9 +337,8 @@
                         // // Verify the drop event does not have the drag flags
                         mTarget.handleMotionEvent(false, 0, 0);
                         final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1);
-                        assertTrue(dropEvent.getDragFlags() ==
-                                (View.DRAG_FLAG_GLOBAL
-                                        | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG));
+                        assertTrue(dropEvent.getDragFlags() == (View.DRAG_FLAG_GLOBAL
+                                | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG));
 
                         mTarget.reportDropResult(iwindow, true);
                     } finally {
@@ -385,16 +381,15 @@
             data.putExtra(Intent.EXTRA_USER, user);
         }
         final ClipData clipData = new ClipData(
-                new ClipDescription("drag", new String[] {
-                        MIMETYPE_APPLICATION_ACTIVITY}),
+                new ClipDescription("drag", new String[]{MIMETYPE_APPLICATION_ACTIVITY}),
                 new ClipData.Item(data));
         return clipData;
     }
 
     @Test
     public void testValidateAppShortcutArguments() {
-        doReturn(PERMISSION_GRANTED).when(mWm.mContext)
-                .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
+        doReturn(PERMISSION_GRANTED).when(mWm.mContext).checkCallingOrSelfPermission(
+                eq(START_TASKS_FROM_RECENTS));
         final Session session = createTestSession(mAtm);
         try {
             session.validateAndResolveDragMimeTypeExtras(
@@ -414,8 +409,8 @@
         }
         try {
             session.validateAndResolveDragMimeTypeExtras(
-                    createClipDataForShortcut("test_package", "test_shortcut_id", null),
-                    TEST_UID, TEST_PID, TEST_PACKAGE);
+                    createClipDataForShortcut("test_package", "test_shortcut_id", null), TEST_UID,
+                    TEST_PID, TEST_PACKAGE);
             fail("Expected failure without package name");
         } catch (IllegalArgumentException e) {
             // Expected failure
@@ -424,8 +419,8 @@
 
     @Test
     public void testValidateProfileAppShortcutArguments_notCallingUid() {
-        doReturn(PERMISSION_GRANTED).when(mWm.mContext)
-                .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
+        doReturn(PERMISSION_GRANTED).when(mWm.mContext).checkCallingOrSelfPermission(
+                eq(START_TASKS_FROM_RECENTS));
         final Session session = createTestSession(mAtm);
         final ShortcutServiceInternal shortcutService = mock(ShortcutServiceInternal.class);
         final Intent[] shortcutIntents = new Intent[1];
@@ -438,10 +433,9 @@
         ArgumentCaptor<Integer> callingUser = ArgumentCaptor.forClass(Integer.class);
         session.validateAndResolveDragMimeTypeExtras(
                 createClipDataForShortcut("test_package", "test_shortcut_id",
-                        mock(UserHandle.class)),
-                TEST_PROFILE_UID, TEST_PID, TEST_PACKAGE);
-        verify(shortcutService).createShortcutIntents(callingUser.capture(), any(),
-                any(), any(), anyInt(), anyInt(), anyInt());
+                        mock(UserHandle.class)), TEST_PROFILE_UID, TEST_PID, TEST_PACKAGE);
+        verify(shortcutService).createShortcutIntents(callingUser.capture(), any(), any(), any(),
+                anyInt(), anyInt(), anyInt());
         assertTrue(callingUser.getValue() == UserHandle.getUserId(TEST_PROFILE_UID));
     }
 
@@ -458,20 +452,19 @@
             data.putExtra(Intent.EXTRA_USER, user);
         }
         final ClipData clipData = new ClipData(
-                new ClipDescription("drag", new String[] {
-                        MIMETYPE_APPLICATION_SHORTCUT}),
+                new ClipDescription("drag", new String[]{MIMETYPE_APPLICATION_SHORTCUT}),
                 new ClipData.Item(data));
         return clipData;
     }
 
     @Test
     public void testValidateAppTaskArguments() {
-        doReturn(PERMISSION_GRANTED).when(mWm.mContext)
-                .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
+        doReturn(PERMISSION_GRANTED).when(mWm.mContext).checkCallingOrSelfPermission(
+                eq(START_TASKS_FROM_RECENTS));
         final Session session = createTestSession(mAtm);
         try {
             final ClipData clipData = new ClipData(
-                    new ClipDescription("drag", new String[] { MIMETYPE_APPLICATION_TASK }),
+                    new ClipDescription("drag", new String[]{MIMETYPE_APPLICATION_TASK}),
                     new ClipData.Item(new Intent()));
 
             session.validateAndResolveDragMimeTypeExtras(clipData, TEST_UID, TEST_PID,
@@ -496,8 +489,8 @@
 
     @Test
     public void testValidateFlagsWithPermission() {
-        doReturn(PERMISSION_GRANTED).when(mWm.mContext)
-                .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
+        doReturn(PERMISSION_GRANTED).when(mWm.mContext).checkCallingOrSelfPermission(
+                eq(START_TASKS_FROM_RECENTS));
         final Session session = createTestSession(mAtm);
         try {
             session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION,
@@ -533,8 +526,8 @@
 
                     // Verify the DRAG_ENDED event does NOT include the drag surface
                     final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1);
-                    assertTrue(dragEvents.get(dragEvents.size() - 1).getAction()
-                            == ACTION_DRAG_ENDED);
+                    assertTrue(
+                            dragEvents.get(dragEvents.size() - 1).getAction() == ACTION_DRAG_ENDED);
                     assertTrue(dropEvent.getDragSurface() == null);
                 });
     }
@@ -564,8 +557,8 @@
 
                     // Verify the DRAG_ENDED event includes the drag surface
                     final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1);
-                    assertTrue(dragEvents.get(dragEvents.size() - 1).getAction()
-                            == ACTION_DRAG_ENDED);
+                    assertTrue(
+                            dragEvents.get(dragEvents.size() - 1).getAction() == ACTION_DRAG_ENDED);
                     assertTrue(dropEvent.getDragSurface() != null);
                 });
     }
@@ -591,18 +584,18 @@
         final int invalidXY = 100_000;
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
                 ClipData.newPlainText("label", "Test"), () -> {
-            // Trigger an unhandled drop and verify the global drag listener was called
-            mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY);
-            mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
-            mTarget.reportDropResult(mWindow.mClient, false);
-            mTarget.onUnhandledDropCallback(true);
-            mToken = null;
-            try {
-                verify(listener, times(1)).onUnhandledDrop(any(), any());
-            } catch (RemoteException e) {
-                fail("Failed to verify unhandled drop: " + e);
-            }
-        });
+                    // Trigger an unhandled drop and verify the global drag listener was called
+                    mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY);
+                    mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+                    mTarget.reportDropResult(mWindow.mClient, false);
+                    mTarget.onUnhandledDropCallback(true);
+                    mToken = null;
+                    try {
+                        verify(listener, times(1)).onUnhandledDrop(any(), any());
+                    } catch (RemoteException e) {
+                        fail("Failed to verify unhandled drop: " + e);
+                    }
+                });
     }
 
     @Test
@@ -615,17 +608,17 @@
         final int invalidXY = 100_000;
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
                 ClipData.newPlainText("label", "Test"), () -> {
-            // Trigger an unhandled drop and verify the global drag listener was called
-            mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
-            mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
-            mTarget.onUnhandledDropCallback(true);
-            mToken = null;
-            try {
-                verify(listener, times(1)).onUnhandledDrop(any(), any());
-            } catch (RemoteException e) {
-                fail("Failed to verify unhandled drop: " + e);
-            }
-        });
+                    // Trigger an unhandled drop and verify the global drag listener was called
+                    mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
+                    mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+                    mTarget.onUnhandledDropCallback(true);
+                    mToken = null;
+                    try {
+                        verify(listener, times(1)).onUnhandledDrop(any(), any());
+                    } catch (RemoteException e) {
+                        fail("Failed to verify unhandled drop: " + e);
+                    }
+                });
     }
 
     @Test
@@ -636,18 +629,17 @@
         doReturn(mock(Binder.class)).when(listener).asBinder();
         mTarget.setGlobalDragListener(listener);
         final int invalidXY = 100_000;
-        startDrag(View.DRAG_FLAG_GLOBAL,
-                ClipData.newPlainText("label", "Test"), () -> {
-                    // Trigger an unhandled drop and verify the global drag listener was not called
-                    mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
-                    mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
-                    mToken = null;
-                    try {
-                        verify(listener, never()).onUnhandledDrop(any(), any());
-                    } catch (RemoteException e) {
-                        fail("Failed to verify unhandled drop: " + e);
-                    }
-                });
+        startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+            // Trigger an unhandled drop and verify the global drag listener was not called
+            mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
+            mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+            mToken = null;
+            try {
+                verify(listener, never()).onUnhandledDrop(any(), any());
+            } catch (RemoteException e) {
+                fail("Failed to verify unhandled drop: " + e);
+            }
+        });
     }
 
     @Test
@@ -660,20 +652,22 @@
         final int invalidXY = 100_000;
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
                 ClipData.newPlainText("label", "Test"), () -> {
-            // Trigger an unhandled drop and verify the global drag listener was called
-            mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
-            mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+                    // Trigger an unhandled drop and verify the global drag listener was called
+                    mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
+                    mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
 
-            // Verify that the unhandled drop listener callback timeout has been scheduled
-            final Handler handler = mTarget.getHandler();
-            assertTrue(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+                    // Verify that the unhandled drop listener callback timeout has been scheduled
+                    final Handler handler = mTarget.getHandler();
+                    assertTrue(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
 
-            // Force trigger the timeout and verify that it actually cleans up the drag & timeout
-            handler.handleMessage(Message.obtain(handler, MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
-            assertFalse(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
-            assertFalse(mTarget.dragDropActiveLocked());
-            mToken = null;
-        });
+                    // Force trigger the timeout and verify that it actually cleans up the drag &
+                    // timeout
+                    handler.handleMessage(
+                            Message.obtain(handler, MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+                    assertFalse(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+                    assertFalse(mTarget.dragDropActiveLocked());
+                    mToken = null;
+                });
     }
 
     private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) {
@@ -690,15 +684,13 @@
     private void startDrag(int flag, ClipData data, Runnable r) {
         final SurfaceSession appSession = new SurfaceSession();
         try {
-            final SurfaceControl surface = new SurfaceControl.Builder(appSession)
-                    .setName("drag surface")
-                    .setBufferSize(100, 100)
-                    .setFormat(PixelFormat.TRANSLUCENT)
-                    .build();
+            final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName(
+                    "drag surface").setBufferSize(100, 100).setFormat(
+                    PixelFormat.TRANSLUCENT).build();
 
             assertTrue(mWm.mInputManager.startDragAndDrop(new Binder(), new Binder()));
-            mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient,
-                    flag, surface, 0, 0, 0, 0, 0, 0, 0, data);
+            mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0,
+                    0, 0, data);
             assertNotNull(mToken);
 
             r.run();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index b27025c..b61dada 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -89,6 +89,7 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.service.voice.IVoiceInteractionSession;
+import android.tools.function.Supplier;
 import android.util.MergedConfiguration;
 import android.util.SparseArray;
 import android.view.Display;
@@ -1804,6 +1805,124 @@
         }
     }
 
+    protected WindowStateBuilder newWindowBuilder(String name, int type) {
+        return new WindowStateBuilder(name, type, mWm, mDisplayContent, mIWindow,
+                this::getTestSession, this::createWindowToken);
+    }
+
+    /**
+     * Builder for creating new window.
+     */
+    protected static class WindowStateBuilder {
+        private final String mName;
+        private final int mType;
+        private final WindowManagerService mWm;
+        private final DisplayContent mDefaultTargetDisplay;
+        private final Supplier<WindowToken, Session> mSessionSupplier;
+        private final WindowTokenCreator mWindowTokenCreator;
+
+        private int mActivityType = ACTIVITY_TYPE_STANDARD;
+        private IWindow mClientWindow;
+        private boolean mOwnerCanAddInternalSystemWindow = false;
+        private int mOwnerId = 0;
+        private WindowState mParent;
+        private DisplayContent mTargetDisplay;
+        private int mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+        private WindowToken mWindowToken;
+
+        WindowStateBuilder(String name, int type, WindowManagerService windowManagerService,
+                DisplayContent dc, IWindow iWindow, Supplier<WindowToken, Session> sessionSupplier,
+                WindowTokenCreator windowTokenCreator) {
+            mName = name;
+            mType = type;
+            mClientWindow = iWindow;
+            mDefaultTargetDisplay = dc;
+            mSessionSupplier = sessionSupplier;
+            mWindowTokenCreator = windowTokenCreator;
+            mWm = windowManagerService;
+        }
+
+        WindowStateBuilder setActivityType(int activityType) {
+            mActivityType = activityType;
+            return this;
+        }
+
+        WindowStateBuilder setClientWindow(IWindow clientWindow) {
+            mClientWindow = clientWindow;
+            return this;
+        }
+
+        WindowStateBuilder setDisplay(DisplayContent displayContent) {
+            mTargetDisplay = displayContent;
+            return this;
+        }
+
+        WindowStateBuilder setOwnerCanAddInternalSystemWindow(
+                boolean ownerCanAddInternalSystemWindow) {
+            mOwnerCanAddInternalSystemWindow = ownerCanAddInternalSystemWindow;
+            return this;
+        }
+
+        WindowStateBuilder setOwnerId(int ownerId) {
+            mOwnerId = ownerId;
+            return this;
+        }
+
+        WindowStateBuilder setParent(WindowState parent) {
+            mParent = parent;
+            return this;
+        }
+
+        WindowStateBuilder setWindowToken(WindowToken token) {
+            mWindowToken = token;
+            return this;
+        }
+
+        WindowStateBuilder setWindowingMode(int windowingMode) {
+            mWindowingMode = windowingMode;
+            return this;
+        }
+
+        WindowState build() {
+            SystemServicesTestRule.checkHoldsLock(mWm.mGlobalLock);
+
+            final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(mType);
+            attrs.setTitle(mName);
+            attrs.packageName = "test";
+
+            assertFalse(
+                    "targetDisplay shouldn't be specified together with windowToken, since"
+                            + " windowToken will be derived from targetDisplay.",
+                    mWindowToken != null && mTargetDisplay != null);
+
+            if (mWindowToken == null) {
+                if (mTargetDisplay != null) {
+                    mWindowToken = mWindowTokenCreator.createWindowToken(mTargetDisplay,
+                            mWindowingMode, mActivityType, mType);
+                } else if (mParent != null) {
+                    mWindowToken = mParent.mToken;
+                } else {
+                    // Use default mDisplayContent as window token.
+                    mWindowToken = mWindowTokenCreator.createWindowToken(mDefaultTargetDisplay,
+                            mWindowingMode, mActivityType, mType);
+                }
+            }
+
+            final WindowState w = new WindowState(mWm, mSessionSupplier.get(mWindowToken),
+                    mClientWindow, mWindowToken, mParent, OP_NONE, attrs, VISIBLE, mOwnerId,
+                    UserHandle.getUserId(mOwnerId), mOwnerCanAddInternalSystemWindow);
+            // TODO: Probably better to make this call in the WindowState ctor to avoid errors with
+            // adding it to the token...
+            mWindowToken.addWindow(w);
+            return w;
+        }
+
+        interface WindowTokenCreator {
+            WindowToken createWindowToken(DisplayContent dc, int windowingMode, int activityType,
+                    int type);
+        }
+    }
+
     static class TestStartingWindowOrganizer extends WindowOrganizerTests.StubOrganizer {
         private final ActivityTaskManagerService mAtm;
         private final WindowManagerService mWMService;
diff --git a/services/usb/java/com/android/server/usb/UsbAlsaDevice.java b/services/usb/java/com/android/server/usb/UsbAlsaDevice.java
index c508fa9..ce3cd29 100644
--- a/services/usb/java/com/android/server/usb/UsbAlsaDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbAlsaDevice.java
@@ -26,6 +26,7 @@
 
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.server.audio.AudioService;
+import com.android.server.usb.flags.Flags;
 
 import java.util.Arrays;
 
@@ -211,6 +212,9 @@
         mIsSelected[direction] = true;
         mState[direction] = 0;
         startJackDetect();
+        if (direction == OUTPUT && Flags.maximizeUsbAudioVolumeWhenConnecting()) {
+            nativeSetVolume(mCardNum, 1.0f /*volume*/);
+        }
         updateWiredDeviceConnectionState(direction, true /*enable*/);
     }
 
@@ -412,5 +416,7 @@
 
         return result;
     }
+
+    private native void nativeSetVolume(int card, float volume);
 }
 
diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
index a2d0efd..dfbd74c 100644
--- a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
+++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
@@ -21,3 +21,10 @@
     description: "This flag checks if phone is unlocked after boot"
     bug: "73654179"
 }
+
+flag {
+    name: "maximize_usb_audio_volume_when_connecting"
+    namespace: "usb"
+    description: "This flag maximizes the usb audio volume when it is connected"
+    bug: "245041322"
+}
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java
index 135f710..9ac08ed 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java
@@ -203,12 +203,10 @@
             transaction
                     .setBuffer(mSurfaceControls[i], buffer)
                     .setPictureProfileHandle(mSurfaceControls[i], handle)
-                    .setContentPriority(mSurfaceControls[i], 0);
+                    .setContentPriority(mSurfaceControls[i], 1);
         }
-        // Make the first layer low priority (high value)
-        transaction.setContentPriority(mSurfaceControls[0], 2);
-        // Make the last layer higher priority (lower value)
-        transaction.setContentPriority(mSurfaceControls[maxPictureProfiles], 1);
+        transaction.setContentPriority(mSurfaceControls[0], -1);
+        transaction.setContentPriority(mSurfaceControls[maxPictureProfiles], 0);
         transaction.apply();
 
         pictures = pollMs(picturesQueue, 200);
@@ -219,8 +217,8 @@
         assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId()))
                 .containsExactlyElementsIn(toIterableRange(2, maxPictureProfiles + 1));
 
-        // Change priority and ensure that the first layer gets access
-        new SurfaceControl.Transaction().setContentPriority(mSurfaceControls[0], 0).apply();
+        // Elevate priority for the first layer and verify it gets to use a profile
+        new SurfaceControl.Transaction().setContentPriority(mSurfaceControls[0], 2).apply();
         pictures = pollMs(picturesQueue, 200);
         assertThat(pictures).isNotNull();
         // Expect all but the last layer to be listed as an active picture